@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.d.cts CHANGED
@@ -21,7 +21,7 @@ declare class Mushi {
21
21
  * user-facing ARIA / keyboard wiring.
22
22
  */
23
23
 
24
- type WidgetStep = 'category' | 'intent' | 'details' | 'success' | 'reports' | 'report-detail';
24
+ type WidgetStep = 'category' | 'intent' | 'details' | 'success' | 'reports' | 'report-detail' | 'leaderboard';
25
25
  interface WidgetRewardsState {
26
26
  tier: {
27
27
  slug: string;
@@ -65,6 +65,7 @@ interface WidgetCallbacks {
65
65
  onReporterReportsRequest?(): Promise<MushiReporterReport[]>;
66
66
  onReporterCommentsRequest?(reportId: string): Promise<MushiReporterComment[]>;
67
67
  onReporterReply?(reportId: string, body: string): Promise<void>;
68
+ onLeaderboardOpen?(): void;
68
69
  }
69
70
  declare class MushiWidget {
70
71
  private readonly sdkVersion;
@@ -121,6 +122,8 @@ declare class MushiWidget {
121
122
  private successTimer;
122
123
  private autoCloseTimer;
123
124
  private rewardsState;
125
+ private leaderboardEntries;
126
+ private leaderboardLoading;
124
127
  /** Server-confirmed id for the just-submitted report. Surfaces in
125
128
  * the success step as a copyable receipt + optional deep link to
126
129
  * the Mushi console (when `dashboardUrl` is configured). Cleared
@@ -178,6 +181,12 @@ declare class MushiWidget {
178
181
  message?: string | null;
179
182
  }): void;
180
183
  setRewardsState(state: WidgetRewardsState | null): void;
184
+ setLeaderboard(entries: Array<{
185
+ display_name: string;
186
+ tier_name: string | null;
187
+ total_points: number;
188
+ points_30d: number;
189
+ }> | null, loading?: boolean): void;
181
190
  destroy(): void;
182
191
  /**
183
192
  * Apply the SDK-owned pass-through layout to the host element so it is
@@ -277,6 +286,7 @@ declare class MushiWidget {
277
286
  */
278
287
  private renderBetaStrip;
279
288
  private renderReportsStep;
289
+ private renderLeaderboardStep;
280
290
  private renderReportDetailStep;
281
291
  private renderIntentStep;
282
292
  private effectiveMinLength;
@@ -331,6 +341,7 @@ declare class MushiWidget {
331
341
  recorderSelectIntent(label: string): void;
332
342
  recorderFocusDescription(): void;
333
343
  recorderSubmit(): void;
344
+ recorderOpenMyReports(): void;
334
345
  }
335
346
 
336
347
  interface ConsoleCapture {
package/dist/index.d.ts CHANGED
@@ -21,7 +21,7 @@ declare class Mushi {
21
21
  * user-facing ARIA / keyboard wiring.
22
22
  */
23
23
 
24
- type WidgetStep = 'category' | 'intent' | 'details' | 'success' | 'reports' | 'report-detail';
24
+ type WidgetStep = 'category' | 'intent' | 'details' | 'success' | 'reports' | 'report-detail' | 'leaderboard';
25
25
  interface WidgetRewardsState {
26
26
  tier: {
27
27
  slug: string;
@@ -65,6 +65,7 @@ interface WidgetCallbacks {
65
65
  onReporterReportsRequest?(): Promise<MushiReporterReport[]>;
66
66
  onReporterCommentsRequest?(reportId: string): Promise<MushiReporterComment[]>;
67
67
  onReporterReply?(reportId: string, body: string): Promise<void>;
68
+ onLeaderboardOpen?(): void;
68
69
  }
69
70
  declare class MushiWidget {
70
71
  private readonly sdkVersion;
@@ -121,6 +122,8 @@ declare class MushiWidget {
121
122
  private successTimer;
122
123
  private autoCloseTimer;
123
124
  private rewardsState;
125
+ private leaderboardEntries;
126
+ private leaderboardLoading;
124
127
  /** Server-confirmed id for the just-submitted report. Surfaces in
125
128
  * the success step as a copyable receipt + optional deep link to
126
129
  * the Mushi console (when `dashboardUrl` is configured). Cleared
@@ -178,6 +181,12 @@ declare class MushiWidget {
178
181
  message?: string | null;
179
182
  }): void;
180
183
  setRewardsState(state: WidgetRewardsState | null): void;
184
+ setLeaderboard(entries: Array<{
185
+ display_name: string;
186
+ tier_name: string | null;
187
+ total_points: number;
188
+ points_30d: number;
189
+ }> | null, loading?: boolean): void;
181
190
  destroy(): void;
182
191
  /**
183
192
  * Apply the SDK-owned pass-through layout to the host element so it is
@@ -277,6 +286,7 @@ declare class MushiWidget {
277
286
  */
278
287
  private renderBetaStrip;
279
288
  private renderReportsStep;
289
+ private renderLeaderboardStep;
280
290
  private renderReportDetailStep;
281
291
  private renderIntentStep;
282
292
  private effectiveMinLength;
@@ -331,6 +341,7 @@ declare class MushiWidget {
331
341
  recorderSelectIntent(label: string): void;
332
342
  recorderFocusDescription(): void;
333
343
  recorderSubmit(): void;
344
+ recorderOpenMyReports(): void;
334
345
  }
335
346
 
336
347
  interface ConsoleCapture {
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,95 @@ 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";
1836
+ case "dismissed":
1837
+ return "Closed";
1838
+ default:
1839
+ return status.replace(/_/g, " ");
1840
+ }
1841
+ }
1842
+ function reporterStatusTone(status) {
1843
+ switch (status) {
1844
+ case "new":
1845
+ case "queued":
1846
+ case "pending":
1847
+ case "submitted":
1848
+ return "sent";
1849
+ case "classified":
1850
+ case "triaged":
1851
+ case "grouped":
1852
+ case "dispatched":
1853
+ return "review";
1854
+ case "fixing":
1855
+ return "fixing";
1856
+ case "fixed":
1857
+ case "resolved":
1858
+ case "completed":
1859
+ return "fixed";
1860
+ case "dismissed":
1861
+ return "closed";
1862
+ default:
1863
+ return "unknown";
1864
+ }
1865
+ }
1866
+ function formatRelativeTime(iso) {
1867
+ const then = Date.parse(iso);
1868
+ if (Number.isNaN(then)) return "";
1869
+ const diffMs = Date.now() - then;
1870
+ if (diffMs < 0) return "just now";
1871
+ const sec = Math.floor(diffMs / 1e3);
1872
+ if (sec < 60) return "just now";
1873
+ const min = Math.floor(sec / 60);
1874
+ if (min < 60) return `${min}m ago`;
1875
+ const hr = Math.floor(min / 60);
1876
+ if (hr < 24) return `${hr}h ago`;
1877
+ const day = Math.floor(hr / 24);
1878
+ if (day < 7) return `${day}d ago`;
1879
+ const week = Math.floor(day / 7);
1880
+ if (week < 5) return `${week}w ago`;
1881
+ return new Date(then).toLocaleDateString(void 0, { month: "short", day: "numeric" });
1882
+ }
1688
1883
  function pad2(n) {
1689
1884
  return n < 10 ? `0${n}` : String(n);
1690
1885
  }
@@ -1799,6 +1994,8 @@ var MushiWidget = class _MushiWidget {
1799
1994
  successTimer = null;
1800
1995
  autoCloseTimer = null;
1801
1996
  rewardsState = null;
1997
+ leaderboardEntries = null;
1998
+ leaderboardLoading = false;
1802
1999
  /** Server-confirmed id for the just-submitted report. Surfaces in
1803
2000
  * the success step as a copyable receipt + optional deep link to
1804
2001
  * the Mushi console (when `dashboardUrl` is configured). Cleared
@@ -2071,6 +2268,11 @@ var MushiWidget = class _MushiWidget {
2071
2268
  this.rewardsState = state;
2072
2269
  if (this.isOpen) this.render();
2073
2270
  }
2271
+ setLeaderboard(entries, loading = false) {
2272
+ this.leaderboardEntries = entries;
2273
+ this.leaderboardLoading = loading;
2274
+ if (this.isOpen && this.step === "leaderboard") this.render();
2275
+ }
2074
2276
  destroy() {
2075
2277
  if (this.successTimer !== null) {
2076
2278
  clearTimeout(this.successTimer);
@@ -2491,6 +2693,8 @@ var MushiWidget = class _MushiWidget {
2491
2693
  return this.renderReportsStep();
2492
2694
  case "report-detail":
2493
2695
  return this.renderReportDetailStep();
2696
+ case "leaderboard":
2697
+ return this.renderLeaderboardStep();
2494
2698
  }
2495
2699
  }
2496
2700
  renderOutdatedBanner() {
@@ -2523,7 +2727,7 @@ var MushiWidget = class _MushiWidget {
2523
2727
  renderHeader(opts) {
2524
2728
  const t = this.locale;
2525
2729
  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>`;
2730
+ 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
2731
  const counterHtml = step ? `<span class="mushi-step-counter" aria-label="Step ${step} of ${TOTAL_STEPS}"><b>${pad2(step)}</b> / ${pad2(TOTAL_STEPS)}</span>` : "";
2528
2732
  return `
2529
2733
  <div class="mushi-header">
@@ -2659,24 +2863,61 @@ var MushiWidget = class _MushiWidget {
2659
2863
  `;
2660
2864
  }
2661
2865
  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("");
2866
+ const reports = this.reporterReports.map((report) => {
2867
+ const title = report.summary ?? report.description ?? `Report ${report.id.slice(0, 8)}`;
2868
+ const tone = reporterStatusTone(report.status);
2869
+ const when = formatRelativeTime(report.created_at);
2870
+ const unread = report.unread_count && report.unread_count > 0 ? `<span class="mushi-unread-badge" aria-label="${report.unread_count} unread">${report.unread_count}</span>` : "";
2871
+ return `
2872
+ <button type="button" class="mushi-report-row" data-report-id="${escapeHtml(report.id)}" aria-label="View report: ${escapeHtml(title)}">
2873
+ <div class="mushi-report-main">
2874
+ <span class="mushi-report-title">${escapeHtml(title)}</span>
2875
+ <span class="mushi-report-meta">
2876
+ <span class="mushi-report-status mushi-status-${tone}">${escapeHtml(reporterStatusShort(report.status))}</span>
2877
+ ${when ? `<span class="mushi-report-when">${escapeHtml(when)}</span>` : ""}
2878
+ ${unread}
2879
+ </span>
2880
+ </div>
2881
+ <span class="mushi-report-chevron" aria-hidden="true">\u203A</span>
2882
+ </button>`;
2883
+ }).join("");
2884
+ const leaderboardBtn = this.rewardsState ? `<button type="button" class="mushi-leaderboard-link" data-action="open-leaderboard">\u{1F3C6} Leaderboard</button>` : "";
2669
2885
  return `
2670
2886
  ${this.renderHeader({ title: "Your reports", showBack: true, eyebrow: "Mushi \xB7 Inbox" })}
2671
2887
  <div class="mushi-body">
2672
2888
  ${this.reporterLoading ? '<p class="mushi-muted">Loading reports\u2026</p>' : ""}
2673
2889
  ${this.reporterError ? `<p class="mushi-error-inline">${escapeHtml(this.reporterError)}</p>` : ""}
2674
2890
  ${reports || (!this.reporterLoading ? '<p class="mushi-muted">No reports from this browser yet.</p>' : "")}
2891
+ ${leaderboardBtn}
2892
+ </div>
2893
+ `;
2894
+ }
2895
+ renderLeaderboardStep() {
2896
+ const myRank = this.rewardsState && this.leaderboardEntries ? this.leaderboardEntries.findIndex((e) => e.display_name === "You") + 1 : 0;
2897
+ const rows = (this.leaderboardEntries ?? []).map((e, i) => `
2898
+ <div class="mushi-lb-row ${i === 0 ? "mushi-lb-top" : ""}">
2899
+ <span class="mushi-lb-rank">#${i + 1}</span>
2900
+ <span class="mushi-lb-name">${escapeHtml(e.display_name)}</span>
2901
+ ${e.tier_name ? `<span class="mushi-lb-tier">${escapeHtml(e.tier_name)}</span>` : ""}
2902
+ <span class="mushi-lb-pts">${e.total_points.toLocaleString()} pts</span>
2903
+ </div>
2904
+ `).join("");
2905
+ return `
2906
+ ${this.renderHeader({ title: "\u{1F3C6} Leaderboard", showBack: true, eyebrow: "Mushi \xB7 Contributors" })}
2907
+ <div class="mushi-body">
2908
+ ${this.leaderboardLoading ? '<p class="mushi-muted">Loading leaderboard\u2026</p>' : ""}
2909
+ ${!this.leaderboardLoading && !this.leaderboardEntries?.length ? '<p class="mushi-muted">No contributors yet \u2014 be the first!</p>' : ""}
2910
+ <div class="mushi-lb-list">${rows}</div>
2911
+ ${myRank > 0 ? `<p class="mushi-lb-myrank">You are ranked #${myRank}</p>` : ""}
2912
+ <p class="mushi-lb-note">Top contributors this month \xB7 Points refresh monthly</p>
2675
2913
  </div>
2676
2914
  `;
2677
2915
  }
2678
2916
  renderReportDetailStep() {
2679
2917
  const report = this.reporterReports.find((r) => r.id === this.selectedReportId);
2918
+ const status = report?.status ?? "unknown";
2919
+ const tone = reporterStatusTone(status);
2920
+ const when = report?.created_at ? formatRelativeTime(report.created_at) : "";
2680
2921
  const comments = this.reporterComments.map((comment) => `
2681
2922
  <div class="mushi-thread-comment ${comment.author_kind}">
2682
2923
  <strong>${escapeHtml(comment.author_kind === "reporter" ? "You" : comment.author_name ?? "Developer")}</strong>
@@ -2687,7 +2928,10 @@ var MushiWidget = class _MushiWidget {
2687
2928
  ${this.renderHeader({ title: "Report thread", showBack: true, eyebrow: "Mushi \xB7 Inbox" })}
2688
2929
  <div class="mushi-body">
2689
2930
  <div class="mushi-thread-summary">
2690
- <span>${escapeHtml(report?.status ?? "unknown")}</span>
2931
+ <div class="mushi-thread-summary-meta">
2932
+ <span class="mushi-report-status mushi-status-${tone}">${escapeHtml(reporterStatusLabel(status))}</span>
2933
+ ${when ? `<span class="mushi-report-when">Reported ${escapeHtml(when)}</span>` : ""}
2934
+ </div>
2691
2935
  <p>${escapeHtml(report?.summary ?? report?.description ?? "Report details")}</p>
2692
2936
  </div>
2693
2937
  <div class="mushi-thread">
@@ -2995,6 +3239,8 @@ var MushiWidget = class _MushiWidget {
2995
3239
  } else if (this.step === "report-detail") {
2996
3240
  this.step = "reports";
2997
3241
  this.selectedReportId = null;
3242
+ } else if (this.step === "leaderboard") {
3243
+ this.step = "reports";
2998
3244
  }
2999
3245
  this.render();
3000
3246
  });
@@ -3014,6 +3260,11 @@ var MushiWidget = class _MushiWidget {
3014
3260
  if (reportId) void this.loadReporterComments(reportId);
3015
3261
  });
3016
3262
  });
3263
+ panel.querySelector('[data-action="open-leaderboard"]')?.addEventListener("click", () => {
3264
+ this.step = "leaderboard";
3265
+ this.callbacks.onLeaderboardOpen?.();
3266
+ this.render();
3267
+ });
3017
3268
  panel.querySelector('[data-action="reporter-reply"]')?.addEventListener("click", () => {
3018
3269
  void this.submitReporterReply(panel);
3019
3270
  });
@@ -3265,6 +3516,10 @@ var MushiWidget = class _MushiWidget {
3265
3516
  const submit = this.shadow.querySelector('[data-action="submit"]');
3266
3517
  submit?.click();
3267
3518
  }
3519
+ recorderOpenMyReports() {
3520
+ if (!this.isOpen) this.open();
3521
+ void this.loadReporterReports();
3522
+ }
3268
3523
  };
3269
3524
 
3270
3525
  // src/marketing-recorder.ts
@@ -3287,7 +3542,8 @@ function exposeMarketingRecorder(widget) {
3287
3542
  selectCategory: (category) => widget.recorderSelectCategory(category),
3288
3543
  selectIntent: (label) => widget.recorderSelectIntent(label),
3289
3544
  focusDescription: () => widget.recorderFocusDescription(),
3290
- submit: () => widget.recorderSubmit()
3545
+ submit: () => widget.recorderSubmit(),
3546
+ openMyReports: () => widget.recorderOpenMyReports()
3291
3547
  };
3292
3548
  globalThis.__mushiRecorder = api;
3293
3549
  }
@@ -3458,6 +3714,24 @@ async function fetchAndCacheTier(userId) {
3458
3714
  noteRewardsApiFailure(res.error?.code);
3459
3715
  return null;
3460
3716
  }
3717
+ async function fetchLeaderboard(limit = 10) {
3718
+ if (!apiClient || isRewardsApiBackedOff()) return null;
3719
+ try {
3720
+ const res = await apiClient.getHallOfFame(limit);
3721
+ if (res.ok && res.data) {
3722
+ return (res.data.data ?? []).map((e) => ({
3723
+ display_name: e.display_name,
3724
+ tier_name: e.tier_name,
3725
+ total_points: e.total_points,
3726
+ points_30d: e.points_30d
3727
+ }));
3728
+ }
3729
+ noteRewardsApiFailure(res.error?.code);
3730
+ return null;
3731
+ } catch {
3732
+ return null;
3733
+ }
3734
+ }
3461
3735
  var routeObserver = null;
3462
3736
  var clickHandler = null;
3463
3737
  var origPushState = null;
@@ -5028,7 +5302,7 @@ function createProactiveManager(config = {}) {
5028
5302
 
5029
5303
  // src/version.ts
5030
5304
  var MUSHI_SDK_PACKAGE = "@mushi-mushi/web";
5031
- var MUSHI_SDK_VERSION = "1.9.0" ;
5305
+ var MUSHI_SDK_VERSION = "1.10.0" ;
5032
5306
 
5033
5307
  // src/mushi.ts
5034
5308
  var instance = null;
@@ -5306,6 +5580,12 @@ function createInstance(config) {
5306
5580
  async onReporterReply(reportId, body) {
5307
5581
  const result = await apiClient2.replyToReporterReport(reportId, getReporterToken(), body);
5308
5582
  if (!result.ok) throw new Error(result.error?.message ?? "Could not send reply");
5583
+ },
5584
+ onLeaderboardOpen() {
5585
+ widget.setLeaderboard(null, true);
5586
+ void fetchLeaderboard(10).then((entries) => {
5587
+ widget.setLeaderboard(entries, false);
5588
+ });
5309
5589
  }
5310
5590
  }, MUSHI_SDK_VERSION);
5311
5591
  syncCaptureModules();
@@ -5562,7 +5842,7 @@ function createInstance(config) {
5562
5842
  await offlineQueue.enqueue(finalReport);
5563
5843
  log.info("Offline \u2014 report queued", { reportId: finalReport.id });
5564
5844
  emit("report:queued", { reportId: finalReport.id });
5565
- return;
5845
+ return { reportId: null, queuedOffline: true };
5566
5846
  }
5567
5847
  const result = await apiClient2.submitReport(finalReport);
5568
5848
  if (result.ok) {
@@ -5860,6 +6140,28 @@ function createInstance(config) {
5860
6140
  },
5861
6141
  pulseTrigger() {
5862
6142
  widget.pulseTrigger?.();
6143
+ },
6144
+ // ─── Reporter API (cross-platform) ────────────────────────────────
6145
+ async listMyReports() {
6146
+ const result = await apiClient2.listReporterReports(getReporterToken());
6147
+ if (!result.ok) return [];
6148
+ return result.data?.reports ?? [];
6149
+ },
6150
+ async listMyComments(reportId) {
6151
+ const result = await apiClient2.listReporterComments(reportId, getReporterToken());
6152
+ if (!result.ok) return [];
6153
+ return result.data?.comments ?? [];
6154
+ },
6155
+ async replyToReport(reportId, body) {
6156
+ const result = await apiClient2.replyToReporterReport(reportId, getReporterToken(), body);
6157
+ if (!result.ok) return null;
6158
+ return result.data?.comment ?? null;
6159
+ },
6160
+ async getHallOfFame(limit = 20) {
6161
+ const result = await apiClient2.getHallOfFame(limit);
6162
+ if (!result.ok) return [];
6163
+ const raw = result.data;
6164
+ return raw?.data ?? [];
5863
6165
  }
5864
6166
  };
5865
6167
  if (typeof globalThis !== "undefined" && (bootstrapConfig.debug ?? false)) {
@@ -6177,7 +6479,11 @@ function createNoopInstance() {
6177
6479
  recordActivity: () => {
6178
6480
  },
6179
6481
  pulseTrigger: () => {
6180
- }
6482
+ },
6483
+ listMyReports: async () => [],
6484
+ listMyComments: async () => [],
6485
+ replyToReport: async () => null,
6486
+ getHallOfFame: async () => []
6181
6487
  };
6182
6488
  }
6183
6489
  function installAutoBreadcrumbs(buffer) {