@mushi-mushi/web 1.9.0 → 1.11.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,103 @@ 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 \u2014 confirm?";
1838
+ case "verified":
1839
+ return "Verified";
1840
+ case "reopened":
1841
+ return "Reopened";
1842
+ case "dismissed":
1843
+ return "Closed";
1844
+ default:
1845
+ return status.replace(/_/g, " ");
1846
+ }
1847
+ }
1848
+ function reporterStatusTone(status) {
1849
+ switch (status) {
1850
+ case "new":
1851
+ case "queued":
1852
+ case "pending":
1853
+ case "submitted":
1854
+ return "sent";
1855
+ case "classified":
1856
+ case "triaged":
1857
+ case "grouped":
1858
+ case "dispatched":
1859
+ return "review";
1860
+ case "fixing":
1861
+ return "fixing";
1862
+ case "fixed":
1863
+ case "resolved":
1864
+ case "completed":
1865
+ return "fixed";
1866
+ case "verified":
1867
+ return "fixed";
1868
+ case "reopened":
1869
+ return "fixing";
1870
+ case "dismissed":
1871
+ return "closed";
1872
+ default:
1873
+ return "unknown";
1874
+ }
1875
+ }
1876
+ function formatRelativeTime(iso) {
1877
+ const then = Date.parse(iso);
1878
+ if (Number.isNaN(then)) return "";
1879
+ const diffMs = Date.now() - then;
1880
+ if (diffMs < 0) return "just now";
1881
+ const sec = Math.floor(diffMs / 1e3);
1882
+ if (sec < 60) return "just now";
1883
+ const min = Math.floor(sec / 60);
1884
+ if (min < 60) return `${min}m ago`;
1885
+ const hr = Math.floor(min / 60);
1886
+ if (hr < 24) return `${hr}h ago`;
1887
+ const day = Math.floor(hr / 24);
1888
+ if (day < 7) return `${day}d ago`;
1889
+ const week = Math.floor(day / 7);
1890
+ if (week < 5) return `${week}w ago`;
1891
+ return new Date(then).toLocaleDateString(void 0, { month: "short", day: "numeric" });
1892
+ }
1690
1893
  function pad2(n) {
1691
1894
  return n < 10 ? `0${n}` : String(n);
1692
1895
  }
@@ -1801,6 +2004,8 @@ var MushiWidget = class _MushiWidget {
1801
2004
  successTimer = null;
1802
2005
  autoCloseTimer = null;
1803
2006
  rewardsState = null;
2007
+ leaderboardEntries = null;
2008
+ leaderboardLoading = false;
1804
2009
  /** Server-confirmed id for the just-submitted report. Surfaces in
1805
2010
  * the success step as a copyable receipt + optional deep link to
1806
2011
  * the Mushi console (when `dashboardUrl` is configured). Cleared
@@ -2073,6 +2278,11 @@ var MushiWidget = class _MushiWidget {
2073
2278
  this.rewardsState = state;
2074
2279
  if (this.isOpen) this.render();
2075
2280
  }
2281
+ setLeaderboard(entries, loading = false) {
2282
+ this.leaderboardEntries = entries;
2283
+ this.leaderboardLoading = loading;
2284
+ if (this.isOpen && this.step === "leaderboard") this.render();
2285
+ }
2076
2286
  destroy() {
2077
2287
  if (this.successTimer !== null) {
2078
2288
  clearTimeout(this.successTimer);
@@ -2493,6 +2703,8 @@ var MushiWidget = class _MushiWidget {
2493
2703
  return this.renderReportsStep();
2494
2704
  case "report-detail":
2495
2705
  return this.renderReportDetailStep();
2706
+ case "leaderboard":
2707
+ return this.renderLeaderboardStep();
2496
2708
  }
2497
2709
  }
2498
2710
  renderOutdatedBanner() {
@@ -2525,7 +2737,7 @@ var MushiWidget = class _MushiWidget {
2525
2737
  renderHeader(opts) {
2526
2738
  const t = this.locale;
2527
2739
  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>`;
2740
+ 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
2741
  const counterHtml = step ? `<span class="mushi-step-counter" aria-label="Step ${step} of ${TOTAL_STEPS}"><b>${pad2(step)}</b> / ${pad2(TOTAL_STEPS)}</span>` : "";
2530
2742
  return `
2531
2743
  <div class="mushi-header">
@@ -2661,24 +2873,61 @@ var MushiWidget = class _MushiWidget {
2661
2873
  `;
2662
2874
  }
2663
2875
  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("");
2876
+ const reports = this.reporterReports.map((report) => {
2877
+ const title = report.summary ?? report.description ?? `Report ${report.id.slice(0, 8)}`;
2878
+ const tone = reporterStatusTone(report.status);
2879
+ const when = formatRelativeTime(report.created_at);
2880
+ const unread = report.unread_count && report.unread_count > 0 ? `<span class="mushi-unread-badge" aria-label="${report.unread_count} unread">${report.unread_count}</span>` : "";
2881
+ return `
2882
+ <button type="button" class="mushi-report-row" data-report-id="${escapeHtml(report.id)}" aria-label="View report: ${escapeHtml(title)}">
2883
+ <div class="mushi-report-main">
2884
+ <span class="mushi-report-title">${escapeHtml(title)}</span>
2885
+ <span class="mushi-report-meta">
2886
+ <span class="mushi-report-status mushi-status-${tone}">${escapeHtml(reporterStatusShort(report.status))}</span>
2887
+ ${when ? `<span class="mushi-report-when">${escapeHtml(when)}</span>` : ""}
2888
+ ${unread}
2889
+ </span>
2890
+ </div>
2891
+ <span class="mushi-report-chevron" aria-hidden="true">\u203A</span>
2892
+ </button>`;
2893
+ }).join("");
2894
+ const leaderboardBtn = this.rewardsState ? `<button type="button" class="mushi-leaderboard-link" data-action="open-leaderboard">\u{1F3C6} Leaderboard</button>` : "";
2671
2895
  return `
2672
2896
  ${this.renderHeader({ title: "Your reports", showBack: true, eyebrow: "Mushi \xB7 Inbox" })}
2673
2897
  <div class="mushi-body">
2674
2898
  ${this.reporterLoading ? '<p class="mushi-muted">Loading reports\u2026</p>' : ""}
2675
2899
  ${this.reporterError ? `<p class="mushi-error-inline">${escapeHtml(this.reporterError)}</p>` : ""}
2676
2900
  ${reports || (!this.reporterLoading ? '<p class="mushi-muted">No reports from this browser yet.</p>' : "")}
2901
+ ${leaderboardBtn}
2902
+ </div>
2903
+ `;
2904
+ }
2905
+ renderLeaderboardStep() {
2906
+ const myRank = this.rewardsState && this.leaderboardEntries ? this.leaderboardEntries.findIndex((e) => e.display_name === "You") + 1 : 0;
2907
+ const rows = (this.leaderboardEntries ?? []).map((e, i) => `
2908
+ <div class="mushi-lb-row ${i === 0 ? "mushi-lb-top" : ""}">
2909
+ <span class="mushi-lb-rank">#${i + 1}</span>
2910
+ <span class="mushi-lb-name">${escapeHtml(e.display_name)}</span>
2911
+ ${e.tier_name ? `<span class="mushi-lb-tier">${escapeHtml(e.tier_name)}</span>` : ""}
2912
+ <span class="mushi-lb-pts">${e.total_points.toLocaleString()} pts</span>
2913
+ </div>
2914
+ `).join("");
2915
+ return `
2916
+ ${this.renderHeader({ title: "\u{1F3C6} Leaderboard", showBack: true, eyebrow: "Mushi \xB7 Contributors" })}
2917
+ <div class="mushi-body">
2918
+ ${this.leaderboardLoading ? '<p class="mushi-muted">Loading leaderboard\u2026</p>' : ""}
2919
+ ${!this.leaderboardLoading && !this.leaderboardEntries?.length ? '<p class="mushi-muted">No contributors yet \u2014 be the first!</p>' : ""}
2920
+ <div class="mushi-lb-list">${rows}</div>
2921
+ ${myRank > 0 ? `<p class="mushi-lb-myrank">You are ranked #${myRank}</p>` : ""}
2922
+ <p class="mushi-lb-note">Top contributors this month \xB7 Points refresh monthly</p>
2677
2923
  </div>
2678
2924
  `;
2679
2925
  }
2680
2926
  renderReportDetailStep() {
2681
2927
  const report = this.reporterReports.find((r) => r.id === this.selectedReportId);
2928
+ const status = report?.status ?? "unknown";
2929
+ const tone = reporterStatusTone(status);
2930
+ const when = report?.created_at ? formatRelativeTime(report.created_at) : "";
2682
2931
  const comments = this.reporterComments.map((comment) => `
2683
2932
  <div class="mushi-thread-comment ${comment.author_kind}">
2684
2933
  <strong>${escapeHtml(comment.author_kind === "reporter" ? "You" : comment.author_name ?? "Developer")}</strong>
@@ -2689,12 +2938,21 @@ var MushiWidget = class _MushiWidget {
2689
2938
  ${this.renderHeader({ title: "Report thread", showBack: true, eyebrow: "Mushi \xB7 Inbox" })}
2690
2939
  <div class="mushi-body">
2691
2940
  <div class="mushi-thread-summary">
2692
- <span>${escapeHtml(report?.status ?? "unknown")}</span>
2941
+ <div class="mushi-thread-summary-meta">
2942
+ <span class="mushi-report-status mushi-status-${tone}">${escapeHtml(reporterStatusLabel(status))}</span>
2943
+ ${when ? `<span class="mushi-report-when">Reported ${escapeHtml(when)}</span>` : ""}
2944
+ </div>
2693
2945
  <p>${escapeHtml(report?.summary ?? report?.description ?? "Report details")}</p>
2694
2946
  </div>
2695
2947
  <div class="mushi-thread">
2696
2948
  ${this.reporterLoading ? '<p class="mushi-muted">Loading thread\u2026</p>' : comments || '<p class="mushi-muted">No developer replies yet.</p>'}
2697
2949
  </div>
2950
+ ${["fixed", "resolved", "verified"].includes(status) ? `
2951
+ <div class="mushi-verify-actions" role="group" aria-label="Fix verification">
2952
+ <button type="button" class="mushi-intent-btn" data-action="reporter-confirms">Yes, fixed for me</button>
2953
+ <button type="button" class="mushi-intent-btn" data-action="reporter-not-fixed">Not fixed yet</button>
2954
+ </div>
2955
+ ` : ""}
2698
2956
  <textarea class="mushi-textarea" data-role="reporter-reply" rows="3" placeholder="Reply to the developer\u2026"></textarea>
2699
2957
  <button type="button" class="mushi-submit" data-action="reporter-reply">
2700
2958
  <span>Reply</span><span class="mushi-submit-arrow" aria-hidden="true">\u2192</span>
@@ -2997,6 +3255,8 @@ var MushiWidget = class _MushiWidget {
2997
3255
  } else if (this.step === "report-detail") {
2998
3256
  this.step = "reports";
2999
3257
  this.selectedReportId = null;
3258
+ } else if (this.step === "leaderboard") {
3259
+ this.step = "reports";
3000
3260
  }
3001
3261
  this.render();
3002
3262
  });
@@ -3016,9 +3276,20 @@ var MushiWidget = class _MushiWidget {
3016
3276
  if (reportId) void this.loadReporterComments(reportId);
3017
3277
  });
3018
3278
  });
3279
+ panel.querySelector('[data-action="open-leaderboard"]')?.addEventListener("click", () => {
3280
+ this.step = "leaderboard";
3281
+ this.callbacks.onLeaderboardOpen?.();
3282
+ this.render();
3283
+ });
3019
3284
  panel.querySelector('[data-action="reporter-reply"]')?.addEventListener("click", () => {
3020
3285
  void this.submitReporterReply(panel);
3021
3286
  });
3287
+ panel.querySelector('[data-action="reporter-confirms"]')?.addEventListener("click", () => {
3288
+ void this.submitReporterFeedback("confirms");
3289
+ });
3290
+ panel.querySelector('[data-action="reporter-not-fixed"]')?.addEventListener("click", () => {
3291
+ void this.submitReporterFeedback("not_fixed");
3292
+ });
3022
3293
  panel.querySelector('[data-action="copy-report-id"]')?.addEventListener("click", (e) => {
3023
3294
  const btn = e.currentTarget;
3024
3295
  const id = btn.dataset.copyId;
@@ -3200,6 +3471,21 @@ var MushiWidget = class _MushiWidget {
3200
3471
  this.render();
3201
3472
  }
3202
3473
  }
3474
+ async submitReporterFeedback(signal) {
3475
+ const reportId = this.selectedReportId;
3476
+ if (!reportId || this.reporterLoading) return;
3477
+ this.reporterLoading = true;
3478
+ this.render();
3479
+ try {
3480
+ await this.callbacks.onReporterFeedback?.(reportId, signal);
3481
+ await this.loadReporterReports();
3482
+ if (reportId) await this.loadReporterComments(reportId);
3483
+ } catch (err) {
3484
+ this.reporterError = err instanceof Error ? err.message : "Could not send feedback.";
3485
+ this.reporterLoading = false;
3486
+ this.render();
3487
+ }
3488
+ }
3203
3489
  async submitReporterReply(panel) {
3204
3490
  const reportId = this.selectedReportId;
3205
3491
  const textarea = panel.querySelector('[data-role="reporter-reply"]');
@@ -3267,6 +3553,10 @@ var MushiWidget = class _MushiWidget {
3267
3553
  const submit = this.shadow.querySelector('[data-action="submit"]');
3268
3554
  submit?.click();
3269
3555
  }
3556
+ recorderOpenMyReports() {
3557
+ if (!this.isOpen) this.open();
3558
+ void this.loadReporterReports();
3559
+ }
3270
3560
  };
3271
3561
 
3272
3562
  // src/marketing-recorder.ts
@@ -3289,7 +3579,8 @@ function exposeMarketingRecorder(widget) {
3289
3579
  selectCategory: (category) => widget.recorderSelectCategory(category),
3290
3580
  selectIntent: (label) => widget.recorderSelectIntent(label),
3291
3581
  focusDescription: () => widget.recorderFocusDescription(),
3292
- submit: () => widget.recorderSubmit()
3582
+ submit: () => widget.recorderSubmit(),
3583
+ openMyReports: () => widget.recorderOpenMyReports()
3293
3584
  };
3294
3585
  globalThis.__mushiRecorder = api;
3295
3586
  }
@@ -3460,6 +3751,24 @@ async function fetchAndCacheTier(userId) {
3460
3751
  noteRewardsApiFailure(res.error?.code);
3461
3752
  return null;
3462
3753
  }
3754
+ async function fetchLeaderboard(limit = 10) {
3755
+ if (!apiClient || isRewardsApiBackedOff()) return null;
3756
+ try {
3757
+ const res = await apiClient.getHallOfFame(limit);
3758
+ if (res.ok && res.data) {
3759
+ return (res.data.data ?? []).map((e) => ({
3760
+ display_name: e.display_name,
3761
+ tier_name: e.tier_name,
3762
+ total_points: e.total_points,
3763
+ points_30d: e.points_30d
3764
+ }));
3765
+ }
3766
+ noteRewardsApiFailure(res.error?.code);
3767
+ return null;
3768
+ } catch {
3769
+ return null;
3770
+ }
3771
+ }
3463
3772
  var routeObserver = null;
3464
3773
  var clickHandler = null;
3465
3774
  var origPushState = null;
@@ -5030,7 +5339,7 @@ function createProactiveManager(config = {}) {
5030
5339
 
5031
5340
  // src/version.ts
5032
5341
  var MUSHI_SDK_PACKAGE = "@mushi-mushi/web";
5033
- var MUSHI_SDK_VERSION = "1.9.0" ;
5342
+ var MUSHI_SDK_VERSION = "1.11.0" ;
5034
5343
 
5035
5344
  // src/mushi.ts
5036
5345
  var instance = null;
@@ -5308,6 +5617,22 @@ function createInstance(config) {
5308
5617
  async onReporterReply(reportId, body) {
5309
5618
  const result = await apiClient2.replyToReporterReport(reportId, core.getReporterToken(), body);
5310
5619
  if (!result.ok) throw new Error(result.error?.message ?? "Could not send reply");
5620
+ },
5621
+ async onReporterFeedback(reportId, signal, note) {
5622
+ const result = await apiClient2.replyToReporterReport(reportId, core.getReporterToken(), note ?? "", signal);
5623
+ if (!result.ok) throw new Error(result.error?.message ?? "Could not send feedback");
5624
+ return result.data?.feedback ?? null;
5625
+ },
5626
+ async onReporterReopen(reportId, note) {
5627
+ const result = await apiClient2.reopenReporterReport(reportId, core.getReporterToken(), note);
5628
+ if (!result.ok) throw new Error(result.error?.message ?? "Could not reopen report");
5629
+ return result.data?.outcome ?? null;
5630
+ },
5631
+ onLeaderboardOpen() {
5632
+ widget.setLeaderboard(null, true);
5633
+ void fetchLeaderboard(10).then((entries) => {
5634
+ widget.setLeaderboard(entries, false);
5635
+ });
5311
5636
  }
5312
5637
  }, MUSHI_SDK_VERSION);
5313
5638
  syncCaptureModules();
@@ -5564,7 +5889,7 @@ function createInstance(config) {
5564
5889
  await offlineQueue.enqueue(finalReport);
5565
5890
  log.info("Offline \u2014 report queued", { reportId: finalReport.id });
5566
5891
  emit("report:queued", { reportId: finalReport.id });
5567
- return;
5892
+ return { reportId: null, queuedOffline: true };
5568
5893
  }
5569
5894
  const result = await apiClient2.submitReport(finalReport);
5570
5895
  if (result.ok) {
@@ -5862,6 +6187,41 @@ function createInstance(config) {
5862
6187
  },
5863
6188
  pulseTrigger() {
5864
6189
  widget.pulseTrigger?.();
6190
+ },
6191
+ // ─── Reporter API (cross-platform) ────────────────────────────────
6192
+ async listMyReports() {
6193
+ const result = await apiClient2.listReporterReports(core.getReporterToken());
6194
+ if (!result.ok) return [];
6195
+ return result.data?.reports ?? [];
6196
+ },
6197
+ async listMyComments(reportId) {
6198
+ const result = await apiClient2.listReporterComments(reportId, core.getReporterToken());
6199
+ if (!result.ok) return [];
6200
+ return result.data?.comments ?? [];
6201
+ },
6202
+ async replyToReport(reportId, body) {
6203
+ const result = await apiClient2.replyToReporterReport(reportId, core.getReporterToken(), body);
6204
+ if (!result.ok) return null;
6205
+ return result.data?.comment ?? null;
6206
+ },
6207
+ async submitFeedbackSignal(reportId, signal, note) {
6208
+ const result = await apiClient2.replyToReporterReport(reportId, core.getReporterToken(), note ?? "", signal);
6209
+ if (!result.ok) return null;
6210
+ return result.data?.feedback ?? null;
6211
+ },
6212
+ async reopenReport(reportId, note) {
6213
+ const result = await apiClient2.reopenReporterReport(reportId, core.getReporterToken(), note);
6214
+ if (!result.ok) return null;
6215
+ return result.data?.outcome ?? null;
6216
+ },
6217
+ openMyReports() {
6218
+ widget.recorderOpenMyReports();
6219
+ },
6220
+ async getHallOfFame(limit = 20) {
6221
+ const result = await apiClient2.getHallOfFame(limit);
6222
+ if (!result.ok) return [];
6223
+ const raw = result.data;
6224
+ return raw?.data ?? [];
5865
6225
  }
5866
6226
  };
5867
6227
  if (typeof globalThis !== "undefined" && (bootstrapConfig.debug ?? false)) {
@@ -6179,7 +6539,15 @@ function createNoopInstance() {
6179
6539
  recordActivity: () => {
6180
6540
  },
6181
6541
  pulseTrigger: () => {
6182
- }
6542
+ },
6543
+ listMyReports: async () => [],
6544
+ listMyComments: async () => [],
6545
+ replyToReport: async () => null,
6546
+ submitFeedbackSignal: async () => null,
6547
+ reopenReport: async () => null,
6548
+ openMyReports: () => {
6549
+ },
6550
+ getHallOfFame: async () => []
6183
6551
  };
6184
6552
  }
6185
6553
  function installAutoBreadcrumbs(buffer) {