@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.js CHANGED
@@ -551,23 +551,47 @@ function getWidgetStyles(theme) {
551
551
  font-weight: 600;
552
552
  color: ${ink};
553
553
  }
554
- .mushi-close, .mushi-back {
554
+ .mushi-close {
555
555
  background: none;
556
556
  border: none;
557
557
  cursor: pointer;
558
- padding: 4px;
558
+ padding: 2px 4px;
559
559
  color: ${inkMuted};
560
560
  font-family: ${fontBody};
561
- font-size: 14px;
561
+ font-size: 16px;
562
562
  line-height: 1;
563
- border-radius: 3px;
563
+ border-radius: 0;
564
+ transition: color 150ms ${easeStamp};
565
+ }
566
+ .mushi-back {
567
+ align-self: flex-start;
568
+ background: none;
569
+ border: none;
570
+ cursor: pointer;
571
+ padding: 0;
572
+ margin: 0 0 2px;
573
+ color: ${inkMuted};
574
+ font-family: ${fontMono};
575
+ font-size: 10px;
576
+ font-weight: 500;
577
+ letter-spacing: 0.12em;
578
+ text-transform: uppercase;
579
+ line-height: 1.2;
580
+ border-radius: 0;
564
581
  transition: color 150ms ${easeStamp};
565
582
  }
566
- .mushi-close:hover, .mushi-back:hover { color: ${widgetAccent}; }
567
- .mushi-close:focus-visible, .mushi-back:focus-visible {
583
+ .mushi-close:hover { color: ${widgetAccent}; }
584
+ .mushi-back:hover { color: ${ink}; }
585
+ .mushi-close:focus-visible {
568
586
  outline: 1.5px solid ${widgetAccent};
569
587
  outline-offset: 2px;
570
588
  }
589
+ .mushi-back:focus-visible {
590
+ outline: none;
591
+ color: ${widgetAccent};
592
+ text-decoration: underline;
593
+ text-underline-offset: 3px;
594
+ }
571
595
 
572
596
  /* \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
573
597
  Generous left/right padding (22px) so type breathes. Vertical
@@ -656,39 +680,121 @@ function getWidgetStyles(theme) {
656
680
  .mushi-report-row {
657
681
  width: 100%;
658
682
  display: grid;
659
- grid-template-columns: auto 1fr auto;
660
- gap: 8px;
683
+ grid-template-columns: 1fr auto;
684
+ gap: 10px;
661
685
  align-items: center;
662
- padding: 10px 0;
686
+ padding: 12px 4px 12px 0;
663
687
  border: 0;
664
688
  border-bottom: 1px solid ${rule};
665
689
  background: transparent;
666
690
  color: ${ink};
667
691
  cursor: pointer;
668
692
  text-align: left;
693
+ transition: background 180ms ${easeStamp}, padding-left 180ms ${easeStamp};
694
+ }
695
+ .mushi-report-row:hover,
696
+ .mushi-report-row:focus-visible {
697
+ background: ${isDark ? "rgba(242,235,221,0.04)" : "rgba(14,13,11,0.03)"};
698
+ padding-left: 4px;
699
+ }
700
+ .mushi-report-main {
701
+ min-width: 0;
702
+ display: grid;
703
+ gap: 6px;
704
+ }
705
+ .mushi-report-title {
706
+ font-size: 13px;
707
+ line-height: 1.35;
708
+ display: -webkit-box;
709
+ -webkit-line-clamp: 2;
710
+ -webkit-box-orient: vertical;
711
+ overflow: hidden;
712
+ }
713
+ .mushi-report-meta {
714
+ display: flex;
715
+ flex-wrap: wrap;
716
+ align-items: center;
717
+ gap: 8px;
669
718
  }
670
719
  .mushi-report-status {
720
+ display: inline-flex;
721
+ align-items: center;
671
722
  font-family: ${fontMono};
672
723
  font-size: 10px;
673
- color: ${widgetAccent};
724
+ font-weight: 600;
725
+ letter-spacing: 0.04em;
674
726
  text-transform: uppercase;
727
+ padding: 2px 7px;
728
+ border-radius: 999px;
729
+ border: 1px solid transparent;
730
+ }
731
+ .mushi-status-sent {
732
+ color: ${isDark ? "#A8C4FF" : "#1E4A8C"};
733
+ background: ${isDark ? "rgba(120,160,255,0.12)" : "rgba(30,74,140,0.08)"};
734
+ border-color: ${isDark ? "rgba(120,160,255,0.22)" : "rgba(30,74,140,0.16)"};
735
+ }
736
+ .mushi-status-review {
737
+ color: ${isDark ? "#FFD27A" : "#8A5A00"};
738
+ background: ${isDark ? "rgba(255,190,90,0.12)" : "rgba(180,120,0,0.10)"};
739
+ border-color: ${isDark ? "rgba(255,190,90,0.22)" : "rgba(180,120,0,0.18)"};
740
+ }
741
+ .mushi-status-fixing {
742
+ color: ${isDark ? "#FFB899" : "#9A3D12"};
743
+ background: ${isDark ? "rgba(255,120,60,0.12)" : "rgba(224,60,44,0.10)"};
744
+ border-color: ${isDark ? "rgba(255,120,60,0.24)" : "rgba(224,60,44,0.18)"};
745
+ }
746
+ .mushi-status-fixed {
747
+ color: ${isDark ? "#8FE3B0" : "#1F6B3A"};
748
+ background: ${isDark ? "rgba(80,200,130,0.12)" : "rgba(31,107,58,0.10)"};
749
+ border-color: ${isDark ? "rgba(80,200,130,0.22)" : "rgba(31,107,58,0.18)"};
750
+ }
751
+ .mushi-status-closed,
752
+ .mushi-status-unknown {
753
+ color: ${inkMuted};
754
+ background: ${isDark ? "rgba(242,235,221,0.06)" : "rgba(14,13,11,0.05)"};
755
+ border-color: ${ruleStrong};
675
756
  }
676
- .mushi-report-title {
677
- font-size: 13px;
678
- overflow: hidden;
679
- text-overflow: ellipsis;
680
- white-space: nowrap;
757
+ .mushi-report-when {
758
+ font-family: ${fontMono};
759
+ font-size: 10px;
760
+ color: ${inkFaint};
761
+ }
762
+ .mushi-unread-badge {
763
+ display: inline-flex;
764
+ align-items: center;
765
+ justify-content: center;
766
+ min-width: 18px;
767
+ height: 18px;
768
+ padding: 0 5px;
769
+ border-radius: 999px;
770
+ font-family: ${fontMono};
771
+ font-size: 10px;
772
+ font-weight: 700;
773
+ color: ${widgetAccentInk};
774
+ background: ${widgetAccent};
775
+ }
776
+ .mushi-report-chevron {
777
+ font-size: 18px;
778
+ line-height: 1;
779
+ color: ${inkFaint};
780
+ transition: color 180ms ${easeStamp}, transform 180ms ${easeStamp};
781
+ }
782
+ .mushi-report-row:hover .mushi-report-chevron,
783
+ .mushi-report-row:focus-visible .mushi-report-chevron {
784
+ color: ${widgetAccent};
785
+ transform: translateX(2px);
681
786
  }
682
787
  .mushi-thread-summary {
683
788
  border-bottom: 1px solid ${rule};
684
789
  padding-bottom: 10px;
685
790
  margin-bottom: 10px;
686
791
  }
687
- .mushi-thread-summary span {
688
- font-family: ${fontMono};
689
- font-size: 10px;
690
- color: ${widgetAccent};
691
- text-transform: uppercase;
792
+ .mushi-thread-summary-meta {
793
+ display: flex;
794
+ flex-wrap: wrap;
795
+ align-items: center;
796
+ gap: 10px;
797
+ margin-bottom: 8px;
692
798
  }
693
799
  .mushi-thread {
694
800
  display: grid;
@@ -1685,6 +1791,103 @@ var CATEGORY_ICONS = {
1685
1791
  other: "\u{1F4DD}"
1686
1792
  };
1687
1793
  var FEATURE_REQUEST_INTENT = "Feature request";
1794
+ function reporterStatusShort(status) {
1795
+ switch (status) {
1796
+ case "new":
1797
+ case "queued":
1798
+ case "pending":
1799
+ case "submitted":
1800
+ return "Sent";
1801
+ case "classified":
1802
+ case "triaged":
1803
+ case "grouped":
1804
+ case "dispatched":
1805
+ return "Review";
1806
+ case "fixing":
1807
+ return "Fixing";
1808
+ case "fixed":
1809
+ case "resolved":
1810
+ case "completed":
1811
+ return "Fixed";
1812
+ case "dismissed":
1813
+ return "Closed";
1814
+ default:
1815
+ return status.replace(/_/g, " ").slice(0, 12);
1816
+ }
1817
+ }
1818
+ function reporterStatusLabel(status) {
1819
+ switch (status) {
1820
+ case "new":
1821
+ case "queued":
1822
+ case "pending":
1823
+ case "submitted":
1824
+ return "Submitted";
1825
+ case "classified":
1826
+ case "triaged":
1827
+ case "grouped":
1828
+ case "dispatched":
1829
+ return "In review";
1830
+ case "fixing":
1831
+ return "Fix in progress";
1832
+ case "fixed":
1833
+ case "resolved":
1834
+ case "completed":
1835
+ return "Fixed \u2014 confirm?";
1836
+ case "verified":
1837
+ return "Verified";
1838
+ case "reopened":
1839
+ return "Reopened";
1840
+ case "dismissed":
1841
+ return "Closed";
1842
+ default:
1843
+ return status.replace(/_/g, " ");
1844
+ }
1845
+ }
1846
+ function reporterStatusTone(status) {
1847
+ switch (status) {
1848
+ case "new":
1849
+ case "queued":
1850
+ case "pending":
1851
+ case "submitted":
1852
+ return "sent";
1853
+ case "classified":
1854
+ case "triaged":
1855
+ case "grouped":
1856
+ case "dispatched":
1857
+ return "review";
1858
+ case "fixing":
1859
+ return "fixing";
1860
+ case "fixed":
1861
+ case "resolved":
1862
+ case "completed":
1863
+ return "fixed";
1864
+ case "verified":
1865
+ return "fixed";
1866
+ case "reopened":
1867
+ return "fixing";
1868
+ case "dismissed":
1869
+ return "closed";
1870
+ default:
1871
+ return "unknown";
1872
+ }
1873
+ }
1874
+ function formatRelativeTime(iso) {
1875
+ const then = Date.parse(iso);
1876
+ if (Number.isNaN(then)) return "";
1877
+ const diffMs = Date.now() - then;
1878
+ if (diffMs < 0) return "just now";
1879
+ const sec = Math.floor(diffMs / 1e3);
1880
+ if (sec < 60) return "just now";
1881
+ const min = Math.floor(sec / 60);
1882
+ if (min < 60) return `${min}m ago`;
1883
+ const hr = Math.floor(min / 60);
1884
+ if (hr < 24) return `${hr}h ago`;
1885
+ const day = Math.floor(hr / 24);
1886
+ if (day < 7) return `${day}d ago`;
1887
+ const week = Math.floor(day / 7);
1888
+ if (week < 5) return `${week}w ago`;
1889
+ return new Date(then).toLocaleDateString(void 0, { month: "short", day: "numeric" });
1890
+ }
1688
1891
  function pad2(n) {
1689
1892
  return n < 10 ? `0${n}` : String(n);
1690
1893
  }
@@ -1799,6 +2002,8 @@ var MushiWidget = class _MushiWidget {
1799
2002
  successTimer = null;
1800
2003
  autoCloseTimer = null;
1801
2004
  rewardsState = null;
2005
+ leaderboardEntries = null;
2006
+ leaderboardLoading = false;
1802
2007
  /** Server-confirmed id for the just-submitted report. Surfaces in
1803
2008
  * the success step as a copyable receipt + optional deep link to
1804
2009
  * the Mushi console (when `dashboardUrl` is configured). Cleared
@@ -2071,6 +2276,11 @@ var MushiWidget = class _MushiWidget {
2071
2276
  this.rewardsState = state;
2072
2277
  if (this.isOpen) this.render();
2073
2278
  }
2279
+ setLeaderboard(entries, loading = false) {
2280
+ this.leaderboardEntries = entries;
2281
+ this.leaderboardLoading = loading;
2282
+ if (this.isOpen && this.step === "leaderboard") this.render();
2283
+ }
2074
2284
  destroy() {
2075
2285
  if (this.successTimer !== null) {
2076
2286
  clearTimeout(this.successTimer);
@@ -2491,6 +2701,8 @@ var MushiWidget = class _MushiWidget {
2491
2701
  return this.renderReportsStep();
2492
2702
  case "report-detail":
2493
2703
  return this.renderReportDetailStep();
2704
+ case "leaderboard":
2705
+ return this.renderLeaderboardStep();
2494
2706
  }
2495
2707
  }
2496
2708
  renderOutdatedBanner() {
@@ -2523,7 +2735,7 @@ var MushiWidget = class _MushiWidget {
2523
2735
  renderHeader(opts) {
2524
2736
  const t = this.locale;
2525
2737
  const { title, showBack = false, step, eyebrow } = opts;
2526
- 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>`;
2738
+ 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>`;
2527
2739
  const counterHtml = step ? `<span class="mushi-step-counter" aria-label="Step ${step} of ${TOTAL_STEPS}"><b>${pad2(step)}</b> / ${pad2(TOTAL_STEPS)}</span>` : "";
2528
2740
  return `
2529
2741
  <div class="mushi-header">
@@ -2659,24 +2871,61 @@ var MushiWidget = class _MushiWidget {
2659
2871
  `;
2660
2872
  }
2661
2873
  renderReportsStep() {
2662
- const reports = this.reporterReports.map((report) => `
2663
- <button type="button" class="mushi-report-row" data-report-id="${escapeHtml(report.id)}">
2664
- <span class="mushi-report-status">${escapeHtml(report.status)}</span>
2665
- <span class="mushi-report-title">${escapeHtml(report.summary ?? report.description ?? `Report ${report.id.slice(0, 8)}`)}</span>
2666
- ${report.unread_count ? `<b>${report.unread_count}</b>` : ""}
2667
- </button>
2668
- `).join("");
2874
+ const reports = this.reporterReports.map((report) => {
2875
+ const title = report.summary ?? report.description ?? `Report ${report.id.slice(0, 8)}`;
2876
+ const tone = reporterStatusTone(report.status);
2877
+ const when = formatRelativeTime(report.created_at);
2878
+ const unread = report.unread_count && report.unread_count > 0 ? `<span class="mushi-unread-badge" aria-label="${report.unread_count} unread">${report.unread_count}</span>` : "";
2879
+ return `
2880
+ <button type="button" class="mushi-report-row" data-report-id="${escapeHtml(report.id)}" aria-label="View report: ${escapeHtml(title)}">
2881
+ <div class="mushi-report-main">
2882
+ <span class="mushi-report-title">${escapeHtml(title)}</span>
2883
+ <span class="mushi-report-meta">
2884
+ <span class="mushi-report-status mushi-status-${tone}">${escapeHtml(reporterStatusShort(report.status))}</span>
2885
+ ${when ? `<span class="mushi-report-when">${escapeHtml(when)}</span>` : ""}
2886
+ ${unread}
2887
+ </span>
2888
+ </div>
2889
+ <span class="mushi-report-chevron" aria-hidden="true">\u203A</span>
2890
+ </button>`;
2891
+ }).join("");
2892
+ const leaderboardBtn = this.rewardsState ? `<button type="button" class="mushi-leaderboard-link" data-action="open-leaderboard">\u{1F3C6} Leaderboard</button>` : "";
2669
2893
  return `
2670
2894
  ${this.renderHeader({ title: "Your reports", showBack: true, eyebrow: "Mushi \xB7 Inbox" })}
2671
2895
  <div class="mushi-body">
2672
2896
  ${this.reporterLoading ? '<p class="mushi-muted">Loading reports\u2026</p>' : ""}
2673
2897
  ${this.reporterError ? `<p class="mushi-error-inline">${escapeHtml(this.reporterError)}</p>` : ""}
2674
2898
  ${reports || (!this.reporterLoading ? '<p class="mushi-muted">No reports from this browser yet.</p>' : "")}
2899
+ ${leaderboardBtn}
2900
+ </div>
2901
+ `;
2902
+ }
2903
+ renderLeaderboardStep() {
2904
+ const myRank = this.rewardsState && this.leaderboardEntries ? this.leaderboardEntries.findIndex((e) => e.display_name === "You") + 1 : 0;
2905
+ const rows = (this.leaderboardEntries ?? []).map((e, i) => `
2906
+ <div class="mushi-lb-row ${i === 0 ? "mushi-lb-top" : ""}">
2907
+ <span class="mushi-lb-rank">#${i + 1}</span>
2908
+ <span class="mushi-lb-name">${escapeHtml(e.display_name)}</span>
2909
+ ${e.tier_name ? `<span class="mushi-lb-tier">${escapeHtml(e.tier_name)}</span>` : ""}
2910
+ <span class="mushi-lb-pts">${e.total_points.toLocaleString()} pts</span>
2911
+ </div>
2912
+ `).join("");
2913
+ return `
2914
+ ${this.renderHeader({ title: "\u{1F3C6} Leaderboard", showBack: true, eyebrow: "Mushi \xB7 Contributors" })}
2915
+ <div class="mushi-body">
2916
+ ${this.leaderboardLoading ? '<p class="mushi-muted">Loading leaderboard\u2026</p>' : ""}
2917
+ ${!this.leaderboardLoading && !this.leaderboardEntries?.length ? '<p class="mushi-muted">No contributors yet \u2014 be the first!</p>' : ""}
2918
+ <div class="mushi-lb-list">${rows}</div>
2919
+ ${myRank > 0 ? `<p class="mushi-lb-myrank">You are ranked #${myRank}</p>` : ""}
2920
+ <p class="mushi-lb-note">Top contributors this month \xB7 Points refresh monthly</p>
2675
2921
  </div>
2676
2922
  `;
2677
2923
  }
2678
2924
  renderReportDetailStep() {
2679
2925
  const report = this.reporterReports.find((r) => r.id === this.selectedReportId);
2926
+ const status = report?.status ?? "unknown";
2927
+ const tone = reporterStatusTone(status);
2928
+ const when = report?.created_at ? formatRelativeTime(report.created_at) : "";
2680
2929
  const comments = this.reporterComments.map((comment) => `
2681
2930
  <div class="mushi-thread-comment ${comment.author_kind}">
2682
2931
  <strong>${escapeHtml(comment.author_kind === "reporter" ? "You" : comment.author_name ?? "Developer")}</strong>
@@ -2687,12 +2936,21 @@ var MushiWidget = class _MushiWidget {
2687
2936
  ${this.renderHeader({ title: "Report thread", showBack: true, eyebrow: "Mushi \xB7 Inbox" })}
2688
2937
  <div class="mushi-body">
2689
2938
  <div class="mushi-thread-summary">
2690
- <span>${escapeHtml(report?.status ?? "unknown")}</span>
2939
+ <div class="mushi-thread-summary-meta">
2940
+ <span class="mushi-report-status mushi-status-${tone}">${escapeHtml(reporterStatusLabel(status))}</span>
2941
+ ${when ? `<span class="mushi-report-when">Reported ${escapeHtml(when)}</span>` : ""}
2942
+ </div>
2691
2943
  <p>${escapeHtml(report?.summary ?? report?.description ?? "Report details")}</p>
2692
2944
  </div>
2693
2945
  <div class="mushi-thread">
2694
2946
  ${this.reporterLoading ? '<p class="mushi-muted">Loading thread\u2026</p>' : comments || '<p class="mushi-muted">No developer replies yet.</p>'}
2695
2947
  </div>
2948
+ ${["fixed", "resolved", "verified"].includes(status) ? `
2949
+ <div class="mushi-verify-actions" role="group" aria-label="Fix verification">
2950
+ <button type="button" class="mushi-intent-btn" data-action="reporter-confirms">Yes, fixed for me</button>
2951
+ <button type="button" class="mushi-intent-btn" data-action="reporter-not-fixed">Not fixed yet</button>
2952
+ </div>
2953
+ ` : ""}
2696
2954
  <textarea class="mushi-textarea" data-role="reporter-reply" rows="3" placeholder="Reply to the developer\u2026"></textarea>
2697
2955
  <button type="button" class="mushi-submit" data-action="reporter-reply">
2698
2956
  <span>Reply</span><span class="mushi-submit-arrow" aria-hidden="true">\u2192</span>
@@ -2995,6 +3253,8 @@ var MushiWidget = class _MushiWidget {
2995
3253
  } else if (this.step === "report-detail") {
2996
3254
  this.step = "reports";
2997
3255
  this.selectedReportId = null;
3256
+ } else if (this.step === "leaderboard") {
3257
+ this.step = "reports";
2998
3258
  }
2999
3259
  this.render();
3000
3260
  });
@@ -3014,9 +3274,20 @@ var MushiWidget = class _MushiWidget {
3014
3274
  if (reportId) void this.loadReporterComments(reportId);
3015
3275
  });
3016
3276
  });
3277
+ panel.querySelector('[data-action="open-leaderboard"]')?.addEventListener("click", () => {
3278
+ this.step = "leaderboard";
3279
+ this.callbacks.onLeaderboardOpen?.();
3280
+ this.render();
3281
+ });
3017
3282
  panel.querySelector('[data-action="reporter-reply"]')?.addEventListener("click", () => {
3018
3283
  void this.submitReporterReply(panel);
3019
3284
  });
3285
+ panel.querySelector('[data-action="reporter-confirms"]')?.addEventListener("click", () => {
3286
+ void this.submitReporterFeedback("confirms");
3287
+ });
3288
+ panel.querySelector('[data-action="reporter-not-fixed"]')?.addEventListener("click", () => {
3289
+ void this.submitReporterFeedback("not_fixed");
3290
+ });
3020
3291
  panel.querySelector('[data-action="copy-report-id"]')?.addEventListener("click", (e) => {
3021
3292
  const btn = e.currentTarget;
3022
3293
  const id = btn.dataset.copyId;
@@ -3198,6 +3469,21 @@ var MushiWidget = class _MushiWidget {
3198
3469
  this.render();
3199
3470
  }
3200
3471
  }
3472
+ async submitReporterFeedback(signal) {
3473
+ const reportId = this.selectedReportId;
3474
+ if (!reportId || this.reporterLoading) return;
3475
+ this.reporterLoading = true;
3476
+ this.render();
3477
+ try {
3478
+ await this.callbacks.onReporterFeedback?.(reportId, signal);
3479
+ await this.loadReporterReports();
3480
+ if (reportId) await this.loadReporterComments(reportId);
3481
+ } catch (err) {
3482
+ this.reporterError = err instanceof Error ? err.message : "Could not send feedback.";
3483
+ this.reporterLoading = false;
3484
+ this.render();
3485
+ }
3486
+ }
3201
3487
  async submitReporterReply(panel) {
3202
3488
  const reportId = this.selectedReportId;
3203
3489
  const textarea = panel.querySelector('[data-role="reporter-reply"]');
@@ -3265,6 +3551,10 @@ var MushiWidget = class _MushiWidget {
3265
3551
  const submit = this.shadow.querySelector('[data-action="submit"]');
3266
3552
  submit?.click();
3267
3553
  }
3554
+ recorderOpenMyReports() {
3555
+ if (!this.isOpen) this.open();
3556
+ void this.loadReporterReports();
3557
+ }
3268
3558
  };
3269
3559
 
3270
3560
  // src/marketing-recorder.ts
@@ -3287,7 +3577,8 @@ function exposeMarketingRecorder(widget) {
3287
3577
  selectCategory: (category) => widget.recorderSelectCategory(category),
3288
3578
  selectIntent: (label) => widget.recorderSelectIntent(label),
3289
3579
  focusDescription: () => widget.recorderFocusDescription(),
3290
- submit: () => widget.recorderSubmit()
3580
+ submit: () => widget.recorderSubmit(),
3581
+ openMyReports: () => widget.recorderOpenMyReports()
3291
3582
  };
3292
3583
  globalThis.__mushiRecorder = api;
3293
3584
  }
@@ -3458,6 +3749,24 @@ async function fetchAndCacheTier(userId) {
3458
3749
  noteRewardsApiFailure(res.error?.code);
3459
3750
  return null;
3460
3751
  }
3752
+ async function fetchLeaderboard(limit = 10) {
3753
+ if (!apiClient || isRewardsApiBackedOff()) return null;
3754
+ try {
3755
+ const res = await apiClient.getHallOfFame(limit);
3756
+ if (res.ok && res.data) {
3757
+ return (res.data.data ?? []).map((e) => ({
3758
+ display_name: e.display_name,
3759
+ tier_name: e.tier_name,
3760
+ total_points: e.total_points,
3761
+ points_30d: e.points_30d
3762
+ }));
3763
+ }
3764
+ noteRewardsApiFailure(res.error?.code);
3765
+ return null;
3766
+ } catch {
3767
+ return null;
3768
+ }
3769
+ }
3461
3770
  var routeObserver = null;
3462
3771
  var clickHandler = null;
3463
3772
  var origPushState = null;
@@ -5028,7 +5337,7 @@ function createProactiveManager(config = {}) {
5028
5337
 
5029
5338
  // src/version.ts
5030
5339
  var MUSHI_SDK_PACKAGE = "@mushi-mushi/web";
5031
- var MUSHI_SDK_VERSION = "1.9.0" ;
5340
+ var MUSHI_SDK_VERSION = "1.11.0" ;
5032
5341
 
5033
5342
  // src/mushi.ts
5034
5343
  var instance = null;
@@ -5306,6 +5615,22 @@ function createInstance(config) {
5306
5615
  async onReporterReply(reportId, body) {
5307
5616
  const result = await apiClient2.replyToReporterReport(reportId, getReporterToken(), body);
5308
5617
  if (!result.ok) throw new Error(result.error?.message ?? "Could not send reply");
5618
+ },
5619
+ async onReporterFeedback(reportId, signal, note) {
5620
+ const result = await apiClient2.replyToReporterReport(reportId, getReporterToken(), note ?? "", signal);
5621
+ if (!result.ok) throw new Error(result.error?.message ?? "Could not send feedback");
5622
+ return result.data?.feedback ?? null;
5623
+ },
5624
+ async onReporterReopen(reportId, note) {
5625
+ const result = await apiClient2.reopenReporterReport(reportId, getReporterToken(), note);
5626
+ if (!result.ok) throw new Error(result.error?.message ?? "Could not reopen report");
5627
+ return result.data?.outcome ?? null;
5628
+ },
5629
+ onLeaderboardOpen() {
5630
+ widget.setLeaderboard(null, true);
5631
+ void fetchLeaderboard(10).then((entries) => {
5632
+ widget.setLeaderboard(entries, false);
5633
+ });
5309
5634
  }
5310
5635
  }, MUSHI_SDK_VERSION);
5311
5636
  syncCaptureModules();
@@ -5562,7 +5887,7 @@ function createInstance(config) {
5562
5887
  await offlineQueue.enqueue(finalReport);
5563
5888
  log.info("Offline \u2014 report queued", { reportId: finalReport.id });
5564
5889
  emit("report:queued", { reportId: finalReport.id });
5565
- return;
5890
+ return { reportId: null, queuedOffline: true };
5566
5891
  }
5567
5892
  const result = await apiClient2.submitReport(finalReport);
5568
5893
  if (result.ok) {
@@ -5860,6 +6185,41 @@ function createInstance(config) {
5860
6185
  },
5861
6186
  pulseTrigger() {
5862
6187
  widget.pulseTrigger?.();
6188
+ },
6189
+ // ─── Reporter API (cross-platform) ────────────────────────────────
6190
+ async listMyReports() {
6191
+ const result = await apiClient2.listReporterReports(getReporterToken());
6192
+ if (!result.ok) return [];
6193
+ return result.data?.reports ?? [];
6194
+ },
6195
+ async listMyComments(reportId) {
6196
+ const result = await apiClient2.listReporterComments(reportId, getReporterToken());
6197
+ if (!result.ok) return [];
6198
+ return result.data?.comments ?? [];
6199
+ },
6200
+ async replyToReport(reportId, body) {
6201
+ const result = await apiClient2.replyToReporterReport(reportId, getReporterToken(), body);
6202
+ if (!result.ok) return null;
6203
+ return result.data?.comment ?? null;
6204
+ },
6205
+ async submitFeedbackSignal(reportId, signal, note) {
6206
+ const result = await apiClient2.replyToReporterReport(reportId, getReporterToken(), note ?? "", signal);
6207
+ if (!result.ok) return null;
6208
+ return result.data?.feedback ?? null;
6209
+ },
6210
+ async reopenReport(reportId, note) {
6211
+ const result = await apiClient2.reopenReporterReport(reportId, getReporterToken(), note);
6212
+ if (!result.ok) return null;
6213
+ return result.data?.outcome ?? null;
6214
+ },
6215
+ openMyReports() {
6216
+ widget.recorderOpenMyReports();
6217
+ },
6218
+ async getHallOfFame(limit = 20) {
6219
+ const result = await apiClient2.getHallOfFame(limit);
6220
+ if (!result.ok) return [];
6221
+ const raw = result.data;
6222
+ return raw?.data ?? [];
5863
6223
  }
5864
6224
  };
5865
6225
  if (typeof globalThis !== "undefined" && (bootstrapConfig.debug ?? false)) {
@@ -6177,7 +6537,15 @@ function createNoopInstance() {
6177
6537
  recordActivity: () => {
6178
6538
  },
6179
6539
  pulseTrigger: () => {
6180
- }
6540
+ },
6541
+ listMyReports: async () => [],
6542
+ listMyComments: async () => [],
6543
+ replyToReport: async () => null,
6544
+ submitFeedbackSignal: async () => null,
6545
+ reopenReport: async () => null,
6546
+ openMyReports: () => {
6547
+ },
6548
+ getHallOfFame: async () => []
6181
6549
  };
6182
6550
  }
6183
6551
  function installAutoBreadcrumbs(buffer) {