@mushi-mushi/web 1.9.0 → 1.10.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.
package/dist/index.cjs CHANGED
@@ -553,23 +553,47 @@ function getWidgetStyles(theme) {
553
553
  font-weight: 600;
554
554
  color: ${ink};
555
555
  }
556
- .mushi-close, .mushi-back {
556
+ .mushi-close {
557
557
  background: none;
558
558
  border: none;
559
559
  cursor: pointer;
560
- padding: 4px;
560
+ padding: 2px 4px;
561
561
  color: ${inkMuted};
562
562
  font-family: ${fontBody};
563
- font-size: 14px;
563
+ font-size: 16px;
564
564
  line-height: 1;
565
- border-radius: 3px;
565
+ border-radius: 0;
566
+ transition: color 150ms ${easeStamp};
567
+ }
568
+ .mushi-back {
569
+ align-self: flex-start;
570
+ background: none;
571
+ border: none;
572
+ cursor: pointer;
573
+ padding: 0;
574
+ margin: 0 0 2px;
575
+ color: ${inkMuted};
576
+ font-family: ${fontMono};
577
+ font-size: 10px;
578
+ font-weight: 500;
579
+ letter-spacing: 0.12em;
580
+ text-transform: uppercase;
581
+ line-height: 1.2;
582
+ border-radius: 0;
566
583
  transition: color 150ms ${easeStamp};
567
584
  }
568
- .mushi-close:hover, .mushi-back:hover { color: ${widgetAccent}; }
569
- .mushi-close:focus-visible, .mushi-back:focus-visible {
585
+ .mushi-close:hover { color: ${widgetAccent}; }
586
+ .mushi-back:hover { color: ${ink}; }
587
+ .mushi-close:focus-visible {
570
588
  outline: 1.5px solid ${widgetAccent};
571
589
  outline-offset: 2px;
572
590
  }
591
+ .mushi-back:focus-visible {
592
+ outline: none;
593
+ color: ${widgetAccent};
594
+ text-decoration: underline;
595
+ text-underline-offset: 3px;
596
+ }
573
597
 
574
598
  /* \u2500\u2500 Body \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
575
599
  Generous left/right padding (22px) so type breathes. Vertical
@@ -658,39 +682,121 @@ function getWidgetStyles(theme) {
658
682
  .mushi-report-row {
659
683
  width: 100%;
660
684
  display: grid;
661
- grid-template-columns: auto 1fr auto;
662
- gap: 8px;
685
+ grid-template-columns: 1fr auto;
686
+ gap: 10px;
663
687
  align-items: center;
664
- padding: 10px 0;
688
+ padding: 12px 4px 12px 0;
665
689
  border: 0;
666
690
  border-bottom: 1px solid ${rule};
667
691
  background: transparent;
668
692
  color: ${ink};
669
693
  cursor: pointer;
670
694
  text-align: left;
695
+ transition: background 180ms ${easeStamp}, padding-left 180ms ${easeStamp};
696
+ }
697
+ .mushi-report-row:hover,
698
+ .mushi-report-row:focus-visible {
699
+ background: ${isDark ? "rgba(242,235,221,0.04)" : "rgba(14,13,11,0.03)"};
700
+ padding-left: 4px;
701
+ }
702
+ .mushi-report-main {
703
+ min-width: 0;
704
+ display: grid;
705
+ gap: 6px;
706
+ }
707
+ .mushi-report-title {
708
+ font-size: 13px;
709
+ line-height: 1.35;
710
+ display: -webkit-box;
711
+ -webkit-line-clamp: 2;
712
+ -webkit-box-orient: vertical;
713
+ overflow: hidden;
714
+ }
715
+ .mushi-report-meta {
716
+ display: flex;
717
+ flex-wrap: wrap;
718
+ align-items: center;
719
+ gap: 8px;
671
720
  }
672
721
  .mushi-report-status {
722
+ display: inline-flex;
723
+ align-items: center;
673
724
  font-family: ${fontMono};
674
725
  font-size: 10px;
675
- color: ${widgetAccent};
726
+ font-weight: 600;
727
+ letter-spacing: 0.04em;
676
728
  text-transform: uppercase;
729
+ padding: 2px 7px;
730
+ border-radius: 999px;
731
+ border: 1px solid transparent;
732
+ }
733
+ .mushi-status-sent {
734
+ color: ${isDark ? "#A8C4FF" : "#1E4A8C"};
735
+ background: ${isDark ? "rgba(120,160,255,0.12)" : "rgba(30,74,140,0.08)"};
736
+ border-color: ${isDark ? "rgba(120,160,255,0.22)" : "rgba(30,74,140,0.16)"};
737
+ }
738
+ .mushi-status-review {
739
+ color: ${isDark ? "#FFD27A" : "#8A5A00"};
740
+ background: ${isDark ? "rgba(255,190,90,0.12)" : "rgba(180,120,0,0.10)"};
741
+ border-color: ${isDark ? "rgba(255,190,90,0.22)" : "rgba(180,120,0,0.18)"};
742
+ }
743
+ .mushi-status-fixing {
744
+ color: ${isDark ? "#FFB899" : "#9A3D12"};
745
+ background: ${isDark ? "rgba(255,120,60,0.12)" : "rgba(224,60,44,0.10)"};
746
+ border-color: ${isDark ? "rgba(255,120,60,0.24)" : "rgba(224,60,44,0.18)"};
747
+ }
748
+ .mushi-status-fixed {
749
+ color: ${isDark ? "#8FE3B0" : "#1F6B3A"};
750
+ background: ${isDark ? "rgba(80,200,130,0.12)" : "rgba(31,107,58,0.10)"};
751
+ border-color: ${isDark ? "rgba(80,200,130,0.22)" : "rgba(31,107,58,0.18)"};
752
+ }
753
+ .mushi-status-closed,
754
+ .mushi-status-unknown {
755
+ color: ${inkMuted};
756
+ background: ${isDark ? "rgba(242,235,221,0.06)" : "rgba(14,13,11,0.05)"};
757
+ border-color: ${ruleStrong};
677
758
  }
678
- .mushi-report-title {
679
- font-size: 13px;
680
- overflow: hidden;
681
- text-overflow: ellipsis;
682
- white-space: nowrap;
759
+ .mushi-report-when {
760
+ font-family: ${fontMono};
761
+ font-size: 10px;
762
+ color: ${inkFaint};
763
+ }
764
+ .mushi-unread-badge {
765
+ display: inline-flex;
766
+ align-items: center;
767
+ justify-content: center;
768
+ min-width: 18px;
769
+ height: 18px;
770
+ padding: 0 5px;
771
+ border-radius: 999px;
772
+ font-family: ${fontMono};
773
+ font-size: 10px;
774
+ font-weight: 700;
775
+ color: ${widgetAccentInk};
776
+ background: ${widgetAccent};
777
+ }
778
+ .mushi-report-chevron {
779
+ font-size: 18px;
780
+ line-height: 1;
781
+ color: ${inkFaint};
782
+ transition: color 180ms ${easeStamp}, transform 180ms ${easeStamp};
783
+ }
784
+ .mushi-report-row:hover .mushi-report-chevron,
785
+ .mushi-report-row:focus-visible .mushi-report-chevron {
786
+ color: ${widgetAccent};
787
+ transform: translateX(2px);
683
788
  }
684
789
  .mushi-thread-summary {
685
790
  border-bottom: 1px solid ${rule};
686
791
  padding-bottom: 10px;
687
792
  margin-bottom: 10px;
688
793
  }
689
- .mushi-thread-summary span {
690
- font-family: ${fontMono};
691
- font-size: 10px;
692
- color: ${widgetAccent};
693
- text-transform: uppercase;
794
+ .mushi-thread-summary-meta {
795
+ display: flex;
796
+ flex-wrap: wrap;
797
+ align-items: center;
798
+ gap: 10px;
799
+ margin-bottom: 8px;
694
800
  }
695
801
  .mushi-thread {
696
802
  display: grid;
@@ -1687,6 +1793,95 @@ var CATEGORY_ICONS = {
1687
1793
  other: "\u{1F4DD}"
1688
1794
  };
1689
1795
  var FEATURE_REQUEST_INTENT = "Feature request";
1796
+ function reporterStatusShort(status) {
1797
+ switch (status) {
1798
+ case "new":
1799
+ case "queued":
1800
+ case "pending":
1801
+ case "submitted":
1802
+ return "Sent";
1803
+ case "classified":
1804
+ case "triaged":
1805
+ case "grouped":
1806
+ case "dispatched":
1807
+ return "Review";
1808
+ case "fixing":
1809
+ return "Fixing";
1810
+ case "fixed":
1811
+ case "resolved":
1812
+ case "completed":
1813
+ return "Fixed";
1814
+ case "dismissed":
1815
+ return "Closed";
1816
+ default:
1817
+ return status.replace(/_/g, " ").slice(0, 12);
1818
+ }
1819
+ }
1820
+ function reporterStatusLabel(status) {
1821
+ switch (status) {
1822
+ case "new":
1823
+ case "queued":
1824
+ case "pending":
1825
+ case "submitted":
1826
+ return "Submitted";
1827
+ case "classified":
1828
+ case "triaged":
1829
+ case "grouped":
1830
+ case "dispatched":
1831
+ return "In review";
1832
+ case "fixing":
1833
+ return "Fix in progress";
1834
+ case "fixed":
1835
+ case "resolved":
1836
+ case "completed":
1837
+ return "Fixed";
1838
+ case "dismissed":
1839
+ return "Closed";
1840
+ default:
1841
+ return status.replace(/_/g, " ");
1842
+ }
1843
+ }
1844
+ function reporterStatusTone(status) {
1845
+ switch (status) {
1846
+ case "new":
1847
+ case "queued":
1848
+ case "pending":
1849
+ case "submitted":
1850
+ return "sent";
1851
+ case "classified":
1852
+ case "triaged":
1853
+ case "grouped":
1854
+ case "dispatched":
1855
+ return "review";
1856
+ case "fixing":
1857
+ return "fixing";
1858
+ case "fixed":
1859
+ case "resolved":
1860
+ case "completed":
1861
+ return "fixed";
1862
+ case "dismissed":
1863
+ return "closed";
1864
+ default:
1865
+ return "unknown";
1866
+ }
1867
+ }
1868
+ function formatRelativeTime(iso) {
1869
+ const then = Date.parse(iso);
1870
+ if (Number.isNaN(then)) return "";
1871
+ const diffMs = Date.now() - then;
1872
+ if (diffMs < 0) return "just now";
1873
+ const sec = Math.floor(diffMs / 1e3);
1874
+ if (sec < 60) return "just now";
1875
+ const min = Math.floor(sec / 60);
1876
+ if (min < 60) return `${min}m ago`;
1877
+ const hr = Math.floor(min / 60);
1878
+ if (hr < 24) return `${hr}h ago`;
1879
+ const day = Math.floor(hr / 24);
1880
+ if (day < 7) return `${day}d ago`;
1881
+ const week = Math.floor(day / 7);
1882
+ if (week < 5) return `${week}w ago`;
1883
+ return new Date(then).toLocaleDateString(void 0, { month: "short", day: "numeric" });
1884
+ }
1690
1885
  function pad2(n) {
1691
1886
  return n < 10 ? `0${n}` : String(n);
1692
1887
  }
@@ -1801,6 +1996,8 @@ var MushiWidget = class _MushiWidget {
1801
1996
  successTimer = null;
1802
1997
  autoCloseTimer = null;
1803
1998
  rewardsState = null;
1999
+ leaderboardEntries = null;
2000
+ leaderboardLoading = false;
1804
2001
  /** Server-confirmed id for the just-submitted report. Surfaces in
1805
2002
  * the success step as a copyable receipt + optional deep link to
1806
2003
  * the Mushi console (when `dashboardUrl` is configured). Cleared
@@ -2073,6 +2270,11 @@ var MushiWidget = class _MushiWidget {
2073
2270
  this.rewardsState = state;
2074
2271
  if (this.isOpen) this.render();
2075
2272
  }
2273
+ setLeaderboard(entries, loading = false) {
2274
+ this.leaderboardEntries = entries;
2275
+ this.leaderboardLoading = loading;
2276
+ if (this.isOpen && this.step === "leaderboard") this.render();
2277
+ }
2076
2278
  destroy() {
2077
2279
  if (this.successTimer !== null) {
2078
2280
  clearTimeout(this.successTimer);
@@ -2493,6 +2695,8 @@ var MushiWidget = class _MushiWidget {
2493
2695
  return this.renderReportsStep();
2494
2696
  case "report-detail":
2495
2697
  return this.renderReportDetailStep();
2698
+ case "leaderboard":
2699
+ return this.renderLeaderboardStep();
2496
2700
  }
2497
2701
  }
2498
2702
  renderOutdatedBanner() {
@@ -2525,7 +2729,7 @@ var MushiWidget = class _MushiWidget {
2525
2729
  renderHeader(opts) {
2526
2730
  const t = this.locale;
2527
2731
  const { title, showBack = false, step, eyebrow } = opts;
2528
- const eyebrowHtml = showBack ? `<button type="button" class="mushi-back" data-action="back" aria-label="${t.widget.back}">\u2190 ${t.widget.back}</button>` : `<span class="mushi-header-eyebrow">${eyebrow ?? "Mushi \xB7 Report"}</span>`;
2732
+ const eyebrowHtml = showBack ? `<button type="button" class="mushi-back" data-action="back" aria-label="${t.widget.back}">\u2190</button>` : `<span class="mushi-header-eyebrow">${eyebrow ?? "Mushi \xB7 Report"}</span>`;
2529
2733
  const counterHtml = step ? `<span class="mushi-step-counter" aria-label="Step ${step} of ${TOTAL_STEPS}"><b>${pad2(step)}</b> / ${pad2(TOTAL_STEPS)}</span>` : "";
2530
2734
  return `
2531
2735
  <div class="mushi-header">
@@ -2661,24 +2865,61 @@ var MushiWidget = class _MushiWidget {
2661
2865
  `;
2662
2866
  }
2663
2867
  renderReportsStep() {
2664
- const reports = this.reporterReports.map((report) => `
2665
- <button type="button" class="mushi-report-row" data-report-id="${escapeHtml(report.id)}">
2666
- <span class="mushi-report-status">${escapeHtml(report.status)}</span>
2667
- <span class="mushi-report-title">${escapeHtml(report.summary ?? report.description ?? `Report ${report.id.slice(0, 8)}`)}</span>
2668
- ${report.unread_count ? `<b>${report.unread_count}</b>` : ""}
2669
- </button>
2670
- `).join("");
2868
+ const reports = this.reporterReports.map((report) => {
2869
+ const title = report.summary ?? report.description ?? `Report ${report.id.slice(0, 8)}`;
2870
+ const tone = reporterStatusTone(report.status);
2871
+ const when = formatRelativeTime(report.created_at);
2872
+ const unread = report.unread_count && report.unread_count > 0 ? `<span class="mushi-unread-badge" aria-label="${report.unread_count} unread">${report.unread_count}</span>` : "";
2873
+ return `
2874
+ <button type="button" class="mushi-report-row" data-report-id="${escapeHtml(report.id)}" aria-label="View report: ${escapeHtml(title)}">
2875
+ <div class="mushi-report-main">
2876
+ <span class="mushi-report-title">${escapeHtml(title)}</span>
2877
+ <span class="mushi-report-meta">
2878
+ <span class="mushi-report-status mushi-status-${tone}">${escapeHtml(reporterStatusShort(report.status))}</span>
2879
+ ${when ? `<span class="mushi-report-when">${escapeHtml(when)}</span>` : ""}
2880
+ ${unread}
2881
+ </span>
2882
+ </div>
2883
+ <span class="mushi-report-chevron" aria-hidden="true">\u203A</span>
2884
+ </button>`;
2885
+ }).join("");
2886
+ const leaderboardBtn = this.rewardsState ? `<button type="button" class="mushi-leaderboard-link" data-action="open-leaderboard">\u{1F3C6} Leaderboard</button>` : "";
2671
2887
  return `
2672
2888
  ${this.renderHeader({ title: "Your reports", showBack: true, eyebrow: "Mushi \xB7 Inbox" })}
2673
2889
  <div class="mushi-body">
2674
2890
  ${this.reporterLoading ? '<p class="mushi-muted">Loading reports\u2026</p>' : ""}
2675
2891
  ${this.reporterError ? `<p class="mushi-error-inline">${escapeHtml(this.reporterError)}</p>` : ""}
2676
2892
  ${reports || (!this.reporterLoading ? '<p class="mushi-muted">No reports from this browser yet.</p>' : "")}
2893
+ ${leaderboardBtn}
2894
+ </div>
2895
+ `;
2896
+ }
2897
+ renderLeaderboardStep() {
2898
+ const myRank = this.rewardsState && this.leaderboardEntries ? this.leaderboardEntries.findIndex((e) => e.display_name === "You") + 1 : 0;
2899
+ const rows = (this.leaderboardEntries ?? []).map((e, i) => `
2900
+ <div class="mushi-lb-row ${i === 0 ? "mushi-lb-top" : ""}">
2901
+ <span class="mushi-lb-rank">#${i + 1}</span>
2902
+ <span class="mushi-lb-name">${escapeHtml(e.display_name)}</span>
2903
+ ${e.tier_name ? `<span class="mushi-lb-tier">${escapeHtml(e.tier_name)}</span>` : ""}
2904
+ <span class="mushi-lb-pts">${e.total_points.toLocaleString()} pts</span>
2905
+ </div>
2906
+ `).join("");
2907
+ return `
2908
+ ${this.renderHeader({ title: "\u{1F3C6} Leaderboard", showBack: true, eyebrow: "Mushi \xB7 Contributors" })}
2909
+ <div class="mushi-body">
2910
+ ${this.leaderboardLoading ? '<p class="mushi-muted">Loading leaderboard\u2026</p>' : ""}
2911
+ ${!this.leaderboardLoading && !this.leaderboardEntries?.length ? '<p class="mushi-muted">No contributors yet \u2014 be the first!</p>' : ""}
2912
+ <div class="mushi-lb-list">${rows}</div>
2913
+ ${myRank > 0 ? `<p class="mushi-lb-myrank">You are ranked #${myRank}</p>` : ""}
2914
+ <p class="mushi-lb-note">Top contributors this month \xB7 Points refresh monthly</p>
2677
2915
  </div>
2678
2916
  `;
2679
2917
  }
2680
2918
  renderReportDetailStep() {
2681
2919
  const report = this.reporterReports.find((r) => r.id === this.selectedReportId);
2920
+ const status = report?.status ?? "unknown";
2921
+ const tone = reporterStatusTone(status);
2922
+ const when = report?.created_at ? formatRelativeTime(report.created_at) : "";
2682
2923
  const comments = this.reporterComments.map((comment) => `
2683
2924
  <div class="mushi-thread-comment ${comment.author_kind}">
2684
2925
  <strong>${escapeHtml(comment.author_kind === "reporter" ? "You" : comment.author_name ?? "Developer")}</strong>
@@ -2689,7 +2930,10 @@ var MushiWidget = class _MushiWidget {
2689
2930
  ${this.renderHeader({ title: "Report thread", showBack: true, eyebrow: "Mushi \xB7 Inbox" })}
2690
2931
  <div class="mushi-body">
2691
2932
  <div class="mushi-thread-summary">
2692
- <span>${escapeHtml(report?.status ?? "unknown")}</span>
2933
+ <div class="mushi-thread-summary-meta">
2934
+ <span class="mushi-report-status mushi-status-${tone}">${escapeHtml(reporterStatusLabel(status))}</span>
2935
+ ${when ? `<span class="mushi-report-when">Reported ${escapeHtml(when)}</span>` : ""}
2936
+ </div>
2693
2937
  <p>${escapeHtml(report?.summary ?? report?.description ?? "Report details")}</p>
2694
2938
  </div>
2695
2939
  <div class="mushi-thread">
@@ -2997,6 +3241,8 @@ var MushiWidget = class _MushiWidget {
2997
3241
  } else if (this.step === "report-detail") {
2998
3242
  this.step = "reports";
2999
3243
  this.selectedReportId = null;
3244
+ } else if (this.step === "leaderboard") {
3245
+ this.step = "reports";
3000
3246
  }
3001
3247
  this.render();
3002
3248
  });
@@ -3016,6 +3262,11 @@ var MushiWidget = class _MushiWidget {
3016
3262
  if (reportId) void this.loadReporterComments(reportId);
3017
3263
  });
3018
3264
  });
3265
+ panel.querySelector('[data-action="open-leaderboard"]')?.addEventListener("click", () => {
3266
+ this.step = "leaderboard";
3267
+ this.callbacks.onLeaderboardOpen?.();
3268
+ this.render();
3269
+ });
3019
3270
  panel.querySelector('[data-action="reporter-reply"]')?.addEventListener("click", () => {
3020
3271
  void this.submitReporterReply(panel);
3021
3272
  });
@@ -3267,6 +3518,10 @@ var MushiWidget = class _MushiWidget {
3267
3518
  const submit = this.shadow.querySelector('[data-action="submit"]');
3268
3519
  submit?.click();
3269
3520
  }
3521
+ recorderOpenMyReports() {
3522
+ if (!this.isOpen) this.open();
3523
+ void this.loadReporterReports();
3524
+ }
3270
3525
  };
3271
3526
 
3272
3527
  // src/marketing-recorder.ts
@@ -3289,7 +3544,8 @@ function exposeMarketingRecorder(widget) {
3289
3544
  selectCategory: (category) => widget.recorderSelectCategory(category),
3290
3545
  selectIntent: (label) => widget.recorderSelectIntent(label),
3291
3546
  focusDescription: () => widget.recorderFocusDescription(),
3292
- submit: () => widget.recorderSubmit()
3547
+ submit: () => widget.recorderSubmit(),
3548
+ openMyReports: () => widget.recorderOpenMyReports()
3293
3549
  };
3294
3550
  globalThis.__mushiRecorder = api;
3295
3551
  }
@@ -3460,6 +3716,24 @@ async function fetchAndCacheTier(userId) {
3460
3716
  noteRewardsApiFailure(res.error?.code);
3461
3717
  return null;
3462
3718
  }
3719
+ async function fetchLeaderboard(limit = 10) {
3720
+ if (!apiClient || isRewardsApiBackedOff()) return null;
3721
+ try {
3722
+ const res = await apiClient.getHallOfFame(limit);
3723
+ if (res.ok && res.data) {
3724
+ return (res.data.data ?? []).map((e) => ({
3725
+ display_name: e.display_name,
3726
+ tier_name: e.tier_name,
3727
+ total_points: e.total_points,
3728
+ points_30d: e.points_30d
3729
+ }));
3730
+ }
3731
+ noteRewardsApiFailure(res.error?.code);
3732
+ return null;
3733
+ } catch {
3734
+ return null;
3735
+ }
3736
+ }
3463
3737
  var routeObserver = null;
3464
3738
  var clickHandler = null;
3465
3739
  var origPushState = null;
@@ -5030,7 +5304,7 @@ function createProactiveManager(config = {}) {
5030
5304
 
5031
5305
  // src/version.ts
5032
5306
  var MUSHI_SDK_PACKAGE = "@mushi-mushi/web";
5033
- var MUSHI_SDK_VERSION = "1.9.0" ;
5307
+ var MUSHI_SDK_VERSION = "1.10.0" ;
5034
5308
 
5035
5309
  // src/mushi.ts
5036
5310
  var instance = null;
@@ -5308,6 +5582,12 @@ function createInstance(config) {
5308
5582
  async onReporterReply(reportId, body) {
5309
5583
  const result = await apiClient2.replyToReporterReport(reportId, core.getReporterToken(), body);
5310
5584
  if (!result.ok) throw new Error(result.error?.message ?? "Could not send reply");
5585
+ },
5586
+ onLeaderboardOpen() {
5587
+ widget.setLeaderboard(null, true);
5588
+ void fetchLeaderboard(10).then((entries) => {
5589
+ widget.setLeaderboard(entries, false);
5590
+ });
5311
5591
  }
5312
5592
  }, MUSHI_SDK_VERSION);
5313
5593
  syncCaptureModules();
@@ -5564,7 +5844,7 @@ function createInstance(config) {
5564
5844
  await offlineQueue.enqueue(finalReport);
5565
5845
  log.info("Offline \u2014 report queued", { reportId: finalReport.id });
5566
5846
  emit("report:queued", { reportId: finalReport.id });
5567
- return;
5847
+ return { reportId: null, queuedOffline: true };
5568
5848
  }
5569
5849
  const result = await apiClient2.submitReport(finalReport);
5570
5850
  if (result.ok) {
@@ -5862,6 +6142,28 @@ function createInstance(config) {
5862
6142
  },
5863
6143
  pulseTrigger() {
5864
6144
  widget.pulseTrigger?.();
6145
+ },
6146
+ // ─── Reporter API (cross-platform) ────────────────────────────────
6147
+ async listMyReports() {
6148
+ const result = await apiClient2.listReporterReports(core.getReporterToken());
6149
+ if (!result.ok) return [];
6150
+ return result.data?.reports ?? [];
6151
+ },
6152
+ async listMyComments(reportId) {
6153
+ const result = await apiClient2.listReporterComments(reportId, core.getReporterToken());
6154
+ if (!result.ok) return [];
6155
+ return result.data?.comments ?? [];
6156
+ },
6157
+ async replyToReport(reportId, body) {
6158
+ const result = await apiClient2.replyToReporterReport(reportId, core.getReporterToken(), body);
6159
+ if (!result.ok) return null;
6160
+ return result.data?.comment ?? null;
6161
+ },
6162
+ async getHallOfFame(limit = 20) {
6163
+ const result = await apiClient2.getHallOfFame(limit);
6164
+ if (!result.ok) return [];
6165
+ const raw = result.data;
6166
+ return raw?.data ?? [];
5865
6167
  }
5866
6168
  };
5867
6169
  if (typeof globalThis !== "undefined" && (bootstrapConfig.debug ?? false)) {
@@ -6179,7 +6481,11 @@ function createNoopInstance() {
6179
6481
  recordActivity: () => {
6180
6482
  },
6181
6483
  pulseTrigger: () => {
6182
- }
6484
+ },
6485
+ listMyReports: async () => [],
6486
+ listMyComments: async () => [],
6487
+ replyToReport: async () => null,
6488
+ getHallOfFame: async () => []
6183
6489
  };
6184
6490
  }
6185
6491
  function installAutoBreadcrumbs(buffer) {