uidex 0.1.1 → 0.2.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.
@@ -232,7 +232,7 @@ function resolveColor(color, colorMap) {
232
232
  }
233
233
 
234
234
  // src/core/overlay.ts
235
- var DEFAULT_COLOR = "#e5e5e5";
235
+ var DEFAULT_COLOR = "#3b82f6";
236
236
  var DEFAULT_BORDER_STYLE = "solid";
237
237
  var DEFAULT_BORDER_WIDTH = 2;
238
238
  var DEFAULT_LABEL_POSITION = "top-left";
@@ -383,6 +383,23 @@ var Overlay = class {
383
383
  this.element.style.left = `${rect.left}px`;
384
384
  this.element.style.width = `${rect.width}px`;
385
385
  this.element.style.height = `${rect.height}px`;
386
+ this.clampLabel(rect);
387
+ }
388
+ /** Move the label inside the overlay when there is no room outside. */
389
+ clampLabel(rect) {
390
+ if (!this.labelElement || this.labelElement.style.display === "none") return;
391
+ const {
392
+ labelPosition = DEFAULT_LABEL_POSITION
393
+ } = this.options;
394
+ const isTop = labelPosition === "top-left" || labelPosition === "top-right";
395
+ const labelHeight = 20;
396
+ if (isTop && rect.top < labelHeight) {
397
+ this.labelElement.style.top = "4px";
398
+ this.labelElement.style.transform = "";
399
+ } else if (!isTop && window.innerHeight - rect.bottom < labelHeight) {
400
+ this.labelElement.style.bottom = "4px";
401
+ this.labelElement.style.transform = "";
402
+ }
386
403
  }
387
404
  addListeners() {
388
405
  window.addEventListener("resize", this.boundUpdatePosition);
@@ -523,6 +540,154 @@ var Inspector = class {
523
540
  }
524
541
  };
525
542
 
543
+ // src/core/ingest.ts
544
+ var MAX_CONSOLE_LOGS = 50;
545
+ var MAX_NETWORK_ERRORS = 20;
546
+ var nativeFetch = null;
547
+ function safeStringify(value) {
548
+ if (typeof value === "string") return value;
549
+ try {
550
+ return JSON.stringify(value);
551
+ } catch {
552
+ return String(value);
553
+ }
554
+ }
555
+ var IngestCapture = class {
556
+ constructor(captureConsole, captureNetwork) {
557
+ this.captureConsole = captureConsole;
558
+ this.captureNetwork = captureNetwork;
559
+ }
560
+ consoleLogs = [];
561
+ networkErrors = [];
562
+ originalConsoleWarn = null;
563
+ originalConsoleError = null;
564
+ originalFetch = null;
565
+ start() {
566
+ this.consoleLogs = [];
567
+ this.networkErrors = [];
568
+ if (this.captureConsole) this.interceptConsole();
569
+ if (this.captureNetwork) this.interceptNetwork();
570
+ }
571
+ stop() {
572
+ this.restoreConsole();
573
+ this.restoreNetwork();
574
+ }
575
+ getConsoleLogs() {
576
+ return [...this.consoleLogs];
577
+ }
578
+ getNetworkErrors() {
579
+ return [...this.networkErrors];
580
+ }
581
+ interceptConsole() {
582
+ if (this.originalConsoleWarn) return;
583
+ this.originalConsoleWarn = console.warn;
584
+ this.originalConsoleError = console.error;
585
+ console.warn = (...args) => {
586
+ this.addConsoleLog("warn", args);
587
+ this.originalConsoleWarn.apply(console, args);
588
+ };
589
+ console.error = (...args) => {
590
+ this.addConsoleLog("error", args);
591
+ this.originalConsoleError.apply(console, args);
592
+ };
593
+ }
594
+ addConsoleLog(level, args) {
595
+ this.consoleLogs.push({
596
+ level,
597
+ message: args.map(safeStringify).join(" "),
598
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
599
+ });
600
+ if (this.consoleLogs.length > MAX_CONSOLE_LOGS) {
601
+ this.consoleLogs.shift();
602
+ }
603
+ }
604
+ restoreConsole() {
605
+ if (this.originalConsoleWarn) {
606
+ console.warn = this.originalConsoleWarn;
607
+ this.originalConsoleWarn = null;
608
+ }
609
+ if (this.originalConsoleError) {
610
+ console.error = this.originalConsoleError;
611
+ this.originalConsoleError = null;
612
+ }
613
+ }
614
+ interceptNetwork() {
615
+ if (this.originalFetch) return;
616
+ this.originalFetch = window.fetch;
617
+ if (!nativeFetch) nativeFetch = this.originalFetch;
618
+ window.fetch = async (...args) => {
619
+ try {
620
+ const response = await this.originalFetch.apply(window, args);
621
+ if (!response.ok) {
622
+ this.addNetworkError(args[0], args[1]?.method, response.status, response.statusText);
623
+ }
624
+ return response;
625
+ } catch (error) {
626
+ this.addNetworkError(
627
+ args[0],
628
+ args[1]?.method,
629
+ null,
630
+ error instanceof Error ? error.message : "Network error"
631
+ );
632
+ throw error;
633
+ }
634
+ };
635
+ }
636
+ addNetworkError(input, method, status, statusText) {
637
+ let url;
638
+ if (typeof input === "string") {
639
+ url = input;
640
+ } else if (input instanceof URL) {
641
+ url = input.href;
642
+ } else {
643
+ url = input.url;
644
+ method ??= input.method;
645
+ }
646
+ this.networkErrors.push({
647
+ url,
648
+ method: method ?? "GET",
649
+ status,
650
+ statusText,
651
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
652
+ });
653
+ if (this.networkErrors.length > MAX_NETWORK_ERRORS) {
654
+ this.networkErrors.shift();
655
+ }
656
+ }
657
+ restoreNetwork() {
658
+ if (this.originalFetch) {
659
+ window.fetch = this.originalFetch;
660
+ this.originalFetch = null;
661
+ }
662
+ }
663
+ };
664
+ function generateSessionId() {
665
+ if (typeof crypto !== "undefined" && crypto.randomUUID) {
666
+ return crypto.randomUUID();
667
+ }
668
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
669
+ const r = Math.random() * 16 | 0;
670
+ const v = c === "x" ? r : r & 3 | 8;
671
+ return v.toString(16);
672
+ });
673
+ }
674
+ async function submitFeedback(endpoint, apiKey, report) {
675
+ const fetchFn = nativeFetch ?? fetch;
676
+ const response = await fetchFn(endpoint, {
677
+ method: "POST",
678
+ headers: {
679
+ "Content-Type": "application/json",
680
+ Authorization: `Bearer ${apiKey}`
681
+ },
682
+ body: JSON.stringify(report)
683
+ });
684
+ if (!response.ok) {
685
+ const text = await response.text().catch(() => "");
686
+ throw new Error(`Ingest failed (${response.status}): ${text}`);
687
+ }
688
+ return response.json();
689
+ }
690
+
526
691
  // src/core/modal.ts
527
692
  var Modal = class _Modal {
528
693
  backdrop = null;
@@ -1505,6 +1670,14 @@ var Modal = class _Modal {
1505
1670
  "medium"
1506
1671
  );
1507
1672
  form.appendChild(severitySelect.group);
1673
+ const titleGroup = this.createFormGroup("Title");
1674
+ const titleInput = document.createElement("input");
1675
+ titleInput.type = "text";
1676
+ titleInput.className = "uidex-form-input";
1677
+ titleInput.placeholder = "Brief summary (optional)";
1678
+ titleInput.maxLength = 200;
1679
+ titleGroup.appendChild(titleInput);
1680
+ form.appendChild(titleGroup);
1508
1681
  const descGroup = this.createFormGroup("Description");
1509
1682
  const textarea = document.createElement("textarea");
1510
1683
  textarea.className = "uidex-form-textarea";
@@ -1512,6 +1685,22 @@ var Modal = class _Modal {
1512
1685
  textarea.rows = 4;
1513
1686
  descGroup.appendChild(textarea);
1514
1687
  form.appendChild(descGroup);
1688
+ let emailInput;
1689
+ let nameInput;
1690
+ if (this.options.ingest && !this.options.ingest.reporter) {
1691
+ const reporterGroup = this.createFormGroup("Reporter");
1692
+ nameInput = document.createElement("input");
1693
+ nameInput.type = "text";
1694
+ nameInput.className = "uidex-form-input";
1695
+ nameInput.placeholder = "Name (optional)";
1696
+ reporterGroup.appendChild(nameInput);
1697
+ emailInput = document.createElement("input");
1698
+ emailInput.type = "email";
1699
+ emailInput.className = "uidex-form-input";
1700
+ emailInput.placeholder = "Email (optional)";
1701
+ reporterGroup.appendChild(emailInput);
1702
+ form.appendChild(reporterGroup);
1703
+ }
1515
1704
  const screenshotGroup = document.createElement("div");
1516
1705
  screenshotGroup.className = "uidex-form-group";
1517
1706
  const screenshotLabel = document.createElement("label");
@@ -1547,9 +1736,14 @@ var Modal = class _Modal {
1547
1736
  }
1548
1737
  const env = this.collectEnv();
1549
1738
  const page = this.data.pages.find((p) => p.componentIds.includes(id));
1739
+ const { ingest } = this.options;
1740
+ const reporterEmail = ingest?.reporter?.email || emailInput?.value.trim() || void 0;
1741
+ const reporterName = ingest?.reporter?.name || nameInput?.value.trim() || void 0;
1742
+ const titleValue = titleInput.value.trim();
1550
1743
  const report = {
1551
1744
  type: typeSelect.select.value,
1552
1745
  severity: severitySelect.select.value,
1746
+ ...titleValue ? { title: titleValue } : {},
1553
1747
  description: textarea.value.trim(),
1554
1748
  componentId: id,
1555
1749
  element: element ? this.describeElement(element) : null,
@@ -1558,17 +1752,66 @@ var Modal = class _Modal {
1558
1752
  path: window.location.pathname,
1559
1753
  route: page?.dir ?? null,
1560
1754
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1755
+ pageTitle: document.title,
1756
+ locale: navigator.language,
1757
+ sessionId: this.options.sessionId ?? "",
1561
1758
  viewport: env.viewport,
1562
1759
  screen: env.screen,
1563
1760
  userAgent: env.userAgent,
1564
- screenshot
1761
+ screenshot,
1762
+ ...reporterEmail ? { reporterEmail } : {},
1763
+ ...reporterName ? { reporterName } : {},
1764
+ ...ingest?.environment ? { environment: ingest.environment } : {},
1765
+ ...ingest?.appVersion ? { appVersion: ingest.appVersion } : {},
1766
+ ...ingest?.metadata ? { metadata: ingest.metadata } : {}
1565
1767
  };
1566
- console.log("[uidex] Feedback submitted:", report);
1567
- submitBtn.textContent = "Submitted!";
1568
- setTimeout(() => {
1569
- submitBtn.textContent = "Submit";
1570
- submitBtn.disabled = false;
1571
- }, 1500);
1768
+ const consoleLogs = this.options.getConsoleLogs?.();
1769
+ if (consoleLogs && consoleLogs.length > 0) {
1770
+ report.consoleLogs = consoleLogs;
1771
+ }
1772
+ const networkErrors = this.options.getNetworkErrors?.();
1773
+ if (networkErrors && networkErrors.length > 0) {
1774
+ report.networkErrors = networkErrors;
1775
+ }
1776
+ const showSuccess = (autoClose) => {
1777
+ submitBtn.textContent = "Submitted!";
1778
+ if (autoClose) {
1779
+ setTimeout(() => this.hide(), 1500);
1780
+ } else {
1781
+ setTimeout(() => {
1782
+ submitBtn.textContent = "Submit";
1783
+ submitBtn.disabled = false;
1784
+ }, 1500);
1785
+ }
1786
+ };
1787
+ if (ingest) {
1788
+ submitBtn.textContent = "Submitting\u2026";
1789
+ try {
1790
+ const serverResult = await submitFeedback(
1791
+ ingest.endpoint,
1792
+ ingest.apiKey,
1793
+ report
1794
+ );
1795
+ this.options.onSubmit?.(report, {
1796
+ ok: true,
1797
+ id: serverResult.id,
1798
+ sequenceNumber: serverResult.sequenceNumber
1799
+ });
1800
+ showSuccess(true);
1801
+ } catch (err) {
1802
+ const errorMessage = err instanceof Error ? err.message : "Unknown error";
1803
+ console.warn("[uidex] Feedback submission failed:", errorMessage);
1804
+ this.options.onSubmit?.(report, { ok: false, error: errorMessage });
1805
+ submitBtn.textContent = "Failed \u2014 retry?";
1806
+ submitBtn.disabled = false;
1807
+ }
1808
+ } else {
1809
+ if (!this.options.onSubmit) {
1810
+ console.log("[uidex] Feedback submitted:", report);
1811
+ }
1812
+ this.options.onSubmit?.(report, { ok: true, id: "", sequenceNumber: 0 });
1813
+ showSuccess(false);
1814
+ }
1572
1815
  });
1573
1816
  form.appendChild(submitBtn);
1574
1817
  this.mainContent.appendChild(form);
@@ -2688,49 +2931,56 @@ body.uidex-inspecting * {
2688
2931
  color: var(--uidex-color-text-muted);
2689
2932
  }
2690
2933
 
2691
- .uidex-form-select {
2692
- appearance: none;
2693
- padding: 6px 10px;
2934
+ .uidex-form-select,
2935
+ .uidex-form-input,
2936
+ .uidex-form-textarea {
2694
2937
  border: 1px solid var(--uidex-color-border);
2695
2938
  border-radius: 0;
2696
2939
  background: transparent;
2697
2940
  color: var(--uidex-color-text);
2698
2941
  font-size: var(--uidex-font-size-sm);
2699
2942
  font-family: var(--uidex-font-mono);
2700
- cursor: pointer;
2701
2943
  outline: none;
2944
+ }
2945
+
2946
+ .uidex-form-select:focus,
2947
+ .uidex-form-input:focus,
2948
+ .uidex-form-textarea:focus {
2949
+ border-color: rgba(255, 255, 255, 0.3);
2950
+ }
2951
+
2952
+ .uidex-form-input::placeholder,
2953
+ .uidex-form-textarea::placeholder {
2954
+ color: var(--uidex-color-text-muted);
2955
+ }
2956
+
2957
+ .uidex-form-select {
2958
+ appearance: none;
2959
+ padding: 6px 10px;
2960
+ cursor: pointer;
2702
2961
  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
2962
  background-repeat: no-repeat;
2704
2963
  background-position: right 8px center;
2705
2964
  padding-right: 26px;
2706
2965
  }
2707
2966
 
2708
- .uidex-form-select:focus {
2709
- border-color: rgba(255, 255, 255, 0.3);
2967
+ .uidex-form-input {
2968
+ padding: 6px 10px;
2969
+ width: 100%;
2970
+ box-sizing: border-box;
2971
+ }
2972
+
2973
+ .uidex-form-input + .uidex-form-input {
2974
+ margin-top: 6px;
2710
2975
  }
2711
2976
 
2712
2977
  .uidex-form-textarea {
2713
2978
  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
2979
  line-height: 1.5;
2721
2980
  resize: vertical;
2722
- outline: none;
2723
2981
  min-height: 80px;
2724
2982
  }
2725
2983
 
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
2984
  .uidex-form-submit {
2735
2985
  padding: 6px 16px;
2736
2986
  border: 1px solid var(--uidex-color-border);
@@ -2749,7 +2999,7 @@ body.uidex-inspecting * {
2749
2999
  background: var(--uidex-color-primary-hover);
2750
3000
  }
2751
3001
 
2752
- .uidex-form-submit:active {
3002
+ .uidex-form-submit:not(:disabled):active {
2753
3003
  transform: translateY(1px);
2754
3004
  }
2755
3005
 
@@ -2893,8 +3143,17 @@ var UidexUI = class {
2893
3143
  copyTimer = null;
2894
3144
  currentPresentIds = [];
2895
3145
  activeMode = null;
3146
+ sessionId;
3147
+ capture = null;
2896
3148
  constructor(options = {}) {
2897
3149
  this.options = options;
3150
+ this.sessionId = generateSessionId();
3151
+ if (options.ingest?.captureConsole || options.ingest?.captureNetwork) {
3152
+ this.capture = new IngestCapture(
3153
+ options.ingest.captureConsole ?? false,
3154
+ options.ingest.captureNetwork ?? false
3155
+ );
3156
+ }
2898
3157
  this.overlay = new Overlay({
2899
3158
  color: options.config?.defaults?.color,
2900
3159
  borderStyle: options.config?.defaults?.borderStyle,
@@ -2911,7 +3170,12 @@ var UidexUI = class {
2911
3170
  }
2912
3171
  this.options.onSelect?.(id);
2913
3172
  },
2914
- elementGetter: (id) => this.findElement(id)
3173
+ elementGetter: (id) => this.findElement(id),
3174
+ ingest: options.ingest,
3175
+ onSubmit: options.onSubmit,
3176
+ sessionId: this.sessionId,
3177
+ getConsoleLogs: () => this.capture?.getConsoleLogs() ?? [],
3178
+ getNetworkErrors: () => this.capture?.getNetworkErrors() ?? []
2915
3179
  });
2916
3180
  this.menu = new Menu({
2917
3181
  onInspectToggle: () => this.toggleMode("inspect"),
@@ -2950,6 +3214,7 @@ var UidexUI = class {
2950
3214
  this.shadowHost.style.width = "0";
2951
3215
  this.shadowHost.style.height = "0";
2952
3216
  this.shadowHost.style.pointerEvents = "none";
3217
+ this.shadowHost.style.zIndex = "2147483646";
2953
3218
  this.shadowRoot = this.shadowHost.attachShadow({ mode: "open" });
2954
3219
  injectStyles(this.shadowRoot);
2955
3220
  const presentIds = this.scanPresentIds(componentIds);
@@ -2962,12 +3227,14 @@ var UidexUI = class {
2962
3227
  this.modal.setShadowRoot(this.shadowRoot);
2963
3228
  this.updateModalData(presentIds);
2964
3229
  this.inspector?.mount();
3230
+ this.capture?.start();
2965
3231
  this.startObserving();
2966
3232
  this.mounted = true;
2967
3233
  }
2968
3234
  destroy() {
2969
3235
  if (!this.mounted) return;
2970
3236
  this.stopObserving();
3237
+ this.capture?.stop();
2971
3238
  if (this.copyTimer !== null) {
2972
3239
  clearTimeout(this.copyTimer);
2973
3240
  this.copyTimer = null;
@@ -3153,6 +3420,7 @@ function createUidexUI(options = {}) {
3153
3420
  return new UidexUI(options);
3154
3421
  }
3155
3422
  export {
3423
+ IngestCapture,
3156
3424
  Inspector,
3157
3425
  Menu,
3158
3426
  Modal,
@@ -3160,6 +3428,7 @@ export {
3160
3428
  UidexUI,
3161
3429
  classNames,
3162
3430
  createUidexUI,
3431
+ generateSessionId,
3163
3432
  getComponents,
3164
3433
  getContrastColor,
3165
3434
  getFeatures,
@@ -3169,6 +3438,7 @@ export {
3169
3438
  registerComponents,
3170
3439
  registerFeatures,
3171
3440
  registerPages,
3172
- resolveColor
3441
+ resolveColor,
3442
+ submitFeedback
3173
3443
  };
3174
3444
  //# sourceMappingURL=index.js.map