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.
@@ -20,6 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/core/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ IngestCapture: () => IngestCapture,
23
24
  Inspector: () => Inspector,
24
25
  Menu: () => Menu,
25
26
  Modal: () => Modal,
@@ -27,6 +28,7 @@ __export(index_exports, {
27
28
  UidexUI: () => UidexUI,
28
29
  classNames: () => classNames,
29
30
  createUidexUI: () => createUidexUI,
31
+ generateSessionId: () => generateSessionId,
30
32
  getComponents: () => getComponents,
31
33
  getContrastColor: () => getContrastColor,
32
34
  getFeatures: () => getFeatures,
@@ -36,7 +38,8 @@ __export(index_exports, {
36
38
  registerComponents: () => registerComponents,
37
39
  registerFeatures: () => registerFeatures,
38
40
  registerPages: () => registerPages,
39
- resolveColor: () => resolveColor
41
+ resolveColor: () => resolveColor,
42
+ submitFeedback: () => submitFeedback
40
43
  });
41
44
  module.exports = __toCommonJS(index_exports);
42
45
 
@@ -274,7 +277,7 @@ function resolveColor(color, colorMap) {
274
277
  }
275
278
 
276
279
  // src/core/overlay.ts
277
- var DEFAULT_COLOR = "#e5e5e5";
280
+ var DEFAULT_COLOR = "#3b82f6";
278
281
  var DEFAULT_BORDER_STYLE = "solid";
279
282
  var DEFAULT_BORDER_WIDTH = 2;
280
283
  var DEFAULT_LABEL_POSITION = "top-left";
@@ -425,6 +428,23 @@ var Overlay = class {
425
428
  this.element.style.left = `${rect.left}px`;
426
429
  this.element.style.width = `${rect.width}px`;
427
430
  this.element.style.height = `${rect.height}px`;
431
+ this.clampLabel(rect);
432
+ }
433
+ /** Move the label inside the overlay when there is no room outside. */
434
+ clampLabel(rect) {
435
+ if (!this.labelElement || this.labelElement.style.display === "none") return;
436
+ const {
437
+ labelPosition = DEFAULT_LABEL_POSITION
438
+ } = this.options;
439
+ const isTop = labelPosition === "top-left" || labelPosition === "top-right";
440
+ const labelHeight = 20;
441
+ if (isTop && rect.top < labelHeight) {
442
+ this.labelElement.style.top = "4px";
443
+ this.labelElement.style.transform = "";
444
+ } else if (!isTop && window.innerHeight - rect.bottom < labelHeight) {
445
+ this.labelElement.style.bottom = "4px";
446
+ this.labelElement.style.transform = "";
447
+ }
428
448
  }
429
449
  addListeners() {
430
450
  window.addEventListener("resize", this.boundUpdatePosition);
@@ -565,6 +585,154 @@ var Inspector = class {
565
585
  }
566
586
  };
567
587
 
588
+ // src/core/ingest.ts
589
+ var MAX_CONSOLE_LOGS = 50;
590
+ var MAX_NETWORK_ERRORS = 20;
591
+ var nativeFetch = null;
592
+ function safeStringify(value) {
593
+ if (typeof value === "string") return value;
594
+ try {
595
+ return JSON.stringify(value);
596
+ } catch {
597
+ return String(value);
598
+ }
599
+ }
600
+ var IngestCapture = class {
601
+ constructor(captureConsole, captureNetwork) {
602
+ this.captureConsole = captureConsole;
603
+ this.captureNetwork = captureNetwork;
604
+ }
605
+ consoleLogs = [];
606
+ networkErrors = [];
607
+ originalConsoleWarn = null;
608
+ originalConsoleError = null;
609
+ originalFetch = null;
610
+ start() {
611
+ this.consoleLogs = [];
612
+ this.networkErrors = [];
613
+ if (this.captureConsole) this.interceptConsole();
614
+ if (this.captureNetwork) this.interceptNetwork();
615
+ }
616
+ stop() {
617
+ this.restoreConsole();
618
+ this.restoreNetwork();
619
+ }
620
+ getConsoleLogs() {
621
+ return [...this.consoleLogs];
622
+ }
623
+ getNetworkErrors() {
624
+ return [...this.networkErrors];
625
+ }
626
+ interceptConsole() {
627
+ if (this.originalConsoleWarn) return;
628
+ this.originalConsoleWarn = console.warn;
629
+ this.originalConsoleError = console.error;
630
+ console.warn = (...args) => {
631
+ this.addConsoleLog("warn", args);
632
+ this.originalConsoleWarn.apply(console, args);
633
+ };
634
+ console.error = (...args) => {
635
+ this.addConsoleLog("error", args);
636
+ this.originalConsoleError.apply(console, args);
637
+ };
638
+ }
639
+ addConsoleLog(level, args) {
640
+ this.consoleLogs.push({
641
+ level,
642
+ message: args.map(safeStringify).join(" "),
643
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
644
+ });
645
+ if (this.consoleLogs.length > MAX_CONSOLE_LOGS) {
646
+ this.consoleLogs.shift();
647
+ }
648
+ }
649
+ restoreConsole() {
650
+ if (this.originalConsoleWarn) {
651
+ console.warn = this.originalConsoleWarn;
652
+ this.originalConsoleWarn = null;
653
+ }
654
+ if (this.originalConsoleError) {
655
+ console.error = this.originalConsoleError;
656
+ this.originalConsoleError = null;
657
+ }
658
+ }
659
+ interceptNetwork() {
660
+ if (this.originalFetch) return;
661
+ this.originalFetch = window.fetch;
662
+ if (!nativeFetch) nativeFetch = this.originalFetch;
663
+ window.fetch = async (...args) => {
664
+ try {
665
+ const response = await this.originalFetch.apply(window, args);
666
+ if (!response.ok) {
667
+ this.addNetworkError(args[0], args[1]?.method, response.status, response.statusText);
668
+ }
669
+ return response;
670
+ } catch (error) {
671
+ this.addNetworkError(
672
+ args[0],
673
+ args[1]?.method,
674
+ null,
675
+ error instanceof Error ? error.message : "Network error"
676
+ );
677
+ throw error;
678
+ }
679
+ };
680
+ }
681
+ addNetworkError(input, method, status, statusText) {
682
+ let url;
683
+ if (typeof input === "string") {
684
+ url = input;
685
+ } else if (input instanceof URL) {
686
+ url = input.href;
687
+ } else {
688
+ url = input.url;
689
+ method ??= input.method;
690
+ }
691
+ this.networkErrors.push({
692
+ url,
693
+ method: method ?? "GET",
694
+ status,
695
+ statusText,
696
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
697
+ });
698
+ if (this.networkErrors.length > MAX_NETWORK_ERRORS) {
699
+ this.networkErrors.shift();
700
+ }
701
+ }
702
+ restoreNetwork() {
703
+ if (this.originalFetch) {
704
+ window.fetch = this.originalFetch;
705
+ this.originalFetch = null;
706
+ }
707
+ }
708
+ };
709
+ function generateSessionId() {
710
+ if (typeof crypto !== "undefined" && crypto.randomUUID) {
711
+ return crypto.randomUUID();
712
+ }
713
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
714
+ const r = Math.random() * 16 | 0;
715
+ const v = c === "x" ? r : r & 3 | 8;
716
+ return v.toString(16);
717
+ });
718
+ }
719
+ async function submitFeedback(endpoint, apiKey, report) {
720
+ const fetchFn = nativeFetch ?? fetch;
721
+ const response = await fetchFn(endpoint, {
722
+ method: "POST",
723
+ headers: {
724
+ "Content-Type": "application/json",
725
+ Authorization: `Bearer ${apiKey}`
726
+ },
727
+ body: JSON.stringify(report)
728
+ });
729
+ if (!response.ok) {
730
+ const text = await response.text().catch(() => "");
731
+ throw new Error(`Ingest failed (${response.status}): ${text}`);
732
+ }
733
+ return response.json();
734
+ }
735
+
568
736
  // src/core/modal.ts
569
737
  var Modal = class _Modal {
570
738
  backdrop = null;
@@ -1547,6 +1715,14 @@ var Modal = class _Modal {
1547
1715
  "medium"
1548
1716
  );
1549
1717
  form.appendChild(severitySelect.group);
1718
+ const titleGroup = this.createFormGroup("Title");
1719
+ const titleInput = document.createElement("input");
1720
+ titleInput.type = "text";
1721
+ titleInput.className = "uidex-form-input";
1722
+ titleInput.placeholder = "Brief summary (optional)";
1723
+ titleInput.maxLength = 200;
1724
+ titleGroup.appendChild(titleInput);
1725
+ form.appendChild(titleGroup);
1550
1726
  const descGroup = this.createFormGroup("Description");
1551
1727
  const textarea = document.createElement("textarea");
1552
1728
  textarea.className = "uidex-form-textarea";
@@ -1554,6 +1730,22 @@ var Modal = class _Modal {
1554
1730
  textarea.rows = 4;
1555
1731
  descGroup.appendChild(textarea);
1556
1732
  form.appendChild(descGroup);
1733
+ let emailInput;
1734
+ let nameInput;
1735
+ if (this.options.ingest && !this.options.ingest.reporter) {
1736
+ const reporterGroup = this.createFormGroup("Reporter");
1737
+ nameInput = document.createElement("input");
1738
+ nameInput.type = "text";
1739
+ nameInput.className = "uidex-form-input";
1740
+ nameInput.placeholder = "Name (optional)";
1741
+ reporterGroup.appendChild(nameInput);
1742
+ emailInput = document.createElement("input");
1743
+ emailInput.type = "email";
1744
+ emailInput.className = "uidex-form-input";
1745
+ emailInput.placeholder = "Email (optional)";
1746
+ reporterGroup.appendChild(emailInput);
1747
+ form.appendChild(reporterGroup);
1748
+ }
1557
1749
  const screenshotGroup = document.createElement("div");
1558
1750
  screenshotGroup.className = "uidex-form-group";
1559
1751
  const screenshotLabel = document.createElement("label");
@@ -1589,9 +1781,14 @@ var Modal = class _Modal {
1589
1781
  }
1590
1782
  const env = this.collectEnv();
1591
1783
  const page = this.data.pages.find((p) => p.componentIds.includes(id));
1784
+ const { ingest } = this.options;
1785
+ const reporterEmail = ingest?.reporter?.email || emailInput?.value.trim() || void 0;
1786
+ const reporterName = ingest?.reporter?.name || nameInput?.value.trim() || void 0;
1787
+ const titleValue = titleInput.value.trim();
1592
1788
  const report = {
1593
1789
  type: typeSelect.select.value,
1594
1790
  severity: severitySelect.select.value,
1791
+ ...titleValue ? { title: titleValue } : {},
1595
1792
  description: textarea.value.trim(),
1596
1793
  componentId: id,
1597
1794
  element: element ? this.describeElement(element) : null,
@@ -1600,17 +1797,66 @@ var Modal = class _Modal {
1600
1797
  path: window.location.pathname,
1601
1798
  route: page?.dir ?? null,
1602
1799
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1800
+ pageTitle: document.title,
1801
+ locale: navigator.language,
1802
+ sessionId: this.options.sessionId ?? "",
1603
1803
  viewport: env.viewport,
1604
1804
  screen: env.screen,
1605
1805
  userAgent: env.userAgent,
1606
- screenshot
1806
+ screenshot,
1807
+ ...reporterEmail ? { reporterEmail } : {},
1808
+ ...reporterName ? { reporterName } : {},
1809
+ ...ingest?.environment ? { environment: ingest.environment } : {},
1810
+ ...ingest?.appVersion ? { appVersion: ingest.appVersion } : {},
1811
+ ...ingest?.metadata ? { metadata: ingest.metadata } : {}
1607
1812
  };
1608
- console.log("[uidex] Feedback submitted:", report);
1609
- submitBtn.textContent = "Submitted!";
1610
- setTimeout(() => {
1611
- submitBtn.textContent = "Submit";
1612
- submitBtn.disabled = false;
1613
- }, 1500);
1813
+ const consoleLogs = this.options.getConsoleLogs?.();
1814
+ if (consoleLogs && consoleLogs.length > 0) {
1815
+ report.consoleLogs = consoleLogs;
1816
+ }
1817
+ const networkErrors = this.options.getNetworkErrors?.();
1818
+ if (networkErrors && networkErrors.length > 0) {
1819
+ report.networkErrors = networkErrors;
1820
+ }
1821
+ const showSuccess = (autoClose) => {
1822
+ submitBtn.textContent = "Submitted!";
1823
+ if (autoClose) {
1824
+ setTimeout(() => this.hide(), 1500);
1825
+ } else {
1826
+ setTimeout(() => {
1827
+ submitBtn.textContent = "Submit";
1828
+ submitBtn.disabled = false;
1829
+ }, 1500);
1830
+ }
1831
+ };
1832
+ if (ingest) {
1833
+ submitBtn.textContent = "Submitting\u2026";
1834
+ try {
1835
+ const serverResult = await submitFeedback(
1836
+ ingest.endpoint,
1837
+ ingest.apiKey,
1838
+ report
1839
+ );
1840
+ this.options.onSubmit?.(report, {
1841
+ ok: true,
1842
+ id: serverResult.id,
1843
+ sequenceNumber: serverResult.sequenceNumber
1844
+ });
1845
+ showSuccess(true);
1846
+ } catch (err) {
1847
+ const errorMessage = err instanceof Error ? err.message : "Unknown error";
1848
+ console.warn("[uidex] Feedback submission failed:", errorMessage);
1849
+ this.options.onSubmit?.(report, { ok: false, error: errorMessage });
1850
+ submitBtn.textContent = "Failed \u2014 retry?";
1851
+ submitBtn.disabled = false;
1852
+ }
1853
+ } else {
1854
+ if (!this.options.onSubmit) {
1855
+ console.log("[uidex] Feedback submitted:", report);
1856
+ }
1857
+ this.options.onSubmit?.(report, { ok: true, id: "", sequenceNumber: 0 });
1858
+ showSuccess(false);
1859
+ }
1614
1860
  });
1615
1861
  form.appendChild(submitBtn);
1616
1862
  this.mainContent.appendChild(form);
@@ -2730,49 +2976,56 @@ body.uidex-inspecting * {
2730
2976
  color: var(--uidex-color-text-muted);
2731
2977
  }
2732
2978
 
2733
- .uidex-form-select {
2734
- appearance: none;
2735
- padding: 6px 10px;
2979
+ .uidex-form-select,
2980
+ .uidex-form-input,
2981
+ .uidex-form-textarea {
2736
2982
  border: 1px solid var(--uidex-color-border);
2737
2983
  border-radius: 0;
2738
2984
  background: transparent;
2739
2985
  color: var(--uidex-color-text);
2740
2986
  font-size: var(--uidex-font-size-sm);
2741
2987
  font-family: var(--uidex-font-mono);
2742
- cursor: pointer;
2743
2988
  outline: none;
2989
+ }
2990
+
2991
+ .uidex-form-select:focus,
2992
+ .uidex-form-input:focus,
2993
+ .uidex-form-textarea:focus {
2994
+ border-color: rgba(255, 255, 255, 0.3);
2995
+ }
2996
+
2997
+ .uidex-form-input::placeholder,
2998
+ .uidex-form-textarea::placeholder {
2999
+ color: var(--uidex-color-text-muted);
3000
+ }
3001
+
3002
+ .uidex-form-select {
3003
+ appearance: none;
3004
+ padding: 6px 10px;
3005
+ cursor: pointer;
2744
3006
  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");
2745
3007
  background-repeat: no-repeat;
2746
3008
  background-position: right 8px center;
2747
3009
  padding-right: 26px;
2748
3010
  }
2749
3011
 
2750
- .uidex-form-select:focus {
2751
- border-color: rgba(255, 255, 255, 0.3);
3012
+ .uidex-form-input {
3013
+ padding: 6px 10px;
3014
+ width: 100%;
3015
+ box-sizing: border-box;
3016
+ }
3017
+
3018
+ .uidex-form-input + .uidex-form-input {
3019
+ margin-top: 6px;
2752
3020
  }
2753
3021
 
2754
3022
  .uidex-form-textarea {
2755
3023
  padding: 8px 10px;
2756
- border: 1px solid var(--uidex-color-border);
2757
- border-radius: 0;
2758
- background: transparent;
2759
- color: var(--uidex-color-text);
2760
- font-size: var(--uidex-font-size-sm);
2761
- font-family: var(--uidex-font-mono);
2762
3024
  line-height: 1.5;
2763
3025
  resize: vertical;
2764
- outline: none;
2765
3026
  min-height: 80px;
2766
3027
  }
2767
3028
 
2768
- .uidex-form-textarea::placeholder {
2769
- color: var(--uidex-color-text-muted);
2770
- }
2771
-
2772
- .uidex-form-textarea:focus {
2773
- border-color: rgba(255, 255, 255, 0.3);
2774
- }
2775
-
2776
3029
  .uidex-form-submit {
2777
3030
  padding: 6px 16px;
2778
3031
  border: 1px solid var(--uidex-color-border);
@@ -2791,7 +3044,7 @@ body.uidex-inspecting * {
2791
3044
  background: var(--uidex-color-primary-hover);
2792
3045
  }
2793
3046
 
2794
- .uidex-form-submit:active {
3047
+ .uidex-form-submit:not(:disabled):active {
2795
3048
  transform: translateY(1px);
2796
3049
  }
2797
3050
 
@@ -2935,8 +3188,17 @@ var UidexUI = class {
2935
3188
  copyTimer = null;
2936
3189
  currentPresentIds = [];
2937
3190
  activeMode = null;
3191
+ sessionId;
3192
+ capture = null;
2938
3193
  constructor(options = {}) {
2939
3194
  this.options = options;
3195
+ this.sessionId = generateSessionId();
3196
+ if (options.ingest?.captureConsole || options.ingest?.captureNetwork) {
3197
+ this.capture = new IngestCapture(
3198
+ options.ingest.captureConsole ?? false,
3199
+ options.ingest.captureNetwork ?? false
3200
+ );
3201
+ }
2940
3202
  this.overlay = new Overlay({
2941
3203
  color: options.config?.defaults?.color,
2942
3204
  borderStyle: options.config?.defaults?.borderStyle,
@@ -2953,7 +3215,12 @@ var UidexUI = class {
2953
3215
  }
2954
3216
  this.options.onSelect?.(id);
2955
3217
  },
2956
- elementGetter: (id) => this.findElement(id)
3218
+ elementGetter: (id) => this.findElement(id),
3219
+ ingest: options.ingest,
3220
+ onSubmit: options.onSubmit,
3221
+ sessionId: this.sessionId,
3222
+ getConsoleLogs: () => this.capture?.getConsoleLogs() ?? [],
3223
+ getNetworkErrors: () => this.capture?.getNetworkErrors() ?? []
2957
3224
  });
2958
3225
  this.menu = new Menu({
2959
3226
  onInspectToggle: () => this.toggleMode("inspect"),
@@ -2992,6 +3259,7 @@ var UidexUI = class {
2992
3259
  this.shadowHost.style.width = "0";
2993
3260
  this.shadowHost.style.height = "0";
2994
3261
  this.shadowHost.style.pointerEvents = "none";
3262
+ this.shadowHost.style.zIndex = "2147483646";
2995
3263
  this.shadowRoot = this.shadowHost.attachShadow({ mode: "open" });
2996
3264
  injectStyles(this.shadowRoot);
2997
3265
  const presentIds = this.scanPresentIds(componentIds);
@@ -3004,12 +3272,14 @@ var UidexUI = class {
3004
3272
  this.modal.setShadowRoot(this.shadowRoot);
3005
3273
  this.updateModalData(presentIds);
3006
3274
  this.inspector?.mount();
3275
+ this.capture?.start();
3007
3276
  this.startObserving();
3008
3277
  this.mounted = true;
3009
3278
  }
3010
3279
  destroy() {
3011
3280
  if (!this.mounted) return;
3012
3281
  this.stopObserving();
3282
+ this.capture?.stop();
3013
3283
  if (this.copyTimer !== null) {
3014
3284
  clearTimeout(this.copyTimer);
3015
3285
  this.copyTimer = null;
@@ -3196,6 +3466,7 @@ function createUidexUI(options = {}) {
3196
3466
  }
3197
3467
  // Annotate the CommonJS export names for ESM import in node:
3198
3468
  0 && (module.exports = {
3469
+ IngestCapture,
3199
3470
  Inspector,
3200
3471
  Menu,
3201
3472
  Modal,
@@ -3203,6 +3474,7 @@ function createUidexUI(options = {}) {
3203
3474
  UidexUI,
3204
3475
  classNames,
3205
3476
  createUidexUI,
3477
+ generateSessionId,
3206
3478
  getComponents,
3207
3479
  getContrastColor,
3208
3480
  getFeatures,
@@ -3212,6 +3484,7 @@ function createUidexUI(options = {}) {
3212
3484
  registerComponents,
3213
3485
  registerFeatures,
3214
3486
  registerPages,
3215
- resolveColor
3487
+ resolveColor,
3488
+ submitFeedback
3216
3489
  });
3217
3490
  //# sourceMappingURL=index.cjs.map