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