uidex 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -523,6 +523,154 @@ var Inspector = class {
523
523
  }
524
524
  };
525
525
 
526
+ // src/core/ingest.ts
527
+ var MAX_CONSOLE_LOGS = 50;
528
+ var MAX_NETWORK_ERRORS = 20;
529
+ var nativeFetch = null;
530
+ function safeStringify(value) {
531
+ if (typeof value === "string") return value;
532
+ try {
533
+ return JSON.stringify(value);
534
+ } catch {
535
+ return String(value);
536
+ }
537
+ }
538
+ var IngestCapture = class {
539
+ constructor(captureConsole, captureNetwork) {
540
+ this.captureConsole = captureConsole;
541
+ this.captureNetwork = captureNetwork;
542
+ }
543
+ consoleLogs = [];
544
+ networkErrors = [];
545
+ originalConsoleWarn = null;
546
+ originalConsoleError = null;
547
+ originalFetch = null;
548
+ start() {
549
+ this.consoleLogs = [];
550
+ this.networkErrors = [];
551
+ if (this.captureConsole) this.interceptConsole();
552
+ if (this.captureNetwork) this.interceptNetwork();
553
+ }
554
+ stop() {
555
+ this.restoreConsole();
556
+ this.restoreNetwork();
557
+ }
558
+ getConsoleLogs() {
559
+ return [...this.consoleLogs];
560
+ }
561
+ getNetworkErrors() {
562
+ return [...this.networkErrors];
563
+ }
564
+ interceptConsole() {
565
+ if (this.originalConsoleWarn) return;
566
+ this.originalConsoleWarn = console.warn;
567
+ this.originalConsoleError = console.error;
568
+ console.warn = (...args) => {
569
+ this.addConsoleLog("warn", args);
570
+ this.originalConsoleWarn.apply(console, args);
571
+ };
572
+ console.error = (...args) => {
573
+ this.addConsoleLog("error", args);
574
+ this.originalConsoleError.apply(console, args);
575
+ };
576
+ }
577
+ addConsoleLog(level, args) {
578
+ this.consoleLogs.push({
579
+ level,
580
+ message: args.map(safeStringify).join(" "),
581
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
582
+ });
583
+ if (this.consoleLogs.length > MAX_CONSOLE_LOGS) {
584
+ this.consoleLogs.shift();
585
+ }
586
+ }
587
+ restoreConsole() {
588
+ if (this.originalConsoleWarn) {
589
+ console.warn = this.originalConsoleWarn;
590
+ this.originalConsoleWarn = null;
591
+ }
592
+ if (this.originalConsoleError) {
593
+ console.error = this.originalConsoleError;
594
+ this.originalConsoleError = null;
595
+ }
596
+ }
597
+ interceptNetwork() {
598
+ if (this.originalFetch) return;
599
+ this.originalFetch = window.fetch;
600
+ if (!nativeFetch) nativeFetch = this.originalFetch;
601
+ window.fetch = async (...args) => {
602
+ try {
603
+ const response = await this.originalFetch.apply(window, args);
604
+ if (!response.ok) {
605
+ this.addNetworkError(args[0], args[1]?.method, response.status, response.statusText);
606
+ }
607
+ return response;
608
+ } catch (error) {
609
+ this.addNetworkError(
610
+ args[0],
611
+ args[1]?.method,
612
+ null,
613
+ error instanceof Error ? error.message : "Network error"
614
+ );
615
+ throw error;
616
+ }
617
+ };
618
+ }
619
+ addNetworkError(input, method, status, statusText) {
620
+ let url;
621
+ if (typeof input === "string") {
622
+ url = input;
623
+ } else if (input instanceof URL) {
624
+ url = input.href;
625
+ } else {
626
+ url = input.url;
627
+ method ??= input.method;
628
+ }
629
+ this.networkErrors.push({
630
+ url,
631
+ method: method ?? "GET",
632
+ status,
633
+ statusText,
634
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
635
+ });
636
+ if (this.networkErrors.length > MAX_NETWORK_ERRORS) {
637
+ this.networkErrors.shift();
638
+ }
639
+ }
640
+ restoreNetwork() {
641
+ if (this.originalFetch) {
642
+ window.fetch = this.originalFetch;
643
+ this.originalFetch = null;
644
+ }
645
+ }
646
+ };
647
+ function generateSessionId() {
648
+ if (typeof crypto !== "undefined" && crypto.randomUUID) {
649
+ return crypto.randomUUID();
650
+ }
651
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
652
+ const r = Math.random() * 16 | 0;
653
+ const v = c === "x" ? r : r & 3 | 8;
654
+ return v.toString(16);
655
+ });
656
+ }
657
+ async function submitFeedback(endpoint, apiKey, report) {
658
+ const fetchFn = nativeFetch ?? fetch;
659
+ const response = await fetchFn(endpoint, {
660
+ method: "POST",
661
+ headers: {
662
+ "Content-Type": "application/json",
663
+ Authorization: `Bearer ${apiKey}`
664
+ },
665
+ body: JSON.stringify(report)
666
+ });
667
+ if (!response.ok) {
668
+ const text = await response.text().catch(() => "");
669
+ throw new Error(`Ingest failed (${response.status}): ${text}`);
670
+ }
671
+ return response.json();
672
+ }
673
+
526
674
  // src/core/modal.ts
527
675
  var Modal = class _Modal {
528
676
  backdrop = null;
@@ -1505,6 +1653,14 @@ var Modal = class _Modal {
1505
1653
  "medium"
1506
1654
  );
1507
1655
  form.appendChild(severitySelect.group);
1656
+ const titleGroup = this.createFormGroup("Title");
1657
+ const titleInput = document.createElement("input");
1658
+ titleInput.type = "text";
1659
+ titleInput.className = "uidex-form-input";
1660
+ titleInput.placeholder = "Brief summary (optional)";
1661
+ titleInput.maxLength = 200;
1662
+ titleGroup.appendChild(titleInput);
1663
+ form.appendChild(titleGroup);
1508
1664
  const descGroup = this.createFormGroup("Description");
1509
1665
  const textarea = document.createElement("textarea");
1510
1666
  textarea.className = "uidex-form-textarea";
@@ -1512,6 +1668,22 @@ var Modal = class _Modal {
1512
1668
  textarea.rows = 4;
1513
1669
  descGroup.appendChild(textarea);
1514
1670
  form.appendChild(descGroup);
1671
+ let emailInput;
1672
+ let nameInput;
1673
+ if (this.options.ingest && !this.options.ingest.reporter) {
1674
+ const reporterGroup = this.createFormGroup("Reporter");
1675
+ nameInput = document.createElement("input");
1676
+ nameInput.type = "text";
1677
+ nameInput.className = "uidex-form-input";
1678
+ nameInput.placeholder = "Name (optional)";
1679
+ reporterGroup.appendChild(nameInput);
1680
+ emailInput = document.createElement("input");
1681
+ emailInput.type = "email";
1682
+ emailInput.className = "uidex-form-input";
1683
+ emailInput.placeholder = "Email (optional)";
1684
+ reporterGroup.appendChild(emailInput);
1685
+ form.appendChild(reporterGroup);
1686
+ }
1515
1687
  const screenshotGroup = document.createElement("div");
1516
1688
  screenshotGroup.className = "uidex-form-group";
1517
1689
  const screenshotLabel = document.createElement("label");
@@ -1547,9 +1719,14 @@ var Modal = class _Modal {
1547
1719
  }
1548
1720
  const env = this.collectEnv();
1549
1721
  const page = this.data.pages.find((p) => p.componentIds.includes(id));
1722
+ const { ingest } = this.options;
1723
+ const reporterEmail = ingest?.reporter?.email || emailInput?.value.trim() || void 0;
1724
+ const reporterName = ingest?.reporter?.name || nameInput?.value.trim() || void 0;
1725
+ const titleValue = titleInput.value.trim();
1550
1726
  const report = {
1551
1727
  type: typeSelect.select.value,
1552
1728
  severity: severitySelect.select.value,
1729
+ ...titleValue ? { title: titleValue } : {},
1553
1730
  description: textarea.value.trim(),
1554
1731
  componentId: id,
1555
1732
  element: element ? this.describeElement(element) : null,
@@ -1558,17 +1735,66 @@ var Modal = class _Modal {
1558
1735
  path: window.location.pathname,
1559
1736
  route: page?.dir ?? null,
1560
1737
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1738
+ pageTitle: document.title,
1739
+ locale: navigator.language,
1740
+ sessionId: this.options.sessionId ?? "",
1561
1741
  viewport: env.viewport,
1562
1742
  screen: env.screen,
1563
1743
  userAgent: env.userAgent,
1564
- screenshot
1744
+ screenshot,
1745
+ ...reporterEmail ? { reporterEmail } : {},
1746
+ ...reporterName ? { reporterName } : {},
1747
+ ...ingest?.environment ? { environment: ingest.environment } : {},
1748
+ ...ingest?.appVersion ? { appVersion: ingest.appVersion } : {},
1749
+ ...ingest?.metadata ? { metadata: ingest.metadata } : {}
1750
+ };
1751
+ const consoleLogs = this.options.getConsoleLogs?.();
1752
+ if (consoleLogs && consoleLogs.length > 0) {
1753
+ report.consoleLogs = consoleLogs;
1754
+ }
1755
+ const networkErrors = this.options.getNetworkErrors?.();
1756
+ if (networkErrors && networkErrors.length > 0) {
1757
+ report.networkErrors = networkErrors;
1758
+ }
1759
+ const showSuccess = (autoClose) => {
1760
+ submitBtn.textContent = "Submitted!";
1761
+ if (autoClose) {
1762
+ setTimeout(() => this.hide(), 1500);
1763
+ } else {
1764
+ setTimeout(() => {
1765
+ submitBtn.textContent = "Submit";
1766
+ submitBtn.disabled = false;
1767
+ }, 1500);
1768
+ }
1565
1769
  };
1566
- console.log("[uidex] Feedback submitted:", report);
1567
- submitBtn.textContent = "Submitted!";
1568
- setTimeout(() => {
1569
- submitBtn.textContent = "Submit";
1570
- submitBtn.disabled = false;
1571
- }, 1500);
1770
+ if (ingest) {
1771
+ submitBtn.textContent = "Submitting\u2026";
1772
+ try {
1773
+ const serverResult = await submitFeedback(
1774
+ ingest.endpoint,
1775
+ ingest.apiKey,
1776
+ report
1777
+ );
1778
+ this.options.onSubmit?.(report, {
1779
+ ok: true,
1780
+ id: serverResult.id,
1781
+ sequenceNumber: serverResult.sequenceNumber
1782
+ });
1783
+ showSuccess(true);
1784
+ } catch (err) {
1785
+ const errorMessage = err instanceof Error ? err.message : "Unknown error";
1786
+ console.warn("[uidex] Feedback submission failed:", errorMessage);
1787
+ this.options.onSubmit?.(report, { ok: false, error: errorMessage });
1788
+ submitBtn.textContent = "Failed \u2014 retry?";
1789
+ submitBtn.disabled = false;
1790
+ }
1791
+ } else {
1792
+ if (!this.options.onSubmit) {
1793
+ console.log("[uidex] Feedback submitted:", report);
1794
+ }
1795
+ this.options.onSubmit?.(report, { ok: true, id: "", sequenceNumber: 0 });
1796
+ showSuccess(false);
1797
+ }
1572
1798
  });
1573
1799
  form.appendChild(submitBtn);
1574
1800
  this.mainContent.appendChild(form);
@@ -2688,49 +2914,56 @@ body.uidex-inspecting * {
2688
2914
  color: var(--uidex-color-text-muted);
2689
2915
  }
2690
2916
 
2691
- .uidex-form-select {
2692
- appearance: none;
2693
- padding: 6px 10px;
2917
+ .uidex-form-select,
2918
+ .uidex-form-input,
2919
+ .uidex-form-textarea {
2694
2920
  border: 1px solid var(--uidex-color-border);
2695
2921
  border-radius: 0;
2696
2922
  background: transparent;
2697
2923
  color: var(--uidex-color-text);
2698
2924
  font-size: var(--uidex-font-size-sm);
2699
2925
  font-family: var(--uidex-font-mono);
2700
- cursor: pointer;
2701
2926
  outline: none;
2927
+ }
2928
+
2929
+ .uidex-form-select:focus,
2930
+ .uidex-form-input:focus,
2931
+ .uidex-form-textarea:focus {
2932
+ border-color: rgba(255, 255, 255, 0.3);
2933
+ }
2934
+
2935
+ .uidex-form-input::placeholder,
2936
+ .uidex-form-textarea::placeholder {
2937
+ color: var(--uidex-color-text-muted);
2938
+ }
2939
+
2940
+ .uidex-form-select {
2941
+ appearance: none;
2942
+ padding: 6px 10px;
2943
+ cursor: pointer;
2702
2944
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23ababab' d='M3 5l3 3 3-3'/%3E%3C/svg%3E");
2703
2945
  background-repeat: no-repeat;
2704
2946
  background-position: right 8px center;
2705
2947
  padding-right: 26px;
2706
2948
  }
2707
2949
 
2708
- .uidex-form-select:focus {
2709
- border-color: rgba(255, 255, 255, 0.3);
2950
+ .uidex-form-input {
2951
+ padding: 6px 10px;
2952
+ width: 100%;
2953
+ box-sizing: border-box;
2954
+ }
2955
+
2956
+ .uidex-form-input + .uidex-form-input {
2957
+ margin-top: 6px;
2710
2958
  }
2711
2959
 
2712
2960
  .uidex-form-textarea {
2713
2961
  padding: 8px 10px;
2714
- border: 1px solid var(--uidex-color-border);
2715
- border-radius: 0;
2716
- background: transparent;
2717
- color: var(--uidex-color-text);
2718
- font-size: var(--uidex-font-size-sm);
2719
- font-family: var(--uidex-font-mono);
2720
2962
  line-height: 1.5;
2721
2963
  resize: vertical;
2722
- outline: none;
2723
2964
  min-height: 80px;
2724
2965
  }
2725
2966
 
2726
- .uidex-form-textarea::placeholder {
2727
- color: var(--uidex-color-text-muted);
2728
- }
2729
-
2730
- .uidex-form-textarea:focus {
2731
- border-color: rgba(255, 255, 255, 0.3);
2732
- }
2733
-
2734
2967
  .uidex-form-submit {
2735
2968
  padding: 6px 16px;
2736
2969
  border: 1px solid var(--uidex-color-border);
@@ -2749,7 +2982,7 @@ body.uidex-inspecting * {
2749
2982
  background: var(--uidex-color-primary-hover);
2750
2983
  }
2751
2984
 
2752
- .uidex-form-submit:active {
2985
+ .uidex-form-submit:not(:disabled):active {
2753
2986
  transform: translateY(1px);
2754
2987
  }
2755
2988
 
@@ -2893,8 +3126,17 @@ var UidexUI = class {
2893
3126
  copyTimer = null;
2894
3127
  currentPresentIds = [];
2895
3128
  activeMode = null;
3129
+ sessionId;
3130
+ capture = null;
2896
3131
  constructor(options = {}) {
2897
3132
  this.options = options;
3133
+ this.sessionId = generateSessionId();
3134
+ if (options.ingest?.captureConsole || options.ingest?.captureNetwork) {
3135
+ this.capture = new IngestCapture(
3136
+ options.ingest.captureConsole ?? false,
3137
+ options.ingest.captureNetwork ?? false
3138
+ );
3139
+ }
2898
3140
  this.overlay = new Overlay({
2899
3141
  color: options.config?.defaults?.color,
2900
3142
  borderStyle: options.config?.defaults?.borderStyle,
@@ -2911,7 +3153,12 @@ var UidexUI = class {
2911
3153
  }
2912
3154
  this.options.onSelect?.(id);
2913
3155
  },
2914
- elementGetter: (id) => this.findElement(id)
3156
+ elementGetter: (id) => this.findElement(id),
3157
+ ingest: options.ingest,
3158
+ onSubmit: options.onSubmit,
3159
+ sessionId: this.sessionId,
3160
+ getConsoleLogs: () => this.capture?.getConsoleLogs() ?? [],
3161
+ getNetworkErrors: () => this.capture?.getNetworkErrors() ?? []
2915
3162
  });
2916
3163
  this.menu = new Menu({
2917
3164
  onInspectToggle: () => this.toggleMode("inspect"),
@@ -2962,12 +3209,14 @@ var UidexUI = class {
2962
3209
  this.modal.setShadowRoot(this.shadowRoot);
2963
3210
  this.updateModalData(presentIds);
2964
3211
  this.inspector?.mount();
3212
+ this.capture?.start();
2965
3213
  this.startObserving();
2966
3214
  this.mounted = true;
2967
3215
  }
2968
3216
  destroy() {
2969
3217
  if (!this.mounted) return;
2970
3218
  this.stopObserving();
3219
+ this.capture?.stop();
2971
3220
  if (this.copyTimer !== null) {
2972
3221
  clearTimeout(this.copyTimer);
2973
3222
  this.copyTimer = null;
@@ -3153,6 +3402,7 @@ function createUidexUI(options = {}) {
3153
3402
  return new UidexUI(options);
3154
3403
  }
3155
3404
  export {
3405
+ IngestCapture,
3156
3406
  Inspector,
3157
3407
  Menu,
3158
3408
  Modal,
@@ -3160,6 +3410,7 @@ export {
3160
3410
  UidexUI,
3161
3411
  classNames,
3162
3412
  createUidexUI,
3413
+ generateSessionId,
3163
3414
  getComponents,
3164
3415
  getContrastColor,
3165
3416
  getFeatures,
@@ -3169,6 +3420,7 @@ export {
3169
3420
  registerComponents,
3170
3421
  registerFeatures,
3171
3422
  registerPages,
3172
- resolveColor
3423
+ resolveColor,
3424
+ submitFeedback
3173
3425
  };
3174
3426
  //# sourceMappingURL=index.js.map