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.
@@ -782,49 +782,56 @@ body.uidex-inspecting * {
782
782
  color: var(--uidex-color-text-muted);
783
783
  }
784
784
 
785
- .uidex-form-select {
786
- appearance: none;
787
- padding: 6px 10px;
785
+ .uidex-form-select,
786
+ .uidex-form-input,
787
+ .uidex-form-textarea {
788
788
  border: 1px solid var(--uidex-color-border);
789
789
  border-radius: 0;
790
790
  background: transparent;
791
791
  color: var(--uidex-color-text);
792
792
  font-size: var(--uidex-font-size-sm);
793
793
  font-family: var(--uidex-font-mono);
794
- cursor: pointer;
795
794
  outline: none;
795
+ }
796
+
797
+ .uidex-form-select:focus,
798
+ .uidex-form-input:focus,
799
+ .uidex-form-textarea:focus {
800
+ border-color: rgba(255, 255, 255, 0.3);
801
+ }
802
+
803
+ .uidex-form-input::placeholder,
804
+ .uidex-form-textarea::placeholder {
805
+ color: var(--uidex-color-text-muted);
806
+ }
807
+
808
+ .uidex-form-select {
809
+ appearance: none;
810
+ padding: 6px 10px;
811
+ cursor: pointer;
796
812
  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");
797
813
  background-repeat: no-repeat;
798
814
  background-position: right 8px center;
799
815
  padding-right: 26px;
800
816
  }
801
817
 
802
- .uidex-form-select:focus {
803
- border-color: rgba(255, 255, 255, 0.3);
818
+ .uidex-form-input {
819
+ padding: 6px 10px;
820
+ width: 100%;
821
+ box-sizing: border-box;
822
+ }
823
+
824
+ .uidex-form-input + .uidex-form-input {
825
+ margin-top: 6px;
804
826
  }
805
827
 
806
828
  .uidex-form-textarea {
807
829
  padding: 8px 10px;
808
- border: 1px solid var(--uidex-color-border);
809
- border-radius: 0;
810
- background: transparent;
811
- color: var(--uidex-color-text);
812
- font-size: var(--uidex-font-size-sm);
813
- font-family: var(--uidex-font-mono);
814
830
  line-height: 1.5;
815
831
  resize: vertical;
816
- outline: none;
817
832
  min-height: 80px;
818
833
  }
819
834
 
820
- .uidex-form-textarea::placeholder {
821
- color: var(--uidex-color-text-muted);
822
- }
823
-
824
- .uidex-form-textarea:focus {
825
- border-color: rgba(255, 255, 255, 0.3);
826
- }
827
-
828
835
  .uidex-form-submit {
829
836
  padding: 6px 16px;
830
837
  border: 1px solid var(--uidex-color-border);
@@ -843,7 +850,7 @@ body.uidex-inspecting * {
843
850
  background: var(--uidex-color-primary-hover);
844
851
  }
845
852
 
846
- .uidex-form-submit:active {
853
+ .uidex-form-submit:not(:disabled):active {
847
854
  transform: translateY(1px);
848
855
  }
849
856
 
package/dist/index.cjs CHANGED
@@ -564,6 +564,154 @@ var Inspector = class {
564
564
  }
565
565
  };
566
566
 
567
+ // src/core/ingest.ts
568
+ var MAX_CONSOLE_LOGS = 50;
569
+ var MAX_NETWORK_ERRORS = 20;
570
+ var nativeFetch = null;
571
+ function safeStringify(value) {
572
+ if (typeof value === "string") return value;
573
+ try {
574
+ return JSON.stringify(value);
575
+ } catch {
576
+ return String(value);
577
+ }
578
+ }
579
+ var IngestCapture = class {
580
+ constructor(captureConsole, captureNetwork) {
581
+ this.captureConsole = captureConsole;
582
+ this.captureNetwork = captureNetwork;
583
+ }
584
+ consoleLogs = [];
585
+ networkErrors = [];
586
+ originalConsoleWarn = null;
587
+ originalConsoleError = null;
588
+ originalFetch = null;
589
+ start() {
590
+ this.consoleLogs = [];
591
+ this.networkErrors = [];
592
+ if (this.captureConsole) this.interceptConsole();
593
+ if (this.captureNetwork) this.interceptNetwork();
594
+ }
595
+ stop() {
596
+ this.restoreConsole();
597
+ this.restoreNetwork();
598
+ }
599
+ getConsoleLogs() {
600
+ return [...this.consoleLogs];
601
+ }
602
+ getNetworkErrors() {
603
+ return [...this.networkErrors];
604
+ }
605
+ interceptConsole() {
606
+ if (this.originalConsoleWarn) return;
607
+ this.originalConsoleWarn = console.warn;
608
+ this.originalConsoleError = console.error;
609
+ console.warn = (...args) => {
610
+ this.addConsoleLog("warn", args);
611
+ this.originalConsoleWarn.apply(console, args);
612
+ };
613
+ console.error = (...args) => {
614
+ this.addConsoleLog("error", args);
615
+ this.originalConsoleError.apply(console, args);
616
+ };
617
+ }
618
+ addConsoleLog(level, args) {
619
+ this.consoleLogs.push({
620
+ level,
621
+ message: args.map(safeStringify).join(" "),
622
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
623
+ });
624
+ if (this.consoleLogs.length > MAX_CONSOLE_LOGS) {
625
+ this.consoleLogs.shift();
626
+ }
627
+ }
628
+ restoreConsole() {
629
+ if (this.originalConsoleWarn) {
630
+ console.warn = this.originalConsoleWarn;
631
+ this.originalConsoleWarn = null;
632
+ }
633
+ if (this.originalConsoleError) {
634
+ console.error = this.originalConsoleError;
635
+ this.originalConsoleError = null;
636
+ }
637
+ }
638
+ interceptNetwork() {
639
+ if (this.originalFetch) return;
640
+ this.originalFetch = window.fetch;
641
+ if (!nativeFetch) nativeFetch = this.originalFetch;
642
+ window.fetch = async (...args) => {
643
+ try {
644
+ const response = await this.originalFetch.apply(window, args);
645
+ if (!response.ok) {
646
+ this.addNetworkError(args[0], args[1]?.method, response.status, response.statusText);
647
+ }
648
+ return response;
649
+ } catch (error) {
650
+ this.addNetworkError(
651
+ args[0],
652
+ args[1]?.method,
653
+ null,
654
+ error instanceof Error ? error.message : "Network error"
655
+ );
656
+ throw error;
657
+ }
658
+ };
659
+ }
660
+ addNetworkError(input, method, status, statusText) {
661
+ let url;
662
+ if (typeof input === "string") {
663
+ url = input;
664
+ } else if (input instanceof URL) {
665
+ url = input.href;
666
+ } else {
667
+ url = input.url;
668
+ method ??= input.method;
669
+ }
670
+ this.networkErrors.push({
671
+ url,
672
+ method: method ?? "GET",
673
+ status,
674
+ statusText,
675
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
676
+ });
677
+ if (this.networkErrors.length > MAX_NETWORK_ERRORS) {
678
+ this.networkErrors.shift();
679
+ }
680
+ }
681
+ restoreNetwork() {
682
+ if (this.originalFetch) {
683
+ window.fetch = this.originalFetch;
684
+ this.originalFetch = null;
685
+ }
686
+ }
687
+ };
688
+ function generateSessionId() {
689
+ if (typeof crypto !== "undefined" && crypto.randomUUID) {
690
+ return crypto.randomUUID();
691
+ }
692
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
693
+ const r = Math.random() * 16 | 0;
694
+ const v = c === "x" ? r : r & 3 | 8;
695
+ return v.toString(16);
696
+ });
697
+ }
698
+ async function submitFeedback(endpoint, apiKey, report) {
699
+ const fetchFn = nativeFetch ?? fetch;
700
+ const response = await fetchFn(endpoint, {
701
+ method: "POST",
702
+ headers: {
703
+ "Content-Type": "application/json",
704
+ Authorization: `Bearer ${apiKey}`
705
+ },
706
+ body: JSON.stringify(report)
707
+ });
708
+ if (!response.ok) {
709
+ const text = await response.text().catch(() => "");
710
+ throw new Error(`Ingest failed (${response.status}): ${text}`);
711
+ }
712
+ return response.json();
713
+ }
714
+
567
715
  // src/core/modal.ts
568
716
  var Modal = class _Modal {
569
717
  backdrop = null;
@@ -1546,6 +1694,14 @@ var Modal = class _Modal {
1546
1694
  "medium"
1547
1695
  );
1548
1696
  form.appendChild(severitySelect.group);
1697
+ const titleGroup = this.createFormGroup("Title");
1698
+ const titleInput = document.createElement("input");
1699
+ titleInput.type = "text";
1700
+ titleInput.className = "uidex-form-input";
1701
+ titleInput.placeholder = "Brief summary (optional)";
1702
+ titleInput.maxLength = 200;
1703
+ titleGroup.appendChild(titleInput);
1704
+ form.appendChild(titleGroup);
1549
1705
  const descGroup = this.createFormGroup("Description");
1550
1706
  const textarea = document.createElement("textarea");
1551
1707
  textarea.className = "uidex-form-textarea";
@@ -1553,6 +1709,22 @@ var Modal = class _Modal {
1553
1709
  textarea.rows = 4;
1554
1710
  descGroup.appendChild(textarea);
1555
1711
  form.appendChild(descGroup);
1712
+ let emailInput;
1713
+ let nameInput;
1714
+ if (this.options.ingest && !this.options.ingest.reporter) {
1715
+ const reporterGroup = this.createFormGroup("Reporter");
1716
+ nameInput = document.createElement("input");
1717
+ nameInput.type = "text";
1718
+ nameInput.className = "uidex-form-input";
1719
+ nameInput.placeholder = "Name (optional)";
1720
+ reporterGroup.appendChild(nameInput);
1721
+ emailInput = document.createElement("input");
1722
+ emailInput.type = "email";
1723
+ emailInput.className = "uidex-form-input";
1724
+ emailInput.placeholder = "Email (optional)";
1725
+ reporterGroup.appendChild(emailInput);
1726
+ form.appendChild(reporterGroup);
1727
+ }
1556
1728
  const screenshotGroup = document.createElement("div");
1557
1729
  screenshotGroup.className = "uidex-form-group";
1558
1730
  const screenshotLabel = document.createElement("label");
@@ -1588,9 +1760,14 @@ var Modal = class _Modal {
1588
1760
  }
1589
1761
  const env = this.collectEnv();
1590
1762
  const page = this.data.pages.find((p) => p.componentIds.includes(id));
1763
+ const { ingest } = this.options;
1764
+ const reporterEmail = ingest?.reporter?.email || emailInput?.value.trim() || void 0;
1765
+ const reporterName = ingest?.reporter?.name || nameInput?.value.trim() || void 0;
1766
+ const titleValue = titleInput.value.trim();
1591
1767
  const report = {
1592
1768
  type: typeSelect.select.value,
1593
1769
  severity: severitySelect.select.value,
1770
+ ...titleValue ? { title: titleValue } : {},
1594
1771
  description: textarea.value.trim(),
1595
1772
  componentId: id,
1596
1773
  element: element ? this.describeElement(element) : null,
@@ -1599,17 +1776,66 @@ var Modal = class _Modal {
1599
1776
  path: window.location.pathname,
1600
1777
  route: page?.dir ?? null,
1601
1778
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1779
+ pageTitle: document.title,
1780
+ locale: navigator.language,
1781
+ sessionId: this.options.sessionId ?? "",
1602
1782
  viewport: env.viewport,
1603
1783
  screen: env.screen,
1604
1784
  userAgent: env.userAgent,
1605
- screenshot
1785
+ screenshot,
1786
+ ...reporterEmail ? { reporterEmail } : {},
1787
+ ...reporterName ? { reporterName } : {},
1788
+ ...ingest?.environment ? { environment: ingest.environment } : {},
1789
+ ...ingest?.appVersion ? { appVersion: ingest.appVersion } : {},
1790
+ ...ingest?.metadata ? { metadata: ingest.metadata } : {}
1606
1791
  };
1607
- console.log("[uidex] Feedback submitted:", report);
1608
- submitBtn.textContent = "Submitted!";
1609
- setTimeout(() => {
1610
- submitBtn.textContent = "Submit";
1611
- submitBtn.disabled = false;
1612
- }, 1500);
1792
+ const consoleLogs = this.options.getConsoleLogs?.();
1793
+ if (consoleLogs && consoleLogs.length > 0) {
1794
+ report.consoleLogs = consoleLogs;
1795
+ }
1796
+ const networkErrors = this.options.getNetworkErrors?.();
1797
+ if (networkErrors && networkErrors.length > 0) {
1798
+ report.networkErrors = networkErrors;
1799
+ }
1800
+ const showSuccess = (autoClose) => {
1801
+ submitBtn.textContent = "Submitted!";
1802
+ if (autoClose) {
1803
+ setTimeout(() => this.hide(), 1500);
1804
+ } else {
1805
+ setTimeout(() => {
1806
+ submitBtn.textContent = "Submit";
1807
+ submitBtn.disabled = false;
1808
+ }, 1500);
1809
+ }
1810
+ };
1811
+ if (ingest) {
1812
+ submitBtn.textContent = "Submitting\u2026";
1813
+ try {
1814
+ const serverResult = await submitFeedback(
1815
+ ingest.endpoint,
1816
+ ingest.apiKey,
1817
+ report
1818
+ );
1819
+ this.options.onSubmit?.(report, {
1820
+ ok: true,
1821
+ id: serverResult.id,
1822
+ sequenceNumber: serverResult.sequenceNumber
1823
+ });
1824
+ showSuccess(true);
1825
+ } catch (err) {
1826
+ const errorMessage = err instanceof Error ? err.message : "Unknown error";
1827
+ console.warn("[uidex] Feedback submission failed:", errorMessage);
1828
+ this.options.onSubmit?.(report, { ok: false, error: errorMessage });
1829
+ submitBtn.textContent = "Failed \u2014 retry?";
1830
+ submitBtn.disabled = false;
1831
+ }
1832
+ } else {
1833
+ if (!this.options.onSubmit) {
1834
+ console.log("[uidex] Feedback submitted:", report);
1835
+ }
1836
+ this.options.onSubmit?.(report, { ok: true, id: "", sequenceNumber: 0 });
1837
+ showSuccess(false);
1838
+ }
1613
1839
  });
1614
1840
  form.appendChild(submitBtn);
1615
1841
  this.mainContent.appendChild(form);
@@ -2729,49 +2955,56 @@ body.uidex-inspecting * {
2729
2955
  color: var(--uidex-color-text-muted);
2730
2956
  }
2731
2957
 
2732
- .uidex-form-select {
2733
- appearance: none;
2734
- padding: 6px 10px;
2958
+ .uidex-form-select,
2959
+ .uidex-form-input,
2960
+ .uidex-form-textarea {
2735
2961
  border: 1px solid var(--uidex-color-border);
2736
2962
  border-radius: 0;
2737
2963
  background: transparent;
2738
2964
  color: var(--uidex-color-text);
2739
2965
  font-size: var(--uidex-font-size-sm);
2740
2966
  font-family: var(--uidex-font-mono);
2741
- cursor: pointer;
2742
2967
  outline: none;
2968
+ }
2969
+
2970
+ .uidex-form-select:focus,
2971
+ .uidex-form-input:focus,
2972
+ .uidex-form-textarea:focus {
2973
+ border-color: rgba(255, 255, 255, 0.3);
2974
+ }
2975
+
2976
+ .uidex-form-input::placeholder,
2977
+ .uidex-form-textarea::placeholder {
2978
+ color: var(--uidex-color-text-muted);
2979
+ }
2980
+
2981
+ .uidex-form-select {
2982
+ appearance: none;
2983
+ padding: 6px 10px;
2984
+ cursor: pointer;
2743
2985
  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");
2744
2986
  background-repeat: no-repeat;
2745
2987
  background-position: right 8px center;
2746
2988
  padding-right: 26px;
2747
2989
  }
2748
2990
 
2749
- .uidex-form-select:focus {
2750
- border-color: rgba(255, 255, 255, 0.3);
2991
+ .uidex-form-input {
2992
+ padding: 6px 10px;
2993
+ width: 100%;
2994
+ box-sizing: border-box;
2995
+ }
2996
+
2997
+ .uidex-form-input + .uidex-form-input {
2998
+ margin-top: 6px;
2751
2999
  }
2752
3000
 
2753
3001
  .uidex-form-textarea {
2754
3002
  padding: 8px 10px;
2755
- border: 1px solid var(--uidex-color-border);
2756
- border-radius: 0;
2757
- background: transparent;
2758
- color: var(--uidex-color-text);
2759
- font-size: var(--uidex-font-size-sm);
2760
- font-family: var(--uidex-font-mono);
2761
3003
  line-height: 1.5;
2762
3004
  resize: vertical;
2763
- outline: none;
2764
3005
  min-height: 80px;
2765
3006
  }
2766
3007
 
2767
- .uidex-form-textarea::placeholder {
2768
- color: var(--uidex-color-text-muted);
2769
- }
2770
-
2771
- .uidex-form-textarea:focus {
2772
- border-color: rgba(255, 255, 255, 0.3);
2773
- }
2774
-
2775
3008
  .uidex-form-submit {
2776
3009
  padding: 6px 16px;
2777
3010
  border: 1px solid var(--uidex-color-border);
@@ -2790,7 +3023,7 @@ body.uidex-inspecting * {
2790
3023
  background: var(--uidex-color-primary-hover);
2791
3024
  }
2792
3025
 
2793
- .uidex-form-submit:active {
3026
+ .uidex-form-submit:not(:disabled):active {
2794
3027
  transform: translateY(1px);
2795
3028
  }
2796
3029
 
@@ -2934,8 +3167,17 @@ var UidexUI = class {
2934
3167
  copyTimer = null;
2935
3168
  currentPresentIds = [];
2936
3169
  activeMode = null;
3170
+ sessionId;
3171
+ capture = null;
2937
3172
  constructor(options = {}) {
2938
3173
  this.options = options;
3174
+ this.sessionId = generateSessionId();
3175
+ if (options.ingest?.captureConsole || options.ingest?.captureNetwork) {
3176
+ this.capture = new IngestCapture(
3177
+ options.ingest.captureConsole ?? false,
3178
+ options.ingest.captureNetwork ?? false
3179
+ );
3180
+ }
2939
3181
  this.overlay = new Overlay({
2940
3182
  color: options.config?.defaults?.color,
2941
3183
  borderStyle: options.config?.defaults?.borderStyle,
@@ -2952,7 +3194,12 @@ var UidexUI = class {
2952
3194
  }
2953
3195
  this.options.onSelect?.(id);
2954
3196
  },
2955
- elementGetter: (id) => this.findElement(id)
3197
+ elementGetter: (id) => this.findElement(id),
3198
+ ingest: options.ingest,
3199
+ onSubmit: options.onSubmit,
3200
+ sessionId: this.sessionId,
3201
+ getConsoleLogs: () => this.capture?.getConsoleLogs() ?? [],
3202
+ getNetworkErrors: () => this.capture?.getNetworkErrors() ?? []
2956
3203
  });
2957
3204
  this.menu = new Menu({
2958
3205
  onInspectToggle: () => this.toggleMode("inspect"),
@@ -3003,12 +3250,14 @@ var UidexUI = class {
3003
3250
  this.modal.setShadowRoot(this.shadowRoot);
3004
3251
  this.updateModalData(presentIds);
3005
3252
  this.inspector?.mount();
3253
+ this.capture?.start();
3006
3254
  this.startObserving();
3007
3255
  this.mounted = true;
3008
3256
  }
3009
3257
  destroy() {
3010
3258
  if (!this.mounted) return;
3011
3259
  this.stopObserving();
3260
+ this.capture?.stop();
3012
3261
  if (this.copyTimer !== null) {
3013
3262
  clearTimeout(this.copyTimer);
3014
3263
  this.copyTimer = null;
@@ -3198,7 +3447,9 @@ function UidexDevtools({
3198
3447
  buttonPosition = "bottom-right",
3199
3448
  disabled = false,
3200
3449
  onSelect,
3201
- inspectShortcut
3450
+ inspectShortcut,
3451
+ ingest,
3452
+ onSubmit
3202
3453
  }) {
3203
3454
  const uiRef = (0, import_react.useRef)(null);
3204
3455
  const stableShortcut = (0, import_react.useMemo)(
@@ -3208,6 +3459,15 @@ function UidexDevtools({
3208
3459
  inspectShortcut === false ? false : `${inspectShortcut?.key}:${inspectShortcut?.ctrlKey}:${inspectShortcut?.shiftKey}:${inspectShortcut?.altKey}:${inspectShortcut?.metaKey}`
3209
3460
  ]
3210
3461
  );
3462
+ const stableIngest = (0, import_react.useMemo)(
3463
+ () => ingest,
3464
+ // eslint-disable-next-line react-hooks/exhaustive-deps
3465
+ [
3466
+ ingest ? `${ingest.endpoint}:${ingest.apiKey}:${ingest.environment}:${ingest.appVersion}:${ingest.captureConsole}:${ingest.captureNetwork}:${ingest.reporter?.email}:${ingest.reporter?.name}` : void 0
3467
+ ]
3468
+ );
3469
+ const onSubmitRef = (0, import_react.useRef)(onSubmit);
3470
+ onSubmitRef.current = onSubmit;
3211
3471
  (0, import_react.useEffect)(() => {
3212
3472
  if (disabled) {
3213
3473
  return;
@@ -3217,7 +3477,9 @@ function UidexDevtools({
3217
3477
  config,
3218
3478
  buttonPosition,
3219
3479
  onSelect,
3220
- inspectShortcut: stableShortcut
3480
+ inspectShortcut: stableShortcut,
3481
+ ingest: stableIngest,
3482
+ onSubmit: (...args) => onSubmitRef.current?.(...args)
3221
3483
  });
3222
3484
  ui.mount();
3223
3485
  uiRef.current = ui;
@@ -3225,7 +3487,7 @@ function UidexDevtools({
3225
3487
  ui.destroy();
3226
3488
  uiRef.current = null;
3227
3489
  };
3228
- }, [components, config, buttonPosition, disabled, onSelect, stableShortcut]);
3490
+ }, [components, config, buttonPosition, disabled, onSelect, stableShortcut, stableIngest]);
3229
3491
  return null;
3230
3492
  }
3231
3493