@triagly/sdk 0.1.1-beta.1

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.
@@ -0,0 +1,1340 @@
1
+ // Feedback Widget UI
2
+ class FeedbackWidget {
3
+ constructor(config) {
4
+ this.container = null;
5
+ this.isOpen = false;
6
+ this.previouslyFocusedElement = null;
7
+ this.focusableElements = [];
8
+ this.config = config;
9
+ }
10
+ /**
11
+ * Initialize the widget
12
+ */
13
+ init() {
14
+ this.createButton();
15
+ this.injectStyles();
16
+ }
17
+ /**
18
+ * Create the feedback button
19
+ */
20
+ createButton() {
21
+ const button = document.createElement('button');
22
+ button.id = 'triagly-button';
23
+ button.className = 'triagly-button';
24
+ // Button shape
25
+ const shape = this.config.buttonShape || 'rounded';
26
+ button.classList.add(`triagly-shape-${shape}`);
27
+ // Button orientation
28
+ const orientation = this.config.orientation || 'horizontal';
29
+ button.classList.add(`triagly-orientation-${orientation}`);
30
+ // Handle button text based on shape
31
+ const fullText = this.config.buttonText || '🐛 Feedback';
32
+ if (shape === 'circular') {
33
+ button.innerHTML = '🐛';
34
+ button.setAttribute('aria-label', fullText);
35
+ }
36
+ else if (shape === 'expandable') {
37
+ // Expandable starts with emoji, expands to full text on hover
38
+ button.innerHTML = '<span class="triagly-btn-icon">🐛</span><span class="triagly-btn-text"> Feedback</span>';
39
+ button.setAttribute('aria-label', fullText);
40
+ // Store custom text if provided
41
+ if (this.config.buttonText) {
42
+ const textSpan = button.querySelector('.triagly-btn-text');
43
+ if (textSpan) {
44
+ textSpan.textContent = ' ' + this.config.buttonText.replace('🐛', '').trim();
45
+ }
46
+ }
47
+ }
48
+ else {
49
+ button.innerHTML = fullText;
50
+ }
51
+ button.onclick = () => this.toggle();
52
+ // Position button
53
+ const position = this.config.position || 'bottom-right';
54
+ button.classList.add(`triagly-${position}`);
55
+ // For expandable buttons, set expansion direction based on position
56
+ if (shape === 'expandable') {
57
+ if (position.includes('right')) {
58
+ button.classList.add('triagly-expand-left');
59
+ }
60
+ else if (position.includes('left')) {
61
+ button.classList.add('triagly-expand-right');
62
+ }
63
+ }
64
+ // Apply custom offsets if provided
65
+ if (this.config.offsetX) {
66
+ if (position.includes('right')) {
67
+ button.style.right = this.config.offsetX;
68
+ }
69
+ else if (position.includes('left')) {
70
+ button.style.left = this.config.offsetX;
71
+ }
72
+ }
73
+ if (this.config.offsetY) {
74
+ if (position.includes('top')) {
75
+ button.style.top = this.config.offsetY;
76
+ }
77
+ else if (position.includes('bottom')) {
78
+ button.style.bottom = this.config.offsetY;
79
+ }
80
+ }
81
+ document.body.appendChild(button);
82
+ }
83
+ /**
84
+ * Toggle widget visibility
85
+ */
86
+ toggle() {
87
+ if (this.isOpen) {
88
+ this.close();
89
+ }
90
+ else {
91
+ this.open();
92
+ }
93
+ }
94
+ /**
95
+ * Open the widget
96
+ */
97
+ open() {
98
+ if (this.isOpen)
99
+ return;
100
+ // Store currently focused element to restore later
101
+ this.previouslyFocusedElement = document.activeElement;
102
+ this.container = this.createContainer();
103
+ document.body.appendChild(this.container);
104
+ this.isOpen = true;
105
+ // Call onOpen callback
106
+ if (this.config.onOpen) {
107
+ this.config.onOpen();
108
+ }
109
+ // Set up keyboard and focus after DOM is ready
110
+ setTimeout(() => {
111
+ // Set up keyboard event listener
112
+ this.setupKeyboardEvents();
113
+ // Set up focus trap
114
+ this.setupFocusTrap();
115
+ // Focus on title field
116
+ const titleInput = this.container?.querySelector('input[type="text"]');
117
+ titleInput?.focus();
118
+ }, 0);
119
+ }
120
+ /**
121
+ * Close the widget
122
+ */
123
+ close(reason) {
124
+ if (!this.isOpen || !this.container)
125
+ return;
126
+ // Clean up tab handler (must match capture phase used when adding)
127
+ const tabHandler = this.container._tabHandler;
128
+ if (tabHandler) {
129
+ document.removeEventListener('keydown', tabHandler, true);
130
+ }
131
+ this.container.remove();
132
+ this.container = null;
133
+ this.isOpen = false;
134
+ // Restore focus to previously focused element
135
+ if (this.previouslyFocusedElement) {
136
+ this.previouslyFocusedElement.focus();
137
+ this.previouslyFocusedElement = null;
138
+ }
139
+ // Call specific callbacks based on reason
140
+ if (reason === 'cancel' && this.config.onCancel) {
141
+ this.config.onCancel();
142
+ }
143
+ else if (reason === 'dismiss' && this.config.onDismiss) {
144
+ this.config.onDismiss();
145
+ }
146
+ else if (reason === 'overlay' && this.config.onOverlayClick) {
147
+ this.config.onOverlayClick();
148
+ }
149
+ // Always call general onClose callback (backward compatible)
150
+ if (this.config.onClose) {
151
+ this.config.onClose();
152
+ }
153
+ }
154
+ /**
155
+ * Create the widget container
156
+ */
157
+ createContainer() {
158
+ const overlay = document.createElement('div');
159
+ overlay.className = 'triagly-overlay';
160
+ overlay.setAttribute('role', 'dialog');
161
+ overlay.setAttribute('aria-modal', 'true');
162
+ overlay.setAttribute('aria-labelledby', 'triagly-modal-title');
163
+ overlay.onclick = (e) => {
164
+ if (e.target === overlay)
165
+ this.close('overlay');
166
+ };
167
+ const modal = document.createElement('div');
168
+ modal.className = 'triagly-modal';
169
+ modal.setAttribute('role', 'document');
170
+ const header = document.createElement('div');
171
+ header.className = 'triagly-header';
172
+ header.innerHTML = `
173
+ <h3 id="triagly-modal-title">Send Feedback</h3>
174
+ <button type="button" class="triagly-close" aria-label="Close feedback form">×</button>
175
+ `;
176
+ const closeBtn = header.querySelector('.triagly-close');
177
+ closeBtn?.addEventListener('click', () => this.close('dismiss'));
178
+ const form = document.createElement('form');
179
+ form.className = 'triagly-form';
180
+ form.innerHTML = `
181
+ <div class="triagly-field">
182
+ <label for="triagly-title">Title (optional)</label>
183
+ <input
184
+ type="text"
185
+ id="triagly-title"
186
+ placeholder="Brief summary of your feedback"
187
+ />
188
+ </div>
189
+
190
+ <div class="triagly-field">
191
+ <label for="triagly-description">Description *</label>
192
+ <textarea
193
+ id="triagly-description"
194
+ required
195
+ rows="5"
196
+ placeholder="${this.config.placeholderText || 'Describe what happened...'}"
197
+ ></textarea>
198
+ </div>
199
+
200
+ <div class="triagly-field">
201
+ <label for="triagly-email">Email (optional)</label>
202
+ <input
203
+ type="email"
204
+ id="triagly-email"
205
+ placeholder="your@email.com"
206
+ />
207
+ </div>
208
+
209
+ <div class="triagly-field triagly-checkbox">
210
+ <label>
211
+ <input type="checkbox" id="triagly-screenshot" checked />
212
+ <span>Include screenshot</span>
213
+ </label>
214
+ </div>
215
+
216
+ ${this.config.turnstileSiteKey ? `
217
+ <div class="triagly-field triagly-turnstile">
218
+ <div class="cf-turnstile" data-sitekey="${this.config.turnstileSiteKey}" data-theme="light"></div>
219
+ </div>
220
+ ` : ''}
221
+
222
+ <div class="triagly-actions">
223
+ <button type="button" class="triagly-btn-secondary" id="triagly-cancel" aria-label="Cancel and close feedback form">
224
+ Cancel
225
+ </button>
226
+ <button type="submit" class="triagly-btn-primary" aria-label="Submit feedback">
227
+ Send Feedback
228
+ </button>
229
+ </div>
230
+
231
+ <div class="triagly-status" id="triagly-status" role="status" aria-live="polite"></div>
232
+ `;
233
+ const cancelBtn = form.querySelector('#triagly-cancel');
234
+ cancelBtn?.addEventListener('click', () => this.close('cancel'));
235
+ form.onsubmit = (e) => {
236
+ e.preventDefault();
237
+ this.handleSubmit(form);
238
+ };
239
+ modal.appendChild(header);
240
+ modal.appendChild(form);
241
+ overlay.appendChild(modal);
242
+ // Render Turnstile widget if available
243
+ if (this.config.turnstileSiteKey) {
244
+ setTimeout(() => {
245
+ this.renderTurnstileWidget(form);
246
+ }, 100);
247
+ }
248
+ return overlay;
249
+ }
250
+ /**
251
+ * Render Cloudflare Turnstile widget
252
+ */
253
+ renderTurnstileWidget(form) {
254
+ const turnstileContainer = form.querySelector('.cf-turnstile');
255
+ if (!turnstileContainer)
256
+ return;
257
+ // Check if Turnstile script is loaded
258
+ if (!window.turnstile) {
259
+ console.warn('Triagly: Turnstile script not loaded. Please include: <script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>');
260
+ return;
261
+ }
262
+ try {
263
+ const widgetId = window.turnstile.render(turnstileContainer, {
264
+ sitekey: this.config.turnstileSiteKey,
265
+ theme: this.config.theme === 'dark' ? 'dark' : 'light',
266
+ callback: (token) => {
267
+ // Store token in a data attribute for easy retrieval
268
+ turnstileContainer.setAttribute('data-turnstile-response', token);
269
+ turnstileContainer.setAttribute('data-widget-id', widgetId);
270
+ },
271
+ 'error-callback': () => {
272
+ console.error('Triagly: Turnstile widget error');
273
+ },
274
+ 'expired-callback': () => {
275
+ // Clear stored token when it expires
276
+ turnstileContainer.removeAttribute('data-turnstile-response');
277
+ },
278
+ });
279
+ turnstileContainer.setAttribute('data-widget-id', widgetId);
280
+ }
281
+ catch (error) {
282
+ console.error('Triagly: Failed to render Turnstile widget:', error);
283
+ }
284
+ }
285
+ /**
286
+ * Handle form submission
287
+ */
288
+ async handleSubmit(form) {
289
+ const titleInput = form.querySelector('#triagly-title');
290
+ const descInput = form.querySelector('#triagly-description');
291
+ const emailInput = form.querySelector('#triagly-email');
292
+ const screenshotCheckbox = form.querySelector('#triagly-screenshot');
293
+ const statusDiv = form.querySelector('#triagly-status');
294
+ const submitBtn = form.querySelector('button[type="submit"]');
295
+ const turnstileContainer = form.querySelector('.cf-turnstile');
296
+ // Disable form
297
+ submitBtn.disabled = true;
298
+ submitBtn.textContent = 'Sending...';
299
+ try {
300
+ // Get Turnstile token if widget is present
301
+ let turnstileToken;
302
+ if (turnstileContainer) {
303
+ turnstileToken = turnstileContainer.getAttribute('data-turnstile-response') || undefined;
304
+ }
305
+ const data = {
306
+ title: titleInput.value.trim() || undefined,
307
+ description: descInput.value.trim(),
308
+ reporterEmail: emailInput.value.trim() || undefined,
309
+ includeScreenshot: screenshotCheckbox.checked,
310
+ turnstileToken,
311
+ };
312
+ // Create a promise that waits for actual submission result
313
+ const submissionPromise = new Promise((resolve, reject) => {
314
+ const handleSuccess = () => {
315
+ document.removeEventListener('triagly:success', handleSuccess);
316
+ document.removeEventListener('triagly:error', handleError);
317
+ resolve();
318
+ };
319
+ const handleError = (e) => {
320
+ document.removeEventListener('triagly:success', handleSuccess);
321
+ document.removeEventListener('triagly:error', handleError);
322
+ reject(e.detail);
323
+ };
324
+ document.addEventListener('triagly:success', handleSuccess, { once: true });
325
+ document.addEventListener('triagly:error', handleError, { once: true });
326
+ // Set a timeout in case events don't fire
327
+ setTimeout(() => {
328
+ document.removeEventListener('triagly:success', handleSuccess);
329
+ document.removeEventListener('triagly:error', handleError);
330
+ reject(new Error('Submission timeout'));
331
+ }, 30000); // 30 second timeout
332
+ });
333
+ // Dispatch custom event for parent to handle
334
+ const event = new CustomEvent('triagly:submit', {
335
+ detail: data,
336
+ bubbles: true,
337
+ });
338
+ document.dispatchEvent(event);
339
+ // Wait for actual submission result
340
+ await submissionPromise;
341
+ // Show success
342
+ statusDiv.className = 'triagly-status triagly-success';
343
+ statusDiv.textContent = this.config.successMessage || 'Feedback sent successfully!';
344
+ // Close after delay
345
+ setTimeout(() => {
346
+ this.close();
347
+ }, 2000);
348
+ }
349
+ catch (error) {
350
+ // Show error with actual error message
351
+ statusDiv.className = 'triagly-status triagly-error';
352
+ const errorMessage = error instanceof Error ? error.message :
353
+ (this.config.errorMessage || 'Failed to send feedback. Please try again.');
354
+ statusDiv.textContent = errorMessage;
355
+ // Re-enable form
356
+ submitBtn.disabled = false;
357
+ submitBtn.textContent = 'Send Feedback';
358
+ }
359
+ }
360
+ /**
361
+ * Inject widget styles
362
+ */
363
+ injectStyles() {
364
+ if (document.getElementById('triagly-styles'))
365
+ return;
366
+ const style = document.createElement('style');
367
+ style.id = 'triagly-styles';
368
+ style.textContent = `
369
+ .triagly-button {
370
+ position: fixed;
371
+ z-index: 999999;
372
+ padding: 12px 20px;
373
+ background: var(--triagly-button-bg, #6366f1);
374
+ color: var(--triagly-button-text, #ffffff);
375
+ border: none;
376
+ border-radius: var(--triagly-button-radius, 8px);
377
+ font-size: 14px;
378
+ font-weight: 500;
379
+ cursor: pointer;
380
+ box-shadow: var(--triagly-button-shadow, 0 4px 12px rgba(99, 102, 241, 0.3));
381
+ transition: all 0.2s;
382
+ }
383
+
384
+ .triagly-button:hover {
385
+ background: var(--triagly-button-bg-hover, #4f46e5);
386
+ transform: translateY(-2px);
387
+ box-shadow: var(--triagly-button-shadow-hover, 0 6px 16px rgba(99, 102, 241, 0.4));
388
+ }
389
+
390
+ /* Prevent expandable buttons from shifting on hover */
391
+ .triagly-button.triagly-shape-expandable:hover {
392
+ transform: translateY(0) !important;
393
+ }
394
+
395
+ .triagly-bottom-right { bottom: 20px; right: 20px; }
396
+ .triagly-bottom-left { bottom: 20px; left: 20px; }
397
+ .triagly-top-right { top: 20px; right: 20px; }
398
+ .triagly-top-left { top: 20px; left: 20px; }
399
+
400
+ /* Edge-aligned positions (0 offset from edges) */
401
+ .triagly-edge-bottom-right { bottom: 0; right: 0; }
402
+ .triagly-edge-bottom-left { bottom: 0; left: 0; }
403
+ .triagly-edge-top-right { top: 0; right: 0; }
404
+ .triagly-edge-top-left { top: 0; left: 0; }
405
+ .triagly-edge-right { top: 50%; right: 0; transform: translateY(-50%); }
406
+ .triagly-edge-left { top: 50%; left: 0; transform: translateY(-50%); }
407
+ .triagly-edge-top { top: 0; left: 50%; transform: translateX(-50%); }
408
+ .triagly-edge-bottom { bottom: 0; left: 50%; transform: translateX(-50%); }
409
+
410
+ /* Button shapes */
411
+ .triagly-shape-rounded {
412
+ border-radius: var(--triagly-button-radius, 8px);
413
+ }
414
+ .triagly-shape-circular {
415
+ border-radius: 50%;
416
+ width: 60px;
417
+ height: 60px;
418
+ padding: 0;
419
+ font-size: 24px;
420
+ display: flex;
421
+ align-items: center;
422
+ justify-content: center;
423
+ }
424
+ .triagly-shape-square {
425
+ border-radius: 0;
426
+ }
427
+ .triagly-shape-pill {
428
+ border-radius: 30px;
429
+ }
430
+ .triagly-shape-expandable {
431
+ border-radius: 50%;
432
+ width: 60px;
433
+ height: 60px;
434
+ min-width: 60px;
435
+ padding: 0;
436
+ font-size: 24px;
437
+ display: flex;
438
+ align-items: center;
439
+ justify-content: center;
440
+ overflow: hidden;
441
+ transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1),
442
+ border-radius 0.3s cubic-bezier(0.4, 0, 0.2, 1),
443
+ padding 0.3s cubic-bezier(0.4, 0, 0.2, 1),
444
+ background 0.2s,
445
+ box-shadow 0.2s;
446
+ white-space: nowrap;
447
+ }
448
+
449
+ /* Expansion direction - expands left for right-positioned buttons */
450
+ .triagly-shape-expandable.triagly-expand-left {
451
+ flex-direction: row-reverse;
452
+ }
453
+
454
+ /* Expansion direction - expands right for left-positioned buttons */
455
+ .triagly-shape-expandable.triagly-expand-right {
456
+ flex-direction: row;
457
+ }
458
+
459
+ .triagly-shape-expandable .triagly-btn-icon {
460
+ display: inline-block;
461
+ flex-shrink: 0;
462
+ transition: margin 0.3s cubic-bezier(0.4, 0, 0.2, 1);
463
+ }
464
+ .triagly-shape-expandable .triagly-btn-text {
465
+ display: inline-block;
466
+ width: 0;
467
+ opacity: 0;
468
+ overflow: hidden;
469
+ font-size: 14px;
470
+ font-weight: 500;
471
+ transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1),
472
+ opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1);
473
+ }
474
+
475
+ /* Hover state */
476
+ .triagly-shape-expandable:hover {
477
+ width: auto;
478
+ min-width: auto;
479
+ padding: 12px 20px;
480
+ border-radius: 30px;
481
+ background: var(--triagly-button-bg-hover, #4f46e5);
482
+ box-shadow: var(--triagly-button-shadow-hover, 0 6px 16px rgba(99, 102, 241, 0.4));
483
+ }
484
+ .triagly-shape-expandable:hover .triagly-btn-text {
485
+ width: auto;
486
+ opacity: 1;
487
+ }
488
+
489
+ /* Button orientations */
490
+ .triagly-orientation-horizontal {
491
+ writing-mode: horizontal-tb;
492
+ }
493
+ .triagly-orientation-vertical {
494
+ writing-mode: vertical-rl;
495
+ text-orientation: mixed;
496
+ }
497
+
498
+ .triagly-overlay {
499
+ position: fixed;
500
+ top: 0;
501
+ left: 0;
502
+ right: 0;
503
+ bottom: 0;
504
+ background: var(--triagly-overlay-bg, rgba(0, 0, 0, 0.5));
505
+ z-index: 1000000;
506
+ display: flex;
507
+ align-items: center;
508
+ justify-content: center;
509
+ animation: triagly-fadeIn 0.2s;
510
+ }
511
+
512
+ @keyframes triagly-fadeIn {
513
+ from { opacity: 0; }
514
+ to { opacity: 1; }
515
+ }
516
+
517
+ .triagly-modal {
518
+ background: var(--triagly-modal-bg, #ffffff);
519
+ border-radius: var(--triagly-modal-radius, 12px);
520
+ width: 90%;
521
+ max-width: var(--triagly-modal-max-width, 500px);
522
+ max-height: 90vh;
523
+ overflow-y: auto;
524
+ box-shadow: var(--triagly-modal-shadow, 0 20px 60px rgba(0, 0, 0, 0.3));
525
+ animation: triagly-slideUp 0.3s;
526
+ }
527
+
528
+ @keyframes triagly-slideUp {
529
+ from {
530
+ opacity: 0;
531
+ transform: translateY(20px);
532
+ }
533
+ to {
534
+ opacity: 1;
535
+ transform: translateY(0);
536
+ }
537
+ }
538
+
539
+ .triagly-header {
540
+ display: flex;
541
+ justify-content: space-between;
542
+ align-items: center;
543
+ padding: 20px 24px;
544
+ background: var(--triagly-header-bg, #ffffff);
545
+ border-bottom: 1px solid var(--triagly-header-border, #e5e7eb);
546
+ }
547
+
548
+ .triagly-header h3 {
549
+ margin: 0;
550
+ font-size: 18px;
551
+ font-weight: 600;
552
+ color: var(--triagly-header-text, #111827);
553
+ }
554
+
555
+ .triagly-close {
556
+ background: none;
557
+ border: none;
558
+ font-size: 28px;
559
+ color: #6b7280;
560
+ cursor: pointer;
561
+ padding: 0;
562
+ width: 32px;
563
+ height: 32px;
564
+ display: flex;
565
+ align-items: center;
566
+ justify-content: center;
567
+ border-radius: 6px;
568
+ transition: all 0.2s;
569
+ }
570
+
571
+ .triagly-close:hover {
572
+ background: #f3f4f6;
573
+ color: #111827;
574
+ }
575
+
576
+ .triagly-form {
577
+ padding: 24px;
578
+ background: var(--triagly-form-bg, #ffffff);
579
+ }
580
+
581
+ .triagly-field {
582
+ margin-bottom: 16px;
583
+ }
584
+
585
+ .triagly-field label {
586
+ display: block;
587
+ margin-bottom: 6px;
588
+ font-size: 14px;
589
+ font-weight: 500;
590
+ color: var(--triagly-label-text, #374151);
591
+ }
592
+
593
+ .triagly-field input,
594
+ .triagly-field textarea {
595
+ width: 100%;
596
+ padding: 10px 12px;
597
+ background: var(--triagly-input-bg, #ffffff);
598
+ border: 1px solid var(--triagly-input-border, #d1d5db);
599
+ border-radius: var(--triagly-input-radius, 6px);
600
+ color: var(--triagly-input-text, #111827);
601
+ font-size: 14px;
602
+ font-family: inherit;
603
+ transition: border-color 0.2s;
604
+ box-sizing: border-box;
605
+ }
606
+
607
+ .triagly-field input:focus,
608
+ .triagly-field textarea:focus {
609
+ outline: none;
610
+ border-color: var(--triagly-input-border-focus, #6366f1);
611
+ box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
612
+ }
613
+
614
+ .triagly-checkbox label {
615
+ display: flex;
616
+ align-items: center;
617
+ gap: 8px;
618
+ cursor: pointer;
619
+ font-weight: 400;
620
+ }
621
+
622
+ .triagly-checkbox label span {
623
+ user-select: none;
624
+ }
625
+
626
+ .triagly-checkbox input {
627
+ width: 16px;
628
+ height: 16px;
629
+ margin: 0;
630
+ cursor: pointer;
631
+ }
632
+
633
+ /* Focus visible styles for accessibility */
634
+ .triagly-button:focus-visible,
635
+ .triagly-field input:focus-visible,
636
+ .triagly-field textarea:focus-visible,
637
+ .triagly-checkbox input:focus-visible,
638
+ .triagly-btn-primary:focus-visible,
639
+ .triagly-btn-secondary:focus-visible,
640
+ .triagly-close:focus-visible {
641
+ outline: 2px solid #6366f1;
642
+ outline-offset: 2px;
643
+ }
644
+
645
+ /* Checkbox label gets visual indicator when checkbox is focused */
646
+ .triagly-checkbox input:focus-visible + span {
647
+ text-decoration: underline;
648
+ }
649
+
650
+ .triagly-turnstile {
651
+ display: flex;
652
+ justify-content: center;
653
+ margin: 8px 0;
654
+ }
655
+
656
+ .triagly-actions {
657
+ display: flex;
658
+ gap: 12px;
659
+ margin-top: 24px;
660
+ }
661
+
662
+ .triagly-btn-primary,
663
+ .triagly-btn-secondary {
664
+ flex: 1;
665
+ padding: 10px 16px;
666
+ border-radius: var(--triagly-btn-radius, 6px);
667
+ font-size: 14px;
668
+ font-weight: 500;
669
+ cursor: pointer;
670
+ transition: all 0.2s;
671
+ border: none;
672
+ }
673
+
674
+ .triagly-btn-primary {
675
+ background: var(--triagly-btn-primary-bg, #6366f1);
676
+ color: var(--triagly-btn-primary-text, #ffffff);
677
+ }
678
+
679
+ .triagly-btn-primary:hover:not(:disabled) {
680
+ background: var(--triagly-btn-primary-bg-hover, #4f46e5);
681
+ }
682
+
683
+ .triagly-btn-primary:disabled {
684
+ opacity: 0.6;
685
+ cursor: not-allowed;
686
+ }
687
+
688
+ .triagly-btn-secondary {
689
+ background: var(--triagly-btn-secondary-bg, #f3f4f6);
690
+ color: var(--triagly-btn-secondary-text, #374151);
691
+ }
692
+
693
+ .triagly-btn-secondary:hover {
694
+ background: var(--triagly-btn-secondary-bg-hover, #e5e7eb);
695
+ }
696
+
697
+ .triagly-status {
698
+ margin-top: 16px;
699
+ padding: 12px;
700
+ border-radius: 6px;
701
+ font-size: 14px;
702
+ display: none;
703
+ }
704
+
705
+ .triagly-status.triagly-success {
706
+ display: block;
707
+ background: var(--triagly-success-bg, #d1fae5);
708
+ color: var(--triagly-success-text, #065f46);
709
+ }
710
+
711
+ .triagly-status.triagly-error {
712
+ display: block;
713
+ background: var(--triagly-error-bg, #fee2e2);
714
+ color: var(--triagly-error-text, #991b1b);
715
+ }
716
+ `;
717
+ document.head.appendChild(style);
718
+ }
719
+ /**
720
+ * Set up keyboard event handlers
721
+ */
722
+ setupKeyboardEvents() {
723
+ const handleKeyDown = (e) => {
724
+ // Close on Escape key
725
+ if (e.key === 'Escape' && this.isOpen) {
726
+ e.preventDefault();
727
+ this.close('dismiss');
728
+ }
729
+ };
730
+ document.addEventListener('keydown', handleKeyDown);
731
+ // Store handler for cleanup
732
+ if (this.container) {
733
+ this.container._keydownHandler = handleKeyDown;
734
+ }
735
+ }
736
+ /**
737
+ * Set up focus trap to keep focus within modal
738
+ */
739
+ setupFocusTrap() {
740
+ if (!this.container)
741
+ return;
742
+ // Get all focusable elements
743
+ const modal = this.container.querySelector('.triagly-modal');
744
+ if (!modal)
745
+ return;
746
+ const focusableSelector = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
747
+ this.focusableElements = Array.from(modal.querySelectorAll(focusableSelector));
748
+ if (this.focusableElements.length === 0) {
749
+ console.warn('Triagly: No focusable elements found in modal');
750
+ return;
751
+ }
752
+ // Handle Tab key to trap focus - must prevent default BEFORE focus moves
753
+ const handleTab = (e) => {
754
+ const keyEvent = e;
755
+ if (keyEvent.key !== 'Tab')
756
+ return;
757
+ // Only handle if focus is within our modal
758
+ if (!this.container?.contains(document.activeElement))
759
+ return;
760
+ const firstFocusable = this.focusableElements[0];
761
+ const lastFocusable = this.focusableElements[this.focusableElements.length - 1];
762
+ if (keyEvent.shiftKey) {
763
+ // Shift + Tab - moving backwards
764
+ if (document.activeElement === firstFocusable) {
765
+ keyEvent.preventDefault();
766
+ lastFocusable?.focus();
767
+ }
768
+ }
769
+ else {
770
+ // Tab - moving forwards
771
+ if (document.activeElement === lastFocusable) {
772
+ keyEvent.preventDefault();
773
+ firstFocusable?.focus();
774
+ }
775
+ }
776
+ };
777
+ // Use capture phase to intercept Tab before browser handles it
778
+ document.addEventListener('keydown', handleTab, true);
779
+ this.container._tabHandler = handleTab;
780
+ }
781
+ /**
782
+ * Destroy the widget
783
+ */
784
+ destroy() {
785
+ // Clean up event listeners
786
+ if (this.container) {
787
+ const keydownHandler = this.container._keydownHandler;
788
+ if (keydownHandler) {
789
+ document.removeEventListener('keydown', keydownHandler);
790
+ }
791
+ const tabHandler = this.container._tabHandler;
792
+ if (tabHandler) {
793
+ document.removeEventListener('keydown', tabHandler, true);
794
+ }
795
+ }
796
+ this.close();
797
+ document.getElementById('triagly-button')?.remove();
798
+ document.getElementById('triagly-styles')?.remove();
799
+ }
800
+ }
801
+
802
+ // API Client
803
+ const DEFAULT_API_URL = 'https://sypkjlwfyvyuqnvzkaxb.supabase.co/functions/v1';
804
+ class TriaglyAPI {
805
+ constructor(publishableKey, apiUrl, getToken, turnstileSiteKey) {
806
+ this.apiUrl = (apiUrl || DEFAULT_API_URL).replace(/\/$/, ''); // Remove trailing slash
807
+ this.publishableKey = publishableKey;
808
+ this.getToken = getToken;
809
+ this.turnstileSiteKey = turnstileSiteKey;
810
+ }
811
+ /**
812
+ * Get Turnstile token from widget if available
813
+ */
814
+ async getTurnstileToken() {
815
+ // Check if Turnstile widget is available
816
+ const turnstileWidget = document.querySelector('[data-turnstile-response]');
817
+ if (turnstileWidget) {
818
+ const token = turnstileWidget.getAttribute('data-turnstile-response');
819
+ if (token)
820
+ return token;
821
+ }
822
+ // Check if window.turnstile is available
823
+ if (window.turnstile) {
824
+ try {
825
+ // Get the first widget's response
826
+ const widgets = document.querySelectorAll('.cf-turnstile');
827
+ if (widgets.length > 0) {
828
+ const widgetId = widgets[0].getAttribute('data-widget-id');
829
+ if (widgetId) {
830
+ const token = window.turnstile.getResponse(widgetId);
831
+ if (token)
832
+ return token;
833
+ }
834
+ }
835
+ }
836
+ catch (error) {
837
+ console.warn('Failed to get Turnstile token:', error);
838
+ }
839
+ }
840
+ return null;
841
+ }
842
+ /**
843
+ * Submit feedback with new authentication
844
+ */
845
+ async submitFeedback(data, metadata, turnstileToken) {
846
+ // Get Turnstile token if not provided
847
+ if (!turnstileToken) {
848
+ turnstileToken = await this.getTurnstileToken() || undefined;
849
+ }
850
+ // Only require Turnstile if configured
851
+ if (this.turnstileSiteKey && !turnstileToken) {
852
+ throw new Error('Turnstile verification required. Please complete the captcha.');
853
+ }
854
+ // Get hardened token if callback is provided
855
+ let hardenedToken;
856
+ if (this.getToken) {
857
+ try {
858
+ hardenedToken = await this.getToken();
859
+ }
860
+ catch (error) {
861
+ console.error('Failed to get hardened token:', error);
862
+ throw new Error('Failed to authenticate. Please try again.');
863
+ }
864
+ }
865
+ const payload = {
866
+ publishableKey: this.publishableKey,
867
+ title: data.title,
868
+ description: data.description,
869
+ metadata: {
870
+ ...metadata,
871
+ consoleLogs: data.consoleLogs,
872
+ },
873
+ tags: data.tags,
874
+ screenshot: data.screenshot,
875
+ reporterEmail: data.reporterEmail,
876
+ reporterName: data.reporterName,
877
+ turnstileToken,
878
+ hardenedToken,
879
+ };
880
+ const response = await fetch(`${this.apiUrl}/feedback`, {
881
+ method: 'POST',
882
+ headers: {
883
+ 'Content-Type': 'application/json',
884
+ },
885
+ body: JSON.stringify(payload),
886
+ });
887
+ if (!response.ok) {
888
+ const error = await response.json().catch(() => ({ error: 'Unknown error', message: 'Unknown error' }));
889
+ // Handle specific error types with user-friendly messages
890
+ if (response.status === 401) {
891
+ if (error.error === 'invalid_publishable_key') {
892
+ throw new Error('Invalid API key. Please contact support.');
893
+ }
894
+ else if (error.error === 'token_required') {
895
+ throw new Error('Authentication required. Please refresh and try again.');
896
+ }
897
+ throw new Error(error.message || 'Authentication failed');
898
+ }
899
+ else if (response.status === 403) {
900
+ if (error.error === 'origin_not_allowed') {
901
+ throw new Error('This website is not authorized to submit feedback.');
902
+ }
903
+ throw new Error(error.message || 'Access denied');
904
+ }
905
+ else if (response.status === 429) {
906
+ const retryAfter = response.headers.get('Retry-After');
907
+ const resetTime = retryAfter ? `in ${retryAfter} seconds` : 'later';
908
+ throw new Error(`Too many requests. Please try again ${resetTime}.`);
909
+ }
910
+ else if (response.status === 400 && error.error === 'captcha_failed') {
911
+ throw new Error('Captcha verification failed. Please try again.');
912
+ }
913
+ throw new Error(error.message || error.error || `Failed to submit feedback (HTTP ${response.status})`);
914
+ }
915
+ return await response.json();
916
+ }
917
+ }
918
+
919
+ // Utility functions
920
+ /**
921
+ * Collect browser and page metadata
922
+ */
923
+ function collectMetadata(customMetadata) {
924
+ const viewport = `${window.innerWidth}x${window.innerHeight}`;
925
+ const browser = detectBrowser();
926
+ return {
927
+ url: window.location.href,
928
+ browser,
929
+ viewport,
930
+ userAgent: navigator.userAgent,
931
+ timestamp: new Date().toISOString(),
932
+ ...customMetadata,
933
+ };
934
+ }
935
+ /**
936
+ * Detect browser name and version
937
+ */
938
+ function detectBrowser() {
939
+ const ua = navigator.userAgent;
940
+ let browser = 'Unknown';
941
+ if (ua.includes('Firefox/')) {
942
+ const version = ua.match(/Firefox\/(\d+)/)?.[1];
943
+ browser = `Firefox ${version}`;
944
+ }
945
+ else if (ua.includes('Chrome/') && !ua.includes('Edg')) {
946
+ const version = ua.match(/Chrome\/(\d+)/)?.[1];
947
+ browser = `Chrome ${version}`;
948
+ }
949
+ else if (ua.includes('Safari/') && !ua.includes('Chrome')) {
950
+ const version = ua.match(/Version\/(\d+)/)?.[1];
951
+ browser = `Safari ${version}`;
952
+ }
953
+ else if (ua.includes('Edg/')) {
954
+ const version = ua.match(/Edg\/(\d+)/)?.[1];
955
+ browser = `Edge ${version}`;
956
+ }
957
+ return browser;
958
+ }
959
+ /**
960
+ * Capture screenshot of current page
961
+ */
962
+ async function captureScreenshot() {
963
+ try {
964
+ // Use html2canvas library if available
965
+ if (typeof window.html2canvas !== 'undefined') {
966
+ const canvas = await window.html2canvas(document.body, {
967
+ logging: false,
968
+ useCORS: true,
969
+ allowTaint: true,
970
+ });
971
+ return canvas.toDataURL('image/png');
972
+ }
973
+ // Fallback to native screenshot API if supported (limited browser support)
974
+ if ('mediaDevices' in navigator && 'getDisplayMedia' in navigator.mediaDevices) {
975
+ // This requires user interaction and shows a permission dialog
976
+ // Not ideal for automatic screenshots
977
+ console.warn('Screenshot capture requires html2canvas library');
978
+ return null;
979
+ }
980
+ return null;
981
+ }
982
+ catch (error) {
983
+ console.error('Screenshot capture failed:', error);
984
+ return null;
985
+ }
986
+ }
987
+ /**
988
+ * Simple rate limiter using localStorage
989
+ */
990
+ class RateLimiter {
991
+ constructor(key, maxAttempts = 3, windowMs = 5 * 60 * 1000) {
992
+ this.key = `triagly_ratelimit_${key}`;
993
+ this.maxAttempts = maxAttempts;
994
+ this.windowMs = windowMs;
995
+ }
996
+ canProceed() {
997
+ const now = Date.now();
998
+ const data = this.getData();
999
+ // Filter out old attempts
1000
+ const recentAttempts = data.attempts.filter((timestamp) => now - timestamp < this.windowMs);
1001
+ if (recentAttempts.length >= this.maxAttempts) {
1002
+ return false;
1003
+ }
1004
+ return true;
1005
+ }
1006
+ recordAttempt() {
1007
+ const now = Date.now();
1008
+ const data = this.getData();
1009
+ // Add new attempt
1010
+ data.attempts.push(now);
1011
+ // Keep only recent attempts
1012
+ data.attempts = data.attempts.filter((timestamp) => now - timestamp < this.windowMs);
1013
+ this.setData(data);
1014
+ }
1015
+ getTimeUntilReset() {
1016
+ const now = Date.now();
1017
+ const data = this.getData();
1018
+ if (data.attempts.length === 0) {
1019
+ return 0;
1020
+ }
1021
+ const oldestAttempt = Math.min(...data.attempts);
1022
+ const resetTime = oldestAttempt + this.windowMs;
1023
+ return Math.max(0, resetTime - now);
1024
+ }
1025
+ getData() {
1026
+ try {
1027
+ const stored = localStorage.getItem(this.key);
1028
+ return stored ? JSON.parse(stored) : { attempts: [] };
1029
+ }
1030
+ catch {
1031
+ return { attempts: [] };
1032
+ }
1033
+ }
1034
+ setData(data) {
1035
+ try {
1036
+ localStorage.setItem(this.key, JSON.stringify(data));
1037
+ }
1038
+ catch (error) {
1039
+ console.error('Failed to store rate limit data:', error);
1040
+ }
1041
+ }
1042
+ }
1043
+ class ConsoleLogger {
1044
+ constructor(maxLogs = 50, levels = ['error', 'warn']) {
1045
+ this.buffer = [];
1046
+ this.isActive = false;
1047
+ this.maxLogs = maxLogs;
1048
+ this.levels = new Set(levels);
1049
+ // Store original console methods
1050
+ this.originalConsole = {
1051
+ error: console.error,
1052
+ warn: console.warn,
1053
+ log: console.log,
1054
+ };
1055
+ }
1056
+ /**
1057
+ * Start capturing console logs
1058
+ */
1059
+ start() {
1060
+ if (this.isActive)
1061
+ return;
1062
+ this.isActive = true;
1063
+ // Intercept console.error
1064
+ if (this.levels.has('error')) {
1065
+ console.error = (...args) => {
1066
+ this.captureLog('error', args);
1067
+ this.originalConsole.error.apply(console, args);
1068
+ };
1069
+ }
1070
+ // Intercept console.warn
1071
+ if (this.levels.has('warn')) {
1072
+ console.warn = (...args) => {
1073
+ this.captureLog('warn', args);
1074
+ this.originalConsole.warn.apply(console, args);
1075
+ };
1076
+ }
1077
+ // Intercept console.log
1078
+ if (this.levels.has('log')) {
1079
+ console.log = (...args) => {
1080
+ this.captureLog('log', args);
1081
+ this.originalConsole.log.apply(console, args);
1082
+ };
1083
+ }
1084
+ }
1085
+ /**
1086
+ * Stop capturing and restore original console methods
1087
+ */
1088
+ stop() {
1089
+ if (!this.isActive)
1090
+ return;
1091
+ this.isActive = false;
1092
+ console.error = this.originalConsole.error;
1093
+ console.warn = this.originalConsole.warn;
1094
+ console.log = this.originalConsole.log;
1095
+ }
1096
+ /**
1097
+ * Capture a log entry
1098
+ */
1099
+ captureLog(level, args) {
1100
+ try {
1101
+ // Convert arguments to string
1102
+ const message = args.map(arg => {
1103
+ if (typeof arg === 'string')
1104
+ return arg;
1105
+ if (arg instanceof Error)
1106
+ return arg.message;
1107
+ try {
1108
+ return JSON.stringify(arg);
1109
+ }
1110
+ catch {
1111
+ return String(arg);
1112
+ }
1113
+ }).join(' ');
1114
+ // Get stack trace for errors
1115
+ let stack;
1116
+ if (level === 'error') {
1117
+ const error = args.find(arg => arg instanceof Error);
1118
+ if (error) {
1119
+ stack = error.stack;
1120
+ }
1121
+ else {
1122
+ // Create stack trace
1123
+ stack = new Error().stack?.split('\n').slice(2).join('\n');
1124
+ }
1125
+ }
1126
+ // Sanitize sensitive data
1127
+ const sanitized = this.sanitize(message);
1128
+ const sanitizedStack = stack ? this.sanitize(stack) : undefined;
1129
+ // Add to buffer
1130
+ const logEntry = {
1131
+ level,
1132
+ message: sanitized,
1133
+ timestamp: new Date().toISOString(),
1134
+ stack: sanitizedStack,
1135
+ };
1136
+ this.buffer.push(logEntry);
1137
+ // Keep buffer size limited (circular buffer)
1138
+ if (this.buffer.length > this.maxLogs) {
1139
+ this.buffer.shift();
1140
+ }
1141
+ }
1142
+ catch (error) {
1143
+ // Don't let logging break the app
1144
+ this.originalConsole.error('Failed to capture log:', error);
1145
+ }
1146
+ }
1147
+ /**
1148
+ * Sanitize sensitive data from logs
1149
+ */
1150
+ sanitize(text) {
1151
+ return text
1152
+ // API keys, tokens, secrets
1153
+ .replace(/[a-zA-Z0-9_-]*token[a-zA-Z0-9_-]*\s*[:=]\s*["']?[\w-]{20,}["']?/gi, 'token=***')
1154
+ .replace(/[a-zA-Z0-9_-]*key[a-zA-Z0-9_-]*\s*[:=]\s*["']?[\w-]{20,}["']?/gi, 'key=***')
1155
+ .replace(/[a-zA-Z0-9_-]*secret[a-zA-Z0-9_-]*\s*[:=]\s*["']?[\w-]{20,}["']?/gi, 'secret=***')
1156
+ // GitHub tokens (ghp_, gho_, etc.)
1157
+ .replace(/gh[ps]_[a-zA-Z0-9]{36,}/g, 'gh*_***')
1158
+ // JWT tokens
1159
+ .replace(/eyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+/g, 'jwt.***')
1160
+ // Passwords
1161
+ .replace(/password\s*[:=]\s*["']?[^"'\s]+["']?/gi, 'password=***')
1162
+ // Email addresses
1163
+ .replace(/\b[\w._%+-]+@[\w.-]+\.[a-zA-Z]{2,}\b/g, '***@***.com')
1164
+ // Credit cards (basic pattern)
1165
+ .replace(/\b\d{4}[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}\b/g, '****-****-****-****')
1166
+ // URLs with tokens in query params
1167
+ .replace(/([?&])(token|key|secret|auth)=[^&\s]+/gi, '$1$2=***');
1168
+ }
1169
+ /**
1170
+ * Get all captured logs
1171
+ */
1172
+ getLogs() {
1173
+ return [...this.buffer];
1174
+ }
1175
+ /**
1176
+ * Clear all captured logs
1177
+ */
1178
+ clear() {
1179
+ this.buffer = [];
1180
+ }
1181
+ /**
1182
+ * Get logs count
1183
+ */
1184
+ getCount() {
1185
+ return this.buffer.length;
1186
+ }
1187
+ }
1188
+
1189
+ // Triagly SDK Main Entry Point
1190
+ class Triagly {
1191
+ constructor(config) {
1192
+ this.consoleLogger = null;
1193
+ // Handle backward compatibility
1194
+ let publishableKey = config.publishableKey;
1195
+ if (!publishableKey && config.projectId) {
1196
+ console.warn('Triagly: projectId is deprecated. Please use publishableKey instead. ' +
1197
+ 'See migration guide: https://docs.triagly.com/sdk/migration');
1198
+ publishableKey = config.projectId;
1199
+ }
1200
+ if (!publishableKey) {
1201
+ throw new Error('Triagly: publishableKey is required. Get yours at https://triagly.com/dashboard');
1202
+ }
1203
+ this.config = {
1204
+ theme: 'auto',
1205
+ position: 'bottom-right',
1206
+ buttonShape: 'rounded',
1207
+ buttonText: '🐛 Feedback',
1208
+ placeholderText: 'Describe what happened...',
1209
+ successMessage: 'Feedback sent successfully!',
1210
+ errorMessage: 'Failed to send feedback. Please try again.',
1211
+ captureConsole: true,
1212
+ consoleLogLimit: 50,
1213
+ consoleLogLevels: ['error', 'warn'],
1214
+ ...config,
1215
+ publishableKey,
1216
+ };
1217
+ this.api = new TriaglyAPI(this.config.publishableKey, this.config.apiUrl, this.config.getToken, this.config.turnstileSiteKey);
1218
+ this.widget = new FeedbackWidget(this.config);
1219
+ this.rateLimiter = new RateLimiter(this.config.publishableKey, 3, 5 * 60 * 1000);
1220
+ // Initialize console logger if enabled
1221
+ if (this.config.captureConsole !== false) {
1222
+ this.consoleLogger = new ConsoleLogger(this.config.consoleLogLimit, this.config.consoleLogLevels);
1223
+ this.consoleLogger.start();
1224
+ }
1225
+ this.init();
1226
+ }
1227
+ /**
1228
+ * Initialize the SDK
1229
+ */
1230
+ init() {
1231
+ // Initialize widget
1232
+ this.widget.init();
1233
+ // Listen for form submissions
1234
+ document.addEventListener('triagly:submit', (e) => {
1235
+ const customEvent = e;
1236
+ this.handleSubmit(customEvent.detail);
1237
+ });
1238
+ }
1239
+ /**
1240
+ * Handle feedback submission
1241
+ */
1242
+ async handleSubmit(data) {
1243
+ try {
1244
+ // Check rate limit
1245
+ if (!this.rateLimiter.canProceed()) {
1246
+ const resetTime = Math.ceil(this.rateLimiter.getTimeUntilReset() / 1000 / 60);
1247
+ throw new Error(`Rate limit exceeded. Please try again in ${resetTime} minute(s).`);
1248
+ }
1249
+ // Collect metadata
1250
+ const metadata = collectMetadata(this.config.metadata);
1251
+ // Capture screenshot if requested
1252
+ let screenshot = null;
1253
+ if (data.includeScreenshot) {
1254
+ screenshot = await captureScreenshot();
1255
+ }
1256
+ // Prepare feedback data
1257
+ const feedbackData = {
1258
+ title: data.title,
1259
+ description: data.description,
1260
+ reporterEmail: data.reporterEmail,
1261
+ screenshot: screenshot || undefined,
1262
+ consoleLogs: this.consoleLogger?.getLogs(),
1263
+ };
1264
+ // Submit to API with Turnstile token if provided
1265
+ const response = await this.api.submitFeedback(feedbackData, metadata, data.turnstileToken);
1266
+ // Record rate limit attempt
1267
+ this.rateLimiter.recordAttempt();
1268
+ // Dispatch success event for UI layer
1269
+ document.dispatchEvent(new CustomEvent('triagly:success', {
1270
+ detail: { feedbackId: response.id }
1271
+ }));
1272
+ // Call success callback
1273
+ if (this.config.onSuccess) {
1274
+ this.config.onSuccess(response.id);
1275
+ }
1276
+ console.log('Feedback submitted successfully:', response.id);
1277
+ }
1278
+ catch (error) {
1279
+ console.error('Failed to submit feedback:', error);
1280
+ // Dispatch error event for UI layer
1281
+ document.dispatchEvent(new CustomEvent('triagly:error', {
1282
+ detail: error
1283
+ }));
1284
+ // Call error callback
1285
+ if (this.config.onError && error instanceof Error) {
1286
+ this.config.onError(error);
1287
+ }
1288
+ throw error;
1289
+ }
1290
+ }
1291
+ /**
1292
+ * Programmatically open the feedback widget
1293
+ */
1294
+ open() {
1295
+ this.widget.open();
1296
+ }
1297
+ /**
1298
+ * Programmatically close the feedback widget
1299
+ */
1300
+ close() {
1301
+ this.widget.close();
1302
+ }
1303
+ /**
1304
+ * Submit feedback programmatically without UI
1305
+ * Note: When Turnstile is enabled, you must use the widget UI or provide a token
1306
+ */
1307
+ async submit(data, turnstileToken) {
1308
+ // Require Turnstile token if configured
1309
+ if (this.config.turnstileSiteKey && !turnstileToken) {
1310
+ throw new Error('Turnstile verification required. When Turnstile is enabled, you must:\n' +
1311
+ '1. Use the widget UI (triagly.open()), or\n' +
1312
+ '2. Implement Turnstile in your form and pass the token: triagly.submit(data, token)');
1313
+ }
1314
+ const metadata = collectMetadata(this.config.metadata);
1315
+ // Include console logs if available and not already provided
1316
+ if (!data.consoleLogs && this.consoleLogger) {
1317
+ data.consoleLogs = this.consoleLogger.getLogs();
1318
+ }
1319
+ await this.api.submitFeedback(data, metadata, turnstileToken);
1320
+ this.rateLimiter.recordAttempt();
1321
+ }
1322
+ /**
1323
+ * Destroy the SDK instance
1324
+ */
1325
+ destroy() {
1326
+ this.widget.destroy();
1327
+ this.consoleLogger?.stop();
1328
+ document.removeEventListener('triagly:submit', () => { });
1329
+ }
1330
+ }
1331
+ // Auto-initialize if config is in window
1332
+ if (typeof window !== 'undefined') {
1333
+ const globalConfig = window.TriaglyConfig;
1334
+ if (globalConfig) {
1335
+ window.triagly = new Triagly(globalConfig);
1336
+ }
1337
+ }
1338
+
1339
+ export { Triagly, Triagly as default };
1340
+ //# sourceMappingURL=index.esm.js.map