@uxbertlabs/reportly 1.0.17 → 1.0.19

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.
@@ -184,11 +184,11 @@ class Modal {
184
184
  }
185
185
  create() {
186
186
  // Create overlay
187
- this.overlay = document.createElement('div');
188
- this.overlay.className = 'uxbert-overlay';
187
+ this.overlay = document.createElement("div");
188
+ this.overlay.className = "uxbert-overlay";
189
189
  // Create modal
190
- this.modal = document.createElement('div');
191
- this.modal.className = 'uxbert-modal';
190
+ this.modal = document.createElement("div");
191
+ this.modal.className = "uxbert-modal";
192
192
  // Modal HTML
193
193
  this.modal.innerHTML = `
194
194
  <div class="uxbert-modal-header">
@@ -233,12 +233,13 @@ class Modal {
233
233
 
234
234
  <div class="uxbert-form-group">
235
235
  <label class="uxbert-form-label" for="uxbert-priority">Priority</label>
236
- <select id="uxbert-priority" class="uxbert-form-select">
237
- <option value="Low">Low</option>
238
- <option value="Medium" selected>Medium</option>
239
- <option value="High">High</option>
240
- <option value="Critical">Critical</option>
241
- </select>
236
+ <select id="uxbert-priority" class="uxbert-form-select">
237
+ <option value="5" selected>Lowest</option>
238
+ <option value="4">Low</option>
239
+ <option value="3" selected>Medium</option>
240
+ <option value="2">High</option>
241
+ <option value="1">Highest</option>
242
+ </select>
242
243
  </div>
243
244
 
244
245
  <div class="uxbert-capture-action" id="uxbert-capture-action">
@@ -263,6 +264,9 @@ class Modal {
263
264
  <button type="button" class="uxbert-btn uxbert-btn-secondary" id="uxbert-cancel-btn">
264
265
  Cancel
265
266
  </button>
267
+ <button type="button" class="uxbert-btn uxbert-btn-success" id="uxbert-n8n-btn" style="display: none;">
268
+ 🚀 Send to n8n
269
+ </button>
266
270
  <button type="submit" class="uxbert-btn uxbert-btn-primary" id="uxbert-submit-btn">
267
271
  📥 Download JSON
268
272
  </button>
@@ -279,72 +283,103 @@ class Modal {
279
283
  if (!this.modal)
280
284
  return;
281
285
  // Close button
282
- const closeBtn = this.modal.querySelector('.uxbert-modal-close');
283
- closeBtn.addEventListener('click', () => this.close());
286
+ const closeBtn = this.modal.querySelector(".uxbert-modal-close");
287
+ closeBtn.addEventListener("click", () => this.close());
284
288
  // Cancel button
285
- const cancelBtn = this.modal.querySelector('#uxbert-cancel-btn');
286
- cancelBtn.addEventListener('click', () => this.close());
289
+ const cancelBtn = this.modal.querySelector("#uxbert-cancel-btn");
290
+ cancelBtn.addEventListener("click", () => this.close());
287
291
  // Overlay click to close
288
- this.overlay?.addEventListener('click', (e) => {
292
+ this.overlay?.addEventListener("click", (e) => {
289
293
  if (e.target === this.overlay) {
290
294
  this.close();
291
295
  }
292
296
  });
293
297
  // Form submit
294
- const form = this.modal.querySelector('#uxbert-issue-form');
295
- form.addEventListener('submit', (e) => {
298
+ const form = this.modal.querySelector("#uxbert-issue-form");
299
+ form.addEventListener("submit", (e) => {
296
300
  e.preventDefault();
297
301
  this.handleSubmit();
298
302
  });
299
303
  // Capture button
300
- const captureBtn = this.modal.querySelector('#uxbert-capture-btn');
301
- captureBtn.addEventListener('click', () => {
304
+ const captureBtn = this.modal.querySelector("#uxbert-capture-btn");
305
+ captureBtn.addEventListener("click", () => {
302
306
  if (this.callbacks.onCapture) {
303
307
  this.callbacks.onCapture();
304
308
  }
305
309
  });
306
310
  // Annotate button
307
- const annotateBtn = this.modal.querySelector('#uxbert-annotate-btn');
308
- annotateBtn.addEventListener('click', () => {
311
+ const annotateBtn = this.modal.querySelector("#uxbert-annotate-btn");
312
+ annotateBtn.addEventListener("click", () => {
309
313
  if (this.callbacks.onAnnotate) {
310
314
  this.callbacks.onAnnotate();
311
315
  }
312
316
  });
313
317
  // Retake button
314
- const retakeBtn = this.modal.querySelector('#uxbert-retake-btn');
315
- retakeBtn.addEventListener('click', () => {
318
+ const retakeBtn = this.modal.querySelector("#uxbert-retake-btn");
319
+ retakeBtn.addEventListener("click", () => {
316
320
  if (this.callbacks.onRetake) {
317
321
  this.callbacks.onRetake();
318
322
  }
319
323
  });
324
+ // n8n button
325
+ const n8nBtn = this.modal.querySelector("#uxbert-n8n-btn");
326
+ n8nBtn.addEventListener("click", () => {
327
+ this.handleN8nSubmit();
328
+ });
329
+ }
330
+ handleN8nSubmit() {
331
+ if (!this.modal)
332
+ return;
333
+ const titleInput = this.modal.querySelector("#uxbert-title");
334
+ const descriptionInput = this.modal.querySelector("#uxbert-description");
335
+ const prioritySelect = this.modal.querySelector("#uxbert-priority");
336
+ const title = titleInput.value;
337
+ const description = descriptionInput.value;
338
+ const priority = prioritySelect.value;
339
+ if (!title.trim()) {
340
+ alert("Please enter an issue title");
341
+ return;
342
+ }
343
+ const issueData = {
344
+ title: title.trim(),
345
+ description: description.trim(),
346
+ priority,
347
+ };
348
+ if (this.callbacks.onSendToN8n) {
349
+ this.callbacks.onSendToN8n(issueData);
350
+ }
320
351
  }
321
352
  handleSubmit() {
322
353
  if (!this.modal)
323
354
  return;
324
- const titleInput = this.modal.querySelector('#uxbert-title');
325
- const descriptionInput = this.modal.querySelector('#uxbert-description');
326
- const prioritySelect = this.modal.querySelector('#uxbert-priority');
355
+ const titleInput = this.modal.querySelector("#uxbert-title");
356
+ const descriptionInput = this.modal.querySelector("#uxbert-description");
357
+ const prioritySelect = this.modal.querySelector("#uxbert-priority");
327
358
  const title = titleInput.value;
328
359
  const description = descriptionInput.value;
329
360
  const priority = prioritySelect.value;
330
361
  if (!title.trim()) {
331
- alert('Please enter an issue title');
362
+ alert("Please enter an issue title");
332
363
  return;
333
364
  }
334
365
  const issueData = {
335
366
  title: title.trim(),
336
367
  description: description.trim(),
337
- priority
368
+ priority,
338
369
  };
339
370
  if (this.callbacks.onSubmit) {
340
371
  this.callbacks.onSubmit(issueData);
341
372
  }
342
373
  }
343
374
  open() {
344
- this.overlay?.classList.add('active');
375
+ this.overlay?.classList.add("active");
376
+ // Prevent body scroll when modal is open
377
+ document.body.style.overflow = "hidden";
345
378
  }
346
379
  close() {
347
- this.overlay?.classList.remove('active');
380
+ this.overlay?.classList.remove("active");
381
+ // Restore body scroll when modal is closed
382
+ document.body.style.overflow = "";
348
383
  this.reset();
349
384
  if (this.callbacks.onClose) {
350
385
  this.callbacks.onClose();
@@ -353,39 +388,49 @@ class Modal {
353
388
  setScreenshot(screenshot) {
354
389
  if (!this.modal)
355
390
  return;
356
- const container = this.modal.querySelector('#uxbert-screenshot-container');
357
- const img = this.modal.querySelector('#uxbert-screenshot-img');
358
- const captureAction = this.modal.querySelector('#uxbert-capture-action');
359
- const submitBtn = this.modal.querySelector('#uxbert-submit-btn');
391
+ const container = this.modal.querySelector("#uxbert-screenshot-container");
392
+ const img = this.modal.querySelector("#uxbert-screenshot-img");
393
+ const captureAction = this.modal.querySelector("#uxbert-capture-action");
394
+ const submitBtn = this.modal.querySelector("#uxbert-submit-btn");
360
395
  if (screenshot) {
361
396
  img.src = screenshot;
362
- container.style.display = 'block';
363
- captureAction.style.display = 'none';
397
+ container.style.display = "block";
398
+ captureAction.style.display = "none";
364
399
  submitBtn.disabled = false;
365
400
  }
366
401
  else {
367
- container.style.display = 'none';
368
- captureAction.style.display = 'block';
402
+ container.style.display = "none";
403
+ captureAction.style.display = "block";
369
404
  submitBtn.disabled = true;
370
405
  }
371
406
  }
372
407
  getCaptureMode() {
373
408
  if (!this.modal)
374
- return 'viewport';
375
- const viewportRadio = this.modal.querySelector('#uxbert-capture-viewport');
376
- return viewportRadio.checked ? 'viewport' : 'fullpage';
409
+ return "viewport";
410
+ const viewportRadio = this.modal.querySelector("#uxbert-capture-viewport");
411
+ return viewportRadio.checked ? "viewport" : "fullpage";
412
+ }
413
+ setN8nButtonVisible(visible) {
414
+ if (!this.modal)
415
+ return;
416
+ const n8nBtn = this.modal.querySelector("#uxbert-n8n-btn");
417
+ if (n8nBtn) {
418
+ n8nBtn.style.display = visible ? "inline-block" : "none";
419
+ }
377
420
  }
378
421
  reset() {
379
422
  if (!this.modal)
380
423
  return;
381
- this.modal.querySelector('#uxbert-title').value = '';
382
- this.modal.querySelector('#uxbert-description').value = '';
383
- this.modal.querySelector('#uxbert-priority').value = 'Medium';
424
+ this.modal.querySelector("#uxbert-title").value = "";
425
+ this.modal.querySelector("#uxbert-description").value = "";
426
+ this.modal.querySelector("#uxbert-priority").value = "Medium";
384
427
  this.setScreenshot(null);
385
428
  }
386
429
  destroy() {
387
430
  if (this.overlay && this.overlay.parentNode) {
388
431
  this.overlay.parentNode.removeChild(this.overlay);
432
+ // Restore body scroll when destroying modal
433
+ document.body.style.overflow = "";
389
434
  this.overlay = null;
390
435
  this.modal = null;
391
436
  }
@@ -10161,9 +10206,223 @@ class DeviceInfo {
10161
10206
  }
10162
10207
  }
10163
10208
 
10209
+ class N8nIntegration {
10210
+ constructor(config) {
10211
+ this.config = config || null;
10212
+ }
10213
+ /**
10214
+ * Configure n8n integration
10215
+ */
10216
+ configure(config) {
10217
+ this.config = config;
10218
+ }
10219
+ /**
10220
+ * Check if n8n integration is configured and enabled
10221
+ */
10222
+ isEnabled() {
10223
+ return !!(this.config && this.config.enabled && this.config.webhookUrl);
10224
+ }
10225
+ /**
10226
+ * Send issue data to n8n webhook
10227
+ */
10228
+ async sendToN8n(issueData) {
10229
+ if (!this.config || !this.config.webhookUrl) {
10230
+ return {
10231
+ success: false,
10232
+ message: "n8n webhook URL is not configured",
10233
+ error: "MISSING_CONFIG",
10234
+ };
10235
+ }
10236
+ if (!this.config.enabled) {
10237
+ return {
10238
+ success: false,
10239
+ message: "n8n integration is disabled",
10240
+ error: "DISABLED",
10241
+ };
10242
+ }
10243
+ try {
10244
+ // Prepare FormData with binary screenshot
10245
+ const formData = await this.prepareFormData(issueData);
10246
+ // Setup request headers (don't set Content-Type - browser will set it with boundary for FormData)
10247
+ const headers = {
10248
+ ...this.config.headers,
10249
+ };
10250
+ // Setup timeout
10251
+ const timeout = this.config.timeout || 30000; // 30 seconds default
10252
+ const controller = new AbortController();
10253
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
10254
+ // Send request
10255
+ const response = await fetch(this.config.webhookUrl, {
10256
+ method: "POST",
10257
+ headers,
10258
+ body: formData,
10259
+ signal: controller.signal,
10260
+ });
10261
+ clearTimeout(timeoutId);
10262
+ // Check response
10263
+ if (!response.ok) {
10264
+ const errorText = await response.text().catch(() => "Unknown error");
10265
+ return {
10266
+ success: false,
10267
+ message: `n8n webhook request failed: ${response.status} ${response.statusText}`,
10268
+ error: errorText,
10269
+ };
10270
+ }
10271
+ // Parse response
10272
+ let responseData;
10273
+ try {
10274
+ responseData = await response.json();
10275
+ }
10276
+ catch (e) {
10277
+ // Some webhooks don't return JSON
10278
+ responseData = { message: "Success" };
10279
+ }
10280
+ return {
10281
+ success: true,
10282
+ message: "Issue sent to n8n successfully",
10283
+ workflowId: responseData.workflowId || responseData.executionId,
10284
+ };
10285
+ }
10286
+ catch (error) {
10287
+ if (error instanceof Error) {
10288
+ if (error.name === "AbortError") {
10289
+ return {
10290
+ success: false,
10291
+ message: "Request to n8n webhook timed out",
10292
+ error: "TIMEOUT",
10293
+ };
10294
+ }
10295
+ return {
10296
+ success: false,
10297
+ message: `Failed to send issue to n8n: ${error.message}`,
10298
+ error: error.message,
10299
+ };
10300
+ }
10301
+ return {
10302
+ success: false,
10303
+ message: "Unknown error occurred while sending to n8n",
10304
+ error: "UNKNOWN",
10305
+ };
10306
+ }
10307
+ }
10308
+ /**
10309
+ * Convert base64 data URL to Blob
10310
+ */
10311
+ base64ToBlob(base64Data) {
10312
+ // Extract the base64 string and mime type
10313
+ const matches = base64Data.match(/^data:([^;]+);base64,(.+)$/);
10314
+ if (!matches) {
10315
+ throw new Error("Invalid base64 data URL");
10316
+ }
10317
+ const mimeType = matches[1];
10318
+ const base64String = matches[2];
10319
+ // Decode base64
10320
+ const byteCharacters = atob(base64String);
10321
+ const byteNumbers = new Array(byteCharacters.length);
10322
+ for (let i = 0; i < byteCharacters.length; i++) {
10323
+ byteNumbers[i] = byteCharacters.charCodeAt(i);
10324
+ }
10325
+ const byteArray = new Uint8Array(byteNumbers);
10326
+ return new Blob([byteArray], { type: mimeType });
10327
+ }
10328
+ /**
10329
+ * Prepare FormData payload for n8n webhook
10330
+ * This sends the screenshot as a binary file instead of base64 JSON
10331
+ */
10332
+ async prepareFormData(issueData) {
10333
+ const formData = new FormData();
10334
+ // Add issue data as JSON string
10335
+ formData.append("title", issueData.title);
10336
+ formData.append("description", issueData.description);
10337
+ formData.append("priority", issueData.priority);
10338
+ formData.append("createdAt", issueData.createdAt);
10339
+ // Convert base64 screenshot to binary Blob and add as file
10340
+ const screenshotBlob = this.base64ToBlob(issueData.screenshot);
10341
+ const filename = `screenshot-${Date.now()}.png`;
10342
+ formData.append("screenshot", screenshotBlob, filename);
10343
+ // Add device info as individual fields
10344
+ formData.append("browser", issueData.deviceInfo.browser);
10345
+ formData.append("os", issueData.deviceInfo.os);
10346
+ formData.append("screenResolution", issueData.deviceInfo.screenResolution);
10347
+ formData.append("viewport", issueData.deviceInfo.viewport);
10348
+ formData.append("url", issueData.deviceInfo.url);
10349
+ formData.append("userAgent", issueData.deviceInfo.userAgent);
10350
+ formData.append("language", issueData.deviceInfo.language || "");
10351
+ formData.append("platform", issueData.deviceInfo.platform);
10352
+ formData.append("cookiesEnabled", String(issueData.deviceInfo.cookiesEnabled));
10353
+ formData.append("onLine", String(issueData.deviceInfo.onLine));
10354
+ formData.append("timestamp", issueData.deviceInfo.timestamp);
10355
+ // Add metadata
10356
+ formData.append("source", "uxbert-reportly");
10357
+ formData.append("version", "1.0.0");
10358
+ return formData;
10359
+ }
10360
+ /**
10361
+ * Test connection to n8n webhook
10362
+ */
10363
+ async testConnection() {
10364
+ if (!this.config || !this.config.webhookUrl) {
10365
+ return {
10366
+ success: false,
10367
+ message: "n8n webhook URL is not configured",
10368
+ error: "MISSING_CONFIG",
10369
+ };
10370
+ }
10371
+ try {
10372
+ const controller = new AbortController();
10373
+ const timeoutId = setTimeout(() => controller.abort(), 5000); // 5 second timeout for test
10374
+ await fetch(this.config.webhookUrl, {
10375
+ method: "HEAD",
10376
+ signal: controller.signal,
10377
+ });
10378
+ clearTimeout(timeoutId);
10379
+ return {
10380
+ success: true,
10381
+ message: "n8n webhook is reachable",
10382
+ };
10383
+ }
10384
+ catch (error) {
10385
+ return {
10386
+ success: false,
10387
+ message: "Failed to reach n8n webhook",
10388
+ error: error instanceof Error ? error.message : "UNKNOWN",
10389
+ };
10390
+ }
10391
+ }
10392
+ /**
10393
+ * Get current configuration
10394
+ */
10395
+ getConfig() {
10396
+ return this.config;
10397
+ }
10398
+ /**
10399
+ * Update webhook URL
10400
+ */
10401
+ setWebhookUrl(url) {
10402
+ if (!this.config) {
10403
+ this.config = {
10404
+ webhookUrl: url,
10405
+ enabled: true,
10406
+ };
10407
+ }
10408
+ else {
10409
+ this.config.webhookUrl = url;
10410
+ }
10411
+ }
10412
+ /**
10413
+ * Enable/disable n8n integration
10414
+ */
10415
+ setEnabled(enabled) {
10416
+ if (this.config) {
10417
+ this.config.enabled = enabled;
10418
+ }
10419
+ }
10420
+ }
10421
+
10164
10422
  class Export {
10165
- constructor() {
10423
+ constructor(n8nConfig) {
10166
10424
  this.savedIssues = this.loadSavedIssues();
10425
+ this.n8nIntegration = new N8nIntegration(n8nConfig);
10167
10426
  }
10168
10427
  exportToJSON(issueData) {
10169
10428
  const json = JSON.stringify(issueData, null, 2);
@@ -10232,6 +10491,54 @@ class Export {
10232
10491
  document.body.removeChild(link);
10233
10492
  URL.revokeObjectURL(url);
10234
10493
  }
10494
+ // n8n Integration Methods
10495
+ /**
10496
+ * Send issue to n8n webhook
10497
+ */
10498
+ async sendToN8n(issueData) {
10499
+ const response = await this.n8nIntegration.sendToN8n(issueData);
10500
+ // Save to localStorage if n8n send was successful
10501
+ if (response.success) {
10502
+ this.saveIssue(issueData);
10503
+ }
10504
+ return response;
10505
+ }
10506
+ /**
10507
+ * Configure n8n integration
10508
+ */
10509
+ configureN8n(config) {
10510
+ this.n8nIntegration.configure(config);
10511
+ }
10512
+ /**
10513
+ * Check if n8n is enabled
10514
+ */
10515
+ isN8nEnabled() {
10516
+ return this.n8nIntegration.isEnabled();
10517
+ }
10518
+ /**
10519
+ * Test n8n connection
10520
+ */
10521
+ async testN8nConnection() {
10522
+ return this.n8nIntegration.testConnection();
10523
+ }
10524
+ /**
10525
+ * Get n8n configuration
10526
+ */
10527
+ getN8nConfig() {
10528
+ return this.n8nIntegration.getConfig();
10529
+ }
10530
+ /**
10531
+ * Enable/disable n8n integration
10532
+ */
10533
+ setN8nEnabled(enabled) {
10534
+ this.n8nIntegration.setEnabled(enabled);
10535
+ }
10536
+ /**
10537
+ * Set n8n webhook URL
10538
+ */
10539
+ setN8nWebhookUrl(url) {
10540
+ this.n8nIntegration.setWebhookUrl(url);
10541
+ }
10235
10542
  }
10236
10543
 
10237
10544
  function styleInject(css, ref) {
@@ -10261,7 +10568,7 @@ function styleInject(css, ref) {
10261
10568
  }
10262
10569
  }
10263
10570
 
10264
- var css_248z = ":root{--uxbert-primary:#4f46e5;--uxbert-primary-hover:#4338ca;--uxbert-danger:#ef4444;--uxbert-success:#10b981;--uxbert-bg:#fff;--uxbert-text:#1f2937;--uxbert-border:#e5e7eb;--uxbert-shadow:0 10px 25px rgba(0,0,0,.1);--uxbert-z-button:999999;--uxbert-z-modal:1000000;--uxbert-z-canvas:1000001;--uxbert-z-toolbar:1000002}[data-theme=dark]{--uxbert-bg:#1f2937;--uxbert-text:#f9fafb;--uxbert-border:#374151}.uxbert-fab{align-items:center;background:var(--uxbert-primary);border:none;border-radius:50%;box-shadow:var(--uxbert-shadow);color:#fff;cursor:pointer;display:flex;font-size:24px;height:56px;justify-content:center;position:fixed;transition:all .3s ease;width:56px;z-index:var(--uxbert-z-button)}.uxbert-fab:hover{background:var(--uxbert-primary-hover);transform:scale(1.1)}.uxbert-fab.bottom-right{bottom:24px;right:24px}.uxbert-fab.bottom-left{bottom:24px;left:24px}.uxbert-fab.top-right{right:24px;top:24px}.uxbert-fab.top-left{left:24px;top:24px}.uxbert-overlay{align-items:center;animation:fadeIn .3s ease;background:rgba(0,0,0,.5);display:none;height:100%;justify-content:center;left:0;position:fixed;top:0;width:100%;z-index:var(--uxbert-z-modal)}.uxbert-overlay.active{display:flex}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.uxbert-modal{animation:slideUp .3s ease;background:var(--uxbert-bg);border-radius:12px;box-shadow:var(--uxbert-shadow);color:var(--uxbert-text);max-height:90vh;max-width:600px;overflow-y:auto;padding:24px;width:90%}@keyframes slideUp{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.uxbert-modal-header{align-items:center;display:flex;justify-content:space-between;margin-bottom:20px}.uxbert-modal-title{font-size:20px;font-weight:600;margin:0}.uxbert-modal-close{align-items:center;background:none;border:none;border-radius:4px;color:var(--uxbert-text);cursor:pointer;display:flex;font-size:24px;height:32px;justify-content:center;padding:0;width:32px}.uxbert-modal-close:hover{background:var(--uxbert-border)}.uxbert-form-group{margin-bottom:16px}.uxbert-form-label{display:block;font-size:14px;font-weight:500;margin-bottom:8px}.uxbert-capture-mode{display:flex;flex-wrap:wrap;gap:12px}.uxbert-radio-label{align-items:center;border:2px solid var(--uxbert-border);border-radius:8px;cursor:pointer;display:flex;flex:1;gap:8px;min-width:140px;padding:10px 16px;transition:all .2s ease}.uxbert-radio-label:hover{background:rgba(79,70,229,.05);border-color:var(--uxbert-primary)}.uxbert-radio-label input[type=radio]{cursor:pointer;height:18px;margin:0;width:18px}.uxbert-radio-label input[type=radio]:checked{accent-color:var(--uxbert-primary)}.uxbert-radio-label:has(input[type=radio]:checked){background:rgba(79,70,229,.1);border-color:var(--uxbert-primary)}.uxbert-radio-label span{flex:1;font-size:14px;font-weight:500}.uxbert-form-input,.uxbert-form-select,.uxbert-form-textarea{background:var(--uxbert-bg);border:1px solid var(--uxbert-border);border-radius:6px;box-sizing:border-box;color:var(--uxbert-text);font-family:inherit;font-size:14px;padding:10px;width:100%}.uxbert-form-textarea{min-height:100px;resize:vertical}.uxbert-form-input:focus,.uxbert-form-select:focus,.uxbert-form-textarea:focus{border-color:var(--uxbert-primary);outline:none}.uxbert-capture-action{margin:16px 0;text-align:center}.uxbert-capture-action .uxbert-btn{min-width:200px}.uxbert-screenshot-preview{border:1px solid var(--uxbert-border);border-radius:8px;margin:16px 0;overflow:hidden}.uxbert-screenshot-preview img{display:block;width:100%}.uxbert-screenshot-actions{background:var(--uxbert-border);display:flex;gap:8px;padding:12px}.uxbert-btn{align-items:center;border:none;border-radius:6px;cursor:pointer;display:inline-flex;font-size:14px;font-weight:500;gap:8px;padding:10px 20px;transition:all .2s ease}.uxbert-btn-primary{background:var(--uxbert-primary);color:#fff}.uxbert-btn-primary:hover{background:var(--uxbert-primary-hover)}.uxbert-btn-secondary{background:var(--uxbert-border);color:var(--uxbert-text)}.uxbert-btn-secondary:hover{background:#d1d5db}.uxbert-btn-danger{background:var(--uxbert-danger);color:#fff}.uxbert-btn-danger:hover{background:#dc2626}.uxbert-modal-actions{display:flex;gap:12px;justify-content:flex-end;margin-top:20px}.uxbert-toolbar-toggle{align-items:center;background:var(--uxbert-primary);border:none;border-radius:50%;bottom:24px;box-shadow:var(--uxbert-shadow);color:#fff;cursor:pointer;display:none;font-size:24px;height:56px;justify-content:center;position:fixed;right:24px;transition:all .3s ease;width:56px;z-index:var(--uxbert-z-toolbar)}.uxbert-toolbar-toggle.active{display:flex}.uxbert-toolbar-toggle.hidden{opacity:0;pointer-events:none;transform:scale(.8)}.uxbert-toolbar-toggle:hover{background:var(--uxbert-primary-hover);transform:scale(1.1)}.uxbert-toolbar{background:var(--uxbert-bg);border-radius:12px;bottom:24px;box-shadow:var(--uxbert-shadow);display:none;flex-direction:column;gap:8px;max-height:80vh;opacity:0;overflow-y:auto;padding:16px;pointer-events:none;position:fixed;right:24px;transform:translateY(20px);transition:all .3s ease;z-index:var(--uxbert-z-toolbar)}.uxbert-toolbar.active{display:flex}.uxbert-toolbar.expanded{opacity:1;pointer-events:all;transform:translateY(0)}.uxbert-toolbar-header{align-items:center;display:flex;gap:8px;justify-content:space-between;margin-bottom:8px}.uxbert-toolbar-title{flex:1;font-size:14px;font-weight:600}.uxbert-toolbar-exit,.uxbert-toolbar-minimize{align-items:center;background:var(--uxbert-border);border:none;border-radius:6px;color:var(--uxbert-text);cursor:pointer;display:flex;font-size:20px;font-weight:700;height:32px;justify-content:center;transition:all .2s ease;width:32px}.uxbert-toolbar-exit{background:var(--uxbert-danger);color:#fff}.uxbert-toolbar-minimize:hover{background:#d1d5db;transform:scale(1.1)}.uxbert-toolbar-exit:hover{background:#dc2626;transform:scale(1.1)}.uxbert-toolbar-tools{display:flex;flex-wrap:wrap;gap:8px}.uxbert-tool-btn{align-items:center;background:var(--uxbert-bg);border:2px solid var(--uxbert-border);border-radius:6px;cursor:pointer;display:flex;font-family:Arial,sans-serif;font-size:18px;font-weight:700;height:40px;justify-content:center;transition:all .2s ease;width:40px}.uxbert-tool-btn.active,.uxbert-tool-btn:hover{background:var(--uxbert-primary);border-color:var(--uxbert-primary);color:#fff}.uxbert-color-picker{display:flex;gap:6px;margin:8px 0}.uxbert-color-option{border:2px solid var(--uxbert-border);border-radius:50%;cursor:pointer;height:32px;transition:transform .2s ease;width:32px}.uxbert-color-option:hover{transform:scale(1.1)}.uxbert-color-option.active{border:3px solid var(--uxbert-text)}.uxbert-canvas-overlay{cursor:crosshair;display:none;left:0;position:absolute;top:0;z-index:var(--uxbert-z-canvas)}.uxbert-canvas-overlay.active{display:block}.uxbert-canvas-overlay.viewport-mode{left:0;position:fixed;top:0}.uxbert-text-input{background:hsla(0,0%,100%,.95);border:2px solid var(--uxbert-primary);border-radius:6px;box-shadow:0 4px 12px rgba(0,0,0,.15);color:var(--uxbert-text);font-family:Arial,sans-serif;font-size:16px;font-weight:700;min-width:200px;outline:none;padding:8px 12px;z-index:var(--uxbert-z-toolbar)}.uxbert-text-input:focus{border-color:var(--uxbert-primary-hover);box-shadow:0 4px 16px rgba(79,70,229,.3)}.uxbert-text-input::placeholder{color:#9ca3af;font-weight:400}@media (max-width:768px){.uxbert-modal{max-height:95vh;padding:16px;width:95%}.uxbert-toolbar{bottom:90px;left:16px;max-height:60vh;right:16px;width:auto}.uxbert-toolbar-toggle{bottom:16px;height:56px;right:16px;width:56px}.uxbert-fab{font-size:20px;height:48px;width:48px}.uxbert-color-picker,.uxbert-toolbar-tools{justify-content:center}}";
10571
+ var css_248z = ":root{--uxbert-primary:#4f46e5;--uxbert-primary-hover:#4338ca;--uxbert-danger:#ef4444;--uxbert-success:#10b981;--uxbert-bg:#fff;--uxbert-text:#1f2937;--uxbert-border:#e5e7eb;--uxbert-shadow:0 10px 25px rgba(0,0,0,.1);--uxbert-z-button:999999;--uxbert-z-modal:1000000;--uxbert-z-canvas:1000001;--uxbert-z-toolbar:1000002}[data-theme=dark]{--uxbert-bg:#1f2937;--uxbert-text:#f9fafb;--uxbert-border:#374151}.uxbert-fab{align-items:center;background:var(--uxbert-primary);border:none;border-radius:50%;box-shadow:var(--uxbert-shadow);color:#fff;cursor:pointer;display:flex;font-size:24px;height:56px;justify-content:center;position:fixed;transition:all .3s ease;width:56px;z-index:var(--uxbert-z-button)}.uxbert-fab:hover{background:var(--uxbert-primary-hover);transform:scale(1.1)}.uxbert-fab.bottom-right{bottom:24px;right:24px}.uxbert-fab.bottom-left{bottom:24px;left:24px}.uxbert-fab.top-right{right:24px;top:24px}.uxbert-fab.top-left{left:24px;top:24px}.uxbert-overlay{align-items:center;animation:fadeIn .3s ease;background:rgba(0,0,0,.5);display:none;height:100%;justify-content:center;left:0;overflow:hidden;position:fixed;top:0;width:100%;z-index:var(--uxbert-z-modal)}.uxbert-overlay.active{display:flex}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.uxbert-modal{animation:slideUp .3s ease;background:var(--uxbert-bg);border-radius:12px;box-shadow:var(--uxbert-shadow);color:var(--uxbert-text);max-height:90vh;max-width:600px;overflow-y:auto;padding:24px;width:90%}@keyframes slideUp{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.uxbert-modal-header{align-items:center;display:flex;justify-content:space-between;margin-bottom:20px}.uxbert-modal-title{font-size:20px;font-weight:600;margin:0}.uxbert-modal-close{align-items:center;background:none;border:none;border-radius:4px;color:var(--uxbert-text);cursor:pointer;display:flex;font-size:24px;height:32px;justify-content:center;padding:0;width:32px}.uxbert-modal-close:hover{background:var(--uxbert-border)}.uxbert-form-group{margin-bottom:16px}.uxbert-form-label{display:block;font-size:14px;font-weight:500;margin-bottom:8px}.uxbert-capture-mode{display:flex;flex-wrap:wrap;gap:12px}.uxbert-radio-label{align-items:center;border:2px solid var(--uxbert-border);border-radius:8px;cursor:pointer;display:flex;flex:1;gap:8px;min-width:140px;padding:10px 16px;transition:all .2s ease}.uxbert-radio-label:hover{background:rgba(79,70,229,.05);border-color:var(--uxbert-primary)}.uxbert-radio-label input[type=radio]{cursor:pointer;height:18px;margin:0;width:18px}.uxbert-radio-label input[type=radio]:checked{accent-color:var(--uxbert-primary)}.uxbert-radio-label:has(input[type=radio]:checked){background:rgba(79,70,229,.1);border-color:var(--uxbert-primary)}.uxbert-radio-label span{flex:1;font-size:14px;font-weight:500}.uxbert-form-input,.uxbert-form-select,.uxbert-form-textarea{background:var(--uxbert-bg);border:1px solid var(--uxbert-border);border-radius:6px;box-sizing:border-box;color:var(--uxbert-text);font-family:inherit;font-size:14px;padding:10px;width:100%}.uxbert-form-textarea{min-height:100px;resize:vertical}.uxbert-form-input:focus,.uxbert-form-select:focus,.uxbert-form-textarea:focus{border-color:var(--uxbert-primary);outline:none}.uxbert-capture-action{margin:16px 0;text-align:center}.uxbert-capture-action .uxbert-btn{min-width:200px}.uxbert-screenshot-preview{border:1px solid var(--uxbert-border);border-radius:8px;margin:16px 0;overflow:hidden}.uxbert-screenshot-preview img{display:block;width:100%}.uxbert-screenshot-actions{background:var(--uxbert-border);display:flex;gap:8px;padding:12px}.uxbert-btn{align-items:center;border:none;border-radius:6px;cursor:pointer;display:inline-flex;font-size:14px;font-weight:500;gap:8px;padding:10px 20px;transition:all .2s ease}.uxbert-btn-primary{background:var(--uxbert-primary);color:#fff}.uxbert-btn-primary:hover{background:var(--uxbert-primary-hover)}.uxbert-btn-secondary{background:var(--uxbert-border);color:var(--uxbert-text)}.uxbert-btn-secondary:hover{background:#d1d5db}.uxbert-btn-success{background:var(--uxbert-success);color:#fff}.uxbert-btn-success:hover{background:#059669}.uxbert-btn-danger{background:var(--uxbert-danger);color:#fff}.uxbert-btn-danger:hover{background:#dc2626}.uxbert-modal-actions{display:flex;gap:12px;justify-content:flex-end;margin-top:20px}.uxbert-toolbar-toggle{align-items:center;background:var(--uxbert-primary);border:none;border-radius:50%;bottom:24px;box-shadow:var(--uxbert-shadow);color:#fff;cursor:pointer;display:none;font-size:24px;height:56px;justify-content:center;position:fixed;right:24px;transition:all .3s ease;width:56px;z-index:var(--uxbert-z-toolbar)}.uxbert-toolbar-toggle.active{display:flex}.uxbert-toolbar-toggle.hidden{opacity:0;pointer-events:none;transform:scale(.8)}.uxbert-toolbar-toggle:hover{background:var(--uxbert-primary-hover);transform:scale(1.1)}.uxbert-toolbar{background:var(--uxbert-bg);border-radius:12px;bottom:24px;box-shadow:var(--uxbert-shadow);display:none;flex-direction:column;gap:8px;max-height:80vh;opacity:0;overflow-y:auto;padding:16px;pointer-events:none;position:fixed;right:24px;transform:translateY(20px);transition:all .3s ease;z-index:var(--uxbert-z-toolbar)}.uxbert-toolbar.active{display:flex}.uxbert-toolbar.expanded{opacity:1;pointer-events:all;transform:translateY(0)}.uxbert-toolbar-header{align-items:center;display:flex;gap:8px;justify-content:space-between;margin-bottom:8px}.uxbert-toolbar-title{flex:1;font-size:14px;font-weight:600}.uxbert-toolbar-exit,.uxbert-toolbar-minimize{align-items:center;background:var(--uxbert-border);border:none;border-radius:6px;color:var(--uxbert-text);cursor:pointer;display:flex;font-size:20px;font-weight:700;height:32px;justify-content:center;transition:all .2s ease;width:32px}.uxbert-toolbar-exit{background:var(--uxbert-danger);color:#fff}.uxbert-toolbar-minimize:hover{background:#d1d5db;transform:scale(1.1)}.uxbert-toolbar-exit:hover{background:#dc2626;transform:scale(1.1)}.uxbert-toolbar-tools{display:flex;flex-wrap:wrap;gap:8px}.uxbert-tool-btn{align-items:center;background:var(--uxbert-bg);border:2px solid var(--uxbert-border);border-radius:6px;cursor:pointer;display:flex;font-family:Arial,sans-serif;font-size:18px;font-weight:700;height:40px;justify-content:center;transition:all .2s ease;width:40px}.uxbert-tool-btn.active,.uxbert-tool-btn:hover{background:var(--uxbert-primary);border-color:var(--uxbert-primary);color:#fff}.uxbert-color-picker{display:flex;gap:6px;margin:8px 0}.uxbert-color-option{border:2px solid var(--uxbert-border);border-radius:50%;cursor:pointer;height:32px;transition:transform .2s ease;width:32px}.uxbert-color-option:hover{transform:scale(1.1)}.uxbert-color-option.active{border:3px solid var(--uxbert-text)}.uxbert-canvas-overlay{cursor:crosshair;display:none;left:0;position:absolute;top:0;z-index:var(--uxbert-z-canvas)}.uxbert-canvas-overlay.active{display:block}.uxbert-canvas-overlay.viewport-mode{left:0;position:fixed;top:0}.uxbert-text-input{background:hsla(0,0%,100%,.95);border:2px solid var(--uxbert-primary);border-radius:6px;box-shadow:0 4px 12px rgba(0,0,0,.15);color:var(--uxbert-text);font-family:Arial,sans-serif;font-size:16px;font-weight:700;min-width:200px;outline:none;padding:8px 12px;z-index:var(--uxbert-z-toolbar)}.uxbert-text-input:focus{border-color:var(--uxbert-primary-hover);box-shadow:0 4px 16px rgba(79,70,229,.3)}.uxbert-text-input::placeholder{color:#9ca3af;font-weight:400}@media (max-width:768px){.uxbert-modal{max-height:95vh;padding:16px;width:95%}.uxbert-toolbar{bottom:90px;left:16px;max-height:60vh;right:16px;width:auto}.uxbert-toolbar-toggle{bottom:16px;height:56px;right:16px;width:56px}.uxbert-fab{font-size:20px;height:48px;width:48px}.uxbert-color-picker,.uxbert-toolbar-tools{justify-content:center}}";
10265
10572
  styleInject(css_248z);
10266
10573
 
10267
10574
  // Main initialization file
@@ -10293,7 +10600,9 @@ class Reportly {
10293
10600
  this.state = new State();
10294
10601
  this.deviceInfo = new DeviceInfo();
10295
10602
  this.screenshot = new Screenshot();
10296
- this.export = new Export();
10603
+ // Initialize export with n8n config if provided
10604
+ const n8nConfig = userConfig.integrations?.n8n;
10605
+ this.export = new Export(n8nConfig);
10297
10606
  // Initialize UI components
10298
10607
  this.button = new Button(this.config, {
10299
10608
  onClick: () => this.handleButtonClick(),
@@ -10304,6 +10613,7 @@ class Reportly {
10304
10613
  onAnnotate: () => this.startAnnotation(),
10305
10614
  onRetake: () => this.retakeScreenshot(),
10306
10615
  onCapture: () => this.captureWithMode(),
10616
+ onSendToN8n: (issueData) => this.handleN8nSubmit(issueData),
10307
10617
  });
10308
10618
  this.toolbar = new Toolbar({
10309
10619
  onToolChange: (tool) => this.annotation?.setTool(tool),
@@ -10319,6 +10629,10 @@ class Reportly {
10319
10629
  this.modal.create();
10320
10630
  this.toolbar.create();
10321
10631
  this.annotation.createCanvas();
10632
+ // Show/hide n8n button based on configuration
10633
+ if (this.export.isN8nEnabled()) {
10634
+ this.modal.setN8nButtonVisible(true);
10635
+ }
10322
10636
  // Setup keyboard shortcuts
10323
10637
  this.setupKeyboardShortcuts();
10324
10638
  this.isInitialized = true;
@@ -10446,6 +10760,54 @@ class Reportly {
10446
10760
  alert("Failed to export issue. Please try again.");
10447
10761
  }
10448
10762
  }
10763
+ async handleN8nSubmit(issueData) {
10764
+ try {
10765
+ // Create complete issue package
10766
+ const completeIssue = {
10767
+ ...issueData,
10768
+ screenshot: this.state?.getScreenshot() || "",
10769
+ deviceInfo: this.deviceInfo.get(),
10770
+ createdAt: new Date().toISOString(),
10771
+ };
10772
+ // Show loading state
10773
+ const n8nBtn = document.querySelector('#uxbert-n8n-btn');
10774
+ if (n8nBtn) {
10775
+ n8nBtn.disabled = true;
10776
+ n8nBtn.textContent = '⏳ Sending...';
10777
+ }
10778
+ // Send to n8n
10779
+ const response = await this.export?.sendToN8n(completeIssue);
10780
+ if (response?.success) {
10781
+ // Emit event
10782
+ this.state?.emit("issue:sent-to-n8n", completeIssue);
10783
+ // Reset state
10784
+ this.state?.reset();
10785
+ this.annotation?.clear();
10786
+ this.modal?.close();
10787
+ alert("Issue sent to n8n successfully!");
10788
+ console.log("Issue sent to n8n successfully");
10789
+ }
10790
+ else {
10791
+ alert(`Failed to send to n8n: ${response?.message || 'Unknown error'}`);
10792
+ console.error("Failed to send to n8n:", response?.error);
10793
+ }
10794
+ // Reset button state
10795
+ if (n8nBtn) {
10796
+ n8nBtn.disabled = false;
10797
+ n8nBtn.textContent = '🚀 Send to n8n';
10798
+ }
10799
+ }
10800
+ catch (error) {
10801
+ console.error("Failed to send to n8n:", error);
10802
+ alert("Failed to send to n8n. Please try again.");
10803
+ // Reset button state
10804
+ const n8nBtn = document.querySelector('#uxbert-n8n-btn');
10805
+ if (n8nBtn) {
10806
+ n8nBtn.disabled = false;
10807
+ n8nBtn.textContent = '🚀 Send to n8n';
10808
+ }
10809
+ }
10810
+ }
10449
10811
  handleModalClose() {
10450
10812
  this.annotation?.hide();
10451
10813
  this.toolbar?.hide();
@@ -10474,6 +10836,24 @@ class Reportly {
10474
10836
  exportAllIssues() {
10475
10837
  this.export?.exportAllIssues();
10476
10838
  }
10839
+ // n8n Integration API methods
10840
+ configureN8n(webhookUrl, enabled = true, options) {
10841
+ this.export?.configureN8n({
10842
+ webhookUrl,
10843
+ enabled,
10844
+ headers: options?.headers,
10845
+ timeout: options?.timeout
10846
+ });
10847
+ // Update button visibility
10848
+ this.modal?.setN8nButtonVisible(enabled);
10849
+ }
10850
+ enableN8n(enabled) {
10851
+ this.export?.setN8nEnabled(enabled);
10852
+ this.modal?.setN8nButtonVisible(enabled);
10853
+ }
10854
+ testN8nConnection() {
10855
+ return this.export?.testN8nConnection() || Promise.resolve({ success: false, message: 'Not initialized' });
10856
+ }
10477
10857
  on(event, callback) {
10478
10858
  this.state?.on(event, callback);
10479
10859
  }