@mushi-mushi/react-native 0.21.0 → 0.23.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
@@ -49,7 +49,8 @@ module.exports = __toCommonJS(index_exports);
49
49
 
50
50
  // src/provider.tsx
51
51
  var import_react4 = require("react");
52
- var import_core6 = require("@mushi-mushi/core");
52
+ var import_react_native5 = require("react-native");
53
+ var import_core9 = require("@mushi-mushi/core");
53
54
 
54
55
  // src/capture/console-capture.ts
55
56
  var import_core = require("@mushi-mushi/core");
@@ -477,32 +478,99 @@ function sleep(ms) {
477
478
  // src/components/MushiBottomSheet.tsx
478
479
  var import_react = require("react");
479
480
  var import_react_native2 = require("react-native");
480
- var import_core3 = require("@mushi-mushi/core");
481
+ var import_core4 = require("@mushi-mushi/core");
482
+ var import_reporter_ui = require("@mushi-mushi/core/reporter-ui");
481
483
  var import_i18n = require("@mushi-mushi/web/i18n");
482
484
 
483
- // src/reporter-status.ts
484
- function reporterStatusShort(status) {
485
- switch (status) {
486
- case "new":
487
- case "queued":
488
- case "pending":
489
- case "submitted":
490
- return "Received";
491
- case "classified":
492
- case "triaged":
493
- case "grouped":
494
- case "dispatched":
495
- return "In review";
496
- case "fixing":
497
- return "Fixing";
498
- case "fixed":
499
- case "resolved":
500
- case "completed":
501
- return "Fixed";
502
- case "dismissed":
503
- return "Closed";
504
- default:
505
- return status.replace(/_/g, " ").slice(0, 16);
485
+ // src/theme.ts
486
+ var import_core3 = require("@mushi-mushi/core");
487
+ function hexToRgb(hex) {
488
+ const m = /^#([0-9a-f]{3}|[0-9a-f]{6})([0-9a-f]{2})?$/i.exec(hex.trim());
489
+ if (!m) return null;
490
+ const h = m[1].length === 3 ? m[1].split("").map((c) => c + c).join("") : m[1];
491
+ return [0, 2, 4].map((i) => parseInt(h.slice(i, i + 2), 16));
492
+ }
493
+ function luminance([r, g, b]) {
494
+ const lin = (c) => {
495
+ const s4 = c / 255;
496
+ return s4 <= 0.03928 ? s4 / 12.92 : ((s4 + 0.055) / 1.055) ** 2.4;
497
+ };
498
+ return 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b);
499
+ }
500
+ function contrastingText(bg, fallback) {
501
+ const rgb = hexToRgb(bg);
502
+ if (!rgb) return fallback;
503
+ const l = luminance(rgb);
504
+ return 1.05 / (l + 0.05) >= (l + 0.05) / 0.05 ? "#FFFFFF" : "#000000";
505
+ }
506
+ function resolveRNTheme(dark, override = {}) {
507
+ const pal = (0, import_core3.mushiPalette)(dark ? "dark" : "light");
508
+ const base = {
509
+ bg: dark ? pal.paper : pal.paperRaised,
510
+ fg: pal.ink,
511
+ muted: pal.inkMuted,
512
+ surface: dark ? pal.paperRaised : pal.paper,
513
+ border: pal.ruleStrong,
514
+ accent: pal.ink,
515
+ accentFg: dark ? pal.paper : pal.paperRaised,
516
+ success: pal.ok,
517
+ error: pal.danger,
518
+ fontFamily: void 0,
519
+ radius: import_core3.MUSHI_RADIUS.card
520
+ };
521
+ const defined = Object.fromEntries(
522
+ Object.entries(override).filter(([, v]) => v !== void 0 && v !== null && v !== "")
523
+ );
524
+ const merged = { ...base, ...defined };
525
+ if (defined.accent && !defined.accentFg) merged.accentFg = contrastingText(defined.accent, base.accentFg);
526
+ return {
527
+ ...merged,
528
+ backdrop: dark ? "rgba(0,0,0,0.6)" : "rgba(0,0,0,0.35)",
529
+ disabled: dark ? import_core3.MUSHI_CONTROL_DISABLED.dark : import_core3.MUSHI_CONTROL_DISABLED.light,
530
+ disabledFg: pal.inkFaint
531
+ };
532
+ }
533
+
534
+ // src/reporter-thread.ts
535
+ var THREAD_LOAD_TIMEOUT_MS = 12e3;
536
+ function settleWithin(p, ms) {
537
+ return new Promise((resolve) => {
538
+ const timer = setTimeout(() => resolve(null), ms);
539
+ p.then(
540
+ (value) => {
541
+ clearTimeout(timer);
542
+ resolve(value);
543
+ },
544
+ () => {
545
+ clearTimeout(timer);
546
+ resolve(null);
547
+ }
548
+ );
549
+ });
550
+ }
551
+ function loadReporterThread(load, reportId, timeoutMs = THREAD_LOAD_TIMEOUT_MS) {
552
+ let started;
553
+ try {
554
+ started = load(reportId);
555
+ } catch {
556
+ return Promise.resolve(null);
557
+ }
558
+ return settleWithin(started, timeoutMs);
559
+ }
560
+ async function markReporterReportRead(client, reporterToken, reportId) {
561
+ try {
562
+ const res = await client.listNotifications(reporterToken, { limit: 100 });
563
+ if (!res.ok) return 0;
564
+ const rows = res.data?.notifications ?? [];
565
+ const ids = rows.filter((n) => !n.read_at && n.payload?.reportId === reportId).map((n) => String(n.id));
566
+ let marked = 0;
567
+ for (const id of ids) {
568
+ const r = await client.markNotificationRead(id, reporterToken);
569
+ if (r.ok) marked++;
570
+ }
571
+ return marked;
572
+ } catch {
573
+ return 0;
506
574
  }
507
575
  }
508
576
 
@@ -511,6 +579,7 @@ var import_jsx_runtime = require("react/jsx-runtime");
511
579
  var { height: SCREEN_HEIGHT } = import_react_native2.Dimensions.get("window");
512
580
  var SHEET_HEIGHT = SCREEN_HEIGHT * 0.55;
513
581
  var DISMISS_THRESHOLD = 80;
582
+ var MIN_DESCRIPTION = 20;
514
583
  var CATEGORY_KEYS = ["bug", "slow", "visual", "confusing", "other"];
515
584
  var CATEGORY_EMOJI = {
516
585
  bug: "\u{1F41B}",
@@ -519,7 +588,6 @@ var CATEGORY_EMOJI = {
519
588
  confusing: "\u{1F615}",
520
589
  other: "\u{1F4AC}"
521
590
  };
522
- var VERIFIABLE_STATUSES = /* @__PURE__ */ new Set(["fixed", "resolved", "verified"]);
523
591
  var MushiBottomSheet = ({
524
592
  visible,
525
593
  onClose,
@@ -528,13 +596,18 @@ var MushiBottomSheet = ({
528
596
  onClearScreenshot,
529
597
  screenshotSensitiveHint,
530
598
  assistantEnabled = false,
531
- assistantLabel = import_core3.MUSHI_COPY.assistantTab,
599
+ assistantLabel = import_core4.MUSHI_COPY.assistantTab,
532
600
  assistantGreeting,
533
601
  assistantSuggestions = [],
534
- inboxPollIntervalMs = 0
602
+ inboxPollIntervalMs = 0,
603
+ theme
535
604
  }) => {
536
605
  const mushi = useMushiContext();
537
606
  const t = (0, import_i18n.getLocale)();
607
+ const locale = (0, import_reporter_ui.resolveReporterLocale)(
608
+ typeof navigator !== "undefined" ? navigator.language : void 0
609
+ );
610
+ const rc = (0, import_reporter_ui.reporterCopy)(locale);
538
611
  const greeting = assistantGreeting ?? t.assistant.defaultGreeting;
539
612
  const scheme = (0, import_react_native2.useColorScheme)();
540
613
  const dark = scheme === "dark";
@@ -546,26 +619,37 @@ var MushiBottomSheet = ({
546
619
  const [submitError, setSubmitError] = (0, import_react.useState)(null);
547
620
  const [sheetTab, setSheetTab] = (0, import_react.useState)("report");
548
621
  const [inboxReports, setInboxReports] = (0, import_react.useState)([]);
622
+ const [inboxLoaded, setInboxLoaded] = (0, import_react.useState)(false);
549
623
  const [inboxLoading, setInboxLoading] = (0, import_react.useState)(false);
550
624
  const [selectedReportId, setSelectedReportId] = (0, import_react.useState)(null);
625
+ const selectedRef = (0, import_react.useRef)(null);
551
626
  const [threadComments, setThreadComments] = (0, import_react.useState)([]);
627
+ const [threadStatus, setThreadStatus] = (0, import_react.useState)("idle");
552
628
  const [replyText, setReplyText] = (0, import_react.useState)("");
629
+ const [replyState, setReplyState] = (0, import_react.useState)("idle");
630
+ const [feedbackSending, setFeedbackSending] = (0, import_react.useState)(false);
631
+ const [feedbackError, setFeedbackError] = (0, import_react.useState)(false);
553
632
  const [assistantInput, setAssistantInput] = (0, import_react.useState)("");
554
633
  const [assistantThreadId, setAssistantThreadId] = (0, import_react.useState)(null);
555
634
  const [assistantSending, setAssistantSending] = (0, import_react.useState)(false);
556
635
  const [assistantError, setAssistantError] = (0, import_react.useState)(null);
557
636
  const [assistantTurns, setAssistantTurns] = (0, import_react.useState)([]);
558
637
  const [screenshotAttached, setScreenshotAttached] = (0, import_react.useState)(true);
559
- const loadInbox = (0, import_react.useCallback)(async () => {
560
- if (!mushi?.listMyReports) return;
561
- setInboxLoading(true);
562
- try {
563
- const rows = await mushi.listMyReports();
564
- setInboxReports(rows);
565
- } finally {
566
- setInboxLoading(false);
567
- }
568
- }, [mushi]);
638
+ const loadInbox = (0, import_react.useCallback)(
639
+ async (opts = {}) => {
640
+ if (!mushi?.listMyReports) return;
641
+ if (!opts.silent) setInboxLoading(true);
642
+ try {
643
+ const rows = await mushi.listMyReports();
644
+ setInboxReports(rows.filter((r) => !(0, import_reporter_ui.reporterStatus)(r, locale).hidden));
645
+ setInboxLoaded(true);
646
+ } catch {
647
+ } finally {
648
+ if (!opts.silent) setInboxLoading(false);
649
+ }
650
+ },
651
+ [mushi, locale]
652
+ );
569
653
  (0, import_react.useEffect)(() => {
570
654
  if (visible) {
571
655
  setScreenshotAttached(true);
@@ -576,28 +660,87 @@ var MushiBottomSheet = ({
576
660
  (0, import_react.useEffect)(() => {
577
661
  if (!visible || sheetTab !== "inbox" || inboxPollIntervalMs <= 0) return;
578
662
  const timer = setInterval(() => {
579
- void loadInbox();
663
+ void loadInbox({ silent: true });
580
664
  }, inboxPollIntervalMs);
581
665
  return () => clearInterval(timer);
582
666
  }, [visible, sheetTab, inboxPollIntervalMs, loadInbox]);
583
- const openThread = (0, import_react.useCallback)(async (reportId) => {
584
- setSelectedReportId(reportId);
585
- if (!mushi?.listMyComments) return;
586
- const comments = await mushi.listMyComments(reportId);
587
- setThreadComments(comments);
588
- }, [mushi]);
667
+ const openThread = (0, import_react.useCallback)(
668
+ async (reportId, opts = {}) => {
669
+ selectedRef.current = reportId;
670
+ setSelectedReportId(reportId);
671
+ if (!opts.silent) {
672
+ setThreadComments([]);
673
+ setThreadStatus("loading");
674
+ setReplyState("idle");
675
+ setFeedbackError(false);
676
+ }
677
+ const load = mushi?.loadMyThread ?? mushi?.listMyComments;
678
+ const comments = load ? await loadReporterThread(load, reportId) : null;
679
+ if (selectedRef.current !== reportId) return;
680
+ if (comments === null) {
681
+ setThreadStatus((prev) => opts.silent && prev === "ready" ? "ready" : "error");
682
+ return;
683
+ }
684
+ setThreadComments(comments);
685
+ setThreadStatus("ready");
686
+ if (!opts.silent && mushi?.markReportRead) {
687
+ mushi.markReportRead(reportId).then((marked) => {
688
+ if (marked > 0) {
689
+ setInboxReports((rows) => rows.map((r) => r.id === reportId ? { ...r, unread_count: 0 } : r));
690
+ }
691
+ }).catch(() => void 0);
692
+ }
693
+ },
694
+ [mushi]
695
+ );
696
+ const closeThread = (0, import_react.useCallback)(() => {
697
+ selectedRef.current = null;
698
+ setSelectedReportId(null);
699
+ setThreadStatus("idle");
700
+ }, []);
589
701
  const sendReply = (0, import_react.useCallback)(async () => {
590
- if (!mushi?.replyToReport || !selectedReportId || !replyText.trim()) return;
591
- await mushi.replyToReport(selectedReportId, replyText.trim());
592
- setReplyText("");
593
- await openThread(selectedReportId);
594
- }, [mushi, selectedReportId, replyText, openThread]);
595
- const submitFeedback = (0, import_react.useCallback)(async (signal) => {
596
- if (!mushi?.submitFeedbackSignal || !selectedReportId) return;
597
- await mushi.submitFeedbackSignal(selectedReportId, signal);
598
- await loadInbox();
599
- await openThread(selectedReportId);
600
- }, [mushi, selectedReportId, loadInbox, openThread]);
702
+ const body = replyText.trim();
703
+ const reportId = selectedReportId;
704
+ if (!mushi?.replyToReport || !reportId || !body || replyState === "sending") return;
705
+ setReplyState("sending");
706
+ try {
707
+ const comment = await settleWithin(mushi.replyToReport(reportId, body), THREAD_LOAD_TIMEOUT_MS);
708
+ if (!comment) {
709
+ setReplyState("failed");
710
+ return;
711
+ }
712
+ setReplyText("");
713
+ setReplyState("idle");
714
+ if (selectedRef.current === reportId) {
715
+ setThreadComments((prev) => [...prev, comment]);
716
+ void openThread(reportId, { silent: true });
717
+ }
718
+ } catch {
719
+ setReplyState("failed");
720
+ }
721
+ }, [mushi, selectedReportId, replyText, replyState, openThread]);
722
+ const submitFeedback = (0, import_react.useCallback)(
723
+ async (signal) => {
724
+ const reportId = selectedReportId;
725
+ if (!mushi?.submitFeedbackSignal || !reportId || feedbackSending) return;
726
+ setFeedbackSending(true);
727
+ setFeedbackError(false);
728
+ try {
729
+ const outcome = await mushi.submitFeedbackSignal(reportId, signal);
730
+ if (!outcome) {
731
+ setFeedbackError(true);
732
+ return;
733
+ }
734
+ await loadInbox({ silent: true });
735
+ await openThread(reportId, { silent: true });
736
+ } catch {
737
+ setFeedbackError(true);
738
+ } finally {
739
+ setFeedbackSending(false);
740
+ }
741
+ },
742
+ [mushi, selectedReportId, feedbackSending, loadInbox, openThread]
743
+ );
601
744
  const sendAssistant = (0, import_react.useCallback)(async (message) => {
602
745
  if (!mushi?.askAssistant || !message.trim() || assistantSending) return;
603
746
  const trimmed = message.trim();
@@ -693,7 +836,7 @@ var MushiBottomSheet = ({
693
836
  const handleSubmit = async () => {
694
837
  if (!category || !description.trim() || !mushi) return;
695
838
  const trimmed = description.trim();
696
- if (trimmed.length < 20) {
839
+ if (trimmed.length < MIN_DESCRIPTION) {
697
840
  setSubmitError("Please write at least 20 characters so we can understand the issue.");
698
841
  return;
699
842
  }
@@ -728,11 +871,181 @@ var MushiBottomSheet = ({
728
871
  }
729
872
  };
730
873
  const activeScreenshot = screenshotDataUrl && screenshotAttached ? screenshotDataUrl : null;
731
- const pal = (0, import_core3.mushiPalette)(dark ? "dark" : "light");
732
- const colors = dark ? { bg: pal.paper, text: pal.ink, sub: pal.inkMuted, card: pal.paperRaised, accent: import_core3.MUSHI_BANNER_NEON.bg, accentInk: import_core3.MUSHI_BANNER_NEON.fg, border: pal.ruleStrong, backdrop: "rgba(0,0,0,0.6)", disabled: import_core3.MUSHI_CONTROL_DISABLED.dark, disabledText: pal.inkFaint } : { bg: pal.paperRaised, text: pal.ink, sub: pal.inkMuted, card: pal.paper, accent: import_core3.MUSHI_BANNER_NEON.bg, accentInk: import_core3.MUSHI_BANNER_NEON.fg, border: pal.ruleStrong, backdrop: "rgba(0,0,0,0.35)", disabled: import_core3.MUSHI_CONTROL_DISABLED.light, disabledText: pal.inkFaint };
874
+ const colors = resolveRNTheme(dark, theme);
875
+ const font = colors.fontFamily ? { fontFamily: colors.fontFamily } : null;
876
+ const radius = colors.radius;
733
877
  const sheetTabs = ["report", "inbox", ...assistantEnabled ? ["assistant"] : []];
734
- const canSubmit = !!category && description.trim().length >= 20 && (phase === "form" || phase === "error");
735
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Modal, { visible, transparent: true, animationType: "none", statusBarTranslucent: true, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
878
+ const canSubmit = !!category && description.trim().length >= MIN_DESCRIPTION && (phase === "form" || phase === "error");
879
+ const submitHint = canSubmit || phase === "sending" ? void 0 : !category ? t.step1.heading : rc.ui.addWords;
880
+ const selectedReport = selectedReportId ? inboxReports.find((r) => r.id === selectedReportId) ?? null : null;
881
+ const selectedView = selectedReport ? (0, import_reporter_ui.reporterStatus)(selectedReport, locale) : null;
882
+ const headerTitle = sheetTab === "assistant" ? assistantLabel : sheetTab === "inbox" ? t.flows.reports.title : t.widget.title;
883
+ const renderThread = () => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
884
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
885
+ import_react_native2.TouchableOpacity,
886
+ {
887
+ onPress: closeThread,
888
+ accessibilityRole: "button",
889
+ accessibilityLabel: t.widget.back,
890
+ hitSlop: { top: 8, bottom: 8, left: 8, right: 8 },
891
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react_native2.Text, { style: [{ color: colors.accent, marginBottom: 8 }, font], children: [
892
+ "\u2190 ",
893
+ t.widget.back
894
+ ] })
895
+ }
896
+ ),
897
+ selectedReport && selectedView ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react_native2.View, { style: [s.headerCard, { backgroundColor: colors.surface, borderRadius: radius, borderColor: colors.border }], children: [
898
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [s.pill, { color: colors.fg, borderColor: colors.border }, font], children: selectedView.label }),
899
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [{ color: colors.muted, fontSize: 12, marginTop: 4 }, font], children: selectedView.detail }),
900
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [{ color: colors.fg, marginTop: 8 }, font], numberOfLines: 4, children: selectedReport.title ?? selectedReport.summary ?? selectedReport.description ?? "" })
901
+ ] }) : null,
902
+ threadStatus === "loading" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.View, { accessibilityLabel: t.flows.reports.loading, accessibilityLiveRegion: "polite", children: [0, 1, 2].map((i) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.View, { style: [s.skeleton, { backgroundColor: colors.surface, width: `${90 - i * 18}%` }] }, i)) }) : threadStatus === "error" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react_native2.View, { style: s.errorRow, accessibilityLiveRegion: "polite", children: [
903
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [{ color: colors.error, flex: 1 }, font], accessibilityRole: "alert", children: rc.ui.loadError }),
904
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
905
+ import_react_native2.TouchableOpacity,
906
+ {
907
+ onPress: () => selectedReportId && void openThread(selectedReportId),
908
+ accessibilityRole: "button",
909
+ style: [s.retryBtn, { borderColor: colors.border, borderRadius: radius }],
910
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [{ color: colors.fg, fontWeight: "600" }, font], children: rc.ui.retry })
911
+ }
912
+ )
913
+ ] }) : threadComments.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [{ color: colors.muted, marginBottom: 8 }, font], children: rc.ui.noReplies }) : threadComments.map((c) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
914
+ import_react_native2.View,
915
+ {
916
+ style: [
917
+ s.threadBubble,
918
+ {
919
+ backgroundColor: colors.surface,
920
+ alignSelf: c.author_kind === "reporter" ? "flex-end" : "flex-start"
921
+ }
922
+ ],
923
+ children: [
924
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [{ color: colors.muted, fontSize: 11 }, font], children: c.author_kind === "reporter" ? rc.ui.you : rc.ui.developer }),
925
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [{ color: colors.fg }, font], children: c.body })
926
+ ]
927
+ },
928
+ c.id
929
+ )),
930
+ replyState === "sending" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react_native2.View, { style: [s.threadBubble, { backgroundColor: colors.surface, alignSelf: "flex-end", opacity: 0.7 }], children: [
931
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [{ color: colors.muted, fontSize: 11 }, font], children: rc.ui.sending }),
932
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [{ color: colors.fg }, font], children: replyText.trim() })
933
+ ] }) : null,
934
+ selectedView?.canVerify ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react_native2.View, { style: s.verifyRow, children: [
935
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
936
+ import_react_native2.TouchableOpacity,
937
+ {
938
+ style: [s.verifyBtn, { backgroundColor: colors.accent, borderRadius: radius }],
939
+ onPress: () => void submitFeedback("confirms"),
940
+ disabled: feedbackSending,
941
+ accessibilityRole: "button",
942
+ accessibilityState: { disabled: feedbackSending, busy: feedbackSending },
943
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [s.submitText, { color: colors.accentFg }, font], children: rc.ui.yes })
944
+ }
945
+ ),
946
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
947
+ import_react_native2.TouchableOpacity,
948
+ {
949
+ style: [s.verifyBtn, { backgroundColor: colors.border, borderRadius: radius }],
950
+ onPress: () => void submitFeedback("not_fixed"),
951
+ disabled: feedbackSending,
952
+ accessibilityRole: "button",
953
+ accessibilityState: { disabled: feedbackSending, busy: feedbackSending },
954
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [s.submitText, { color: colors.fg }, font], children: rc.ui.notYet })
955
+ }
956
+ )
957
+ ] }) : null,
958
+ feedbackError ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [{ color: colors.error, marginBottom: 8 }, font], accessibilityRole: "alert", children: rc.ui.sendFailed }) : null,
959
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react_native2.View, { style: s.composer, children: [
960
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
961
+ import_react_native2.TextInput,
962
+ {
963
+ style: [
964
+ s.input,
965
+ s.composerInput,
966
+ { backgroundColor: colors.surface, color: colors.fg, borderColor: colors.border, borderRadius: radius },
967
+ font
968
+ ],
969
+ placeholder: t.flows.thread.replyPlaceholder,
970
+ placeholderTextColor: colors.muted,
971
+ value: replyText,
972
+ onChangeText: (text) => {
973
+ setReplyText(text.slice(0, 2e3));
974
+ if (replyState === "failed") setReplyState("idle");
975
+ },
976
+ multiline: true,
977
+ maxLength: 2e3,
978
+ editable: replyState !== "sending",
979
+ accessibilityLabel: t.flows.thread.replyPlaceholder
980
+ }
981
+ ),
982
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
983
+ import_react_native2.TouchableOpacity,
984
+ {
985
+ style: [
986
+ s.sendBtn,
987
+ {
988
+ backgroundColor: replyText.trim() && replyState !== "sending" ? colors.accent : colors.disabled,
989
+ borderRadius: radius
990
+ }
991
+ ],
992
+ onPress: () => void sendReply(),
993
+ disabled: !replyText.trim() || replyState === "sending",
994
+ accessibilityRole: "button",
995
+ accessibilityLabel: replyState === "failed" ? rc.ui.retry : t.flows.thread.send,
996
+ accessibilityState: { disabled: !replyText.trim() || replyState === "sending", busy: replyState === "sending" },
997
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
998
+ import_react_native2.Text,
999
+ {
1000
+ style: [
1001
+ s.submitText,
1002
+ { color: replyText.trim() && replyState !== "sending" ? colors.accentFg : colors.disabledFg },
1003
+ font
1004
+ ],
1005
+ children: replyState === "failed" ? rc.ui.retry : t.flows.thread.send
1006
+ }
1007
+ )
1008
+ }
1009
+ )
1010
+ ] }),
1011
+ replyState === "failed" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react_native2.Text, { style: [{ color: colors.error, marginTop: 6, fontSize: 12 }, font], accessibilityRole: "alert", children: [
1012
+ rc.ui.sendFailed,
1013
+ " \xB7 ",
1014
+ rc.ui.retry
1015
+ ] }) : null
1016
+ ] });
1017
+ const renderInboxList = () => {
1018
+ if (inboxLoading && !inboxLoaded) {
1019
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [{ color: colors.muted }, font], children: t.flows.reports.loading });
1020
+ }
1021
+ if (inboxReports.length === 0) {
1022
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [{ color: colors.muted }, font], children: rc.ui.empty });
1023
+ }
1024
+ return inboxReports.map((r) => {
1025
+ const view = (0, import_reporter_ui.reporterStatus)(r, locale);
1026
+ const unread = (r.unread_count ?? 0) > 0;
1027
+ const title = (r.title ?? r.summary ?? r.description ?? "Report").slice(0, 80);
1028
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1029
+ import_react_native2.TouchableOpacity,
1030
+ {
1031
+ style: [s.inboxRow, { borderColor: colors.border }],
1032
+ onPress: () => void openThread(r.id),
1033
+ accessibilityRole: "button",
1034
+ accessibilityLabel: `${view.label}. ${title}${unread ? `. ${r.unread_count} new` : ""}`,
1035
+ children: [
1036
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [{ color: colors.fg, fontWeight: unread ? "700" : "600" }, font], numberOfLines: 1, children: title }),
1037
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react_native2.Text, { style: [{ color: unread ? colors.fg : colors.muted, fontSize: 12, marginTop: 2 }, font], numberOfLines: 1, children: [
1038
+ view.label,
1039
+ view.othersNote ? ` \xB7 ${view.othersNote}` : ""
1040
+ ] }),
1041
+ unread && r.last_event_preview ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [{ color: colors.fg, fontSize: 12, fontWeight: "700", marginTop: 2 }, font], numberOfLines: 1, children: r.last_event_preview }) : null
1042
+ ]
1043
+ },
1044
+ r.id
1045
+ );
1046
+ });
1047
+ };
1048
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Modal, { visible, transparent: true, animationType: "none", statusBarTranslucent: true, onRequestClose: handleClose, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
736
1049
  import_react_native2.KeyboardAvoidingView,
737
1050
  {
738
1051
  style: s.flex,
@@ -743,44 +1056,69 @@ var MushiBottomSheet = ({
743
1056
  import_react_native2.Animated.View,
744
1057
  {
745
1058
  style: [s.backdrop, { backgroundColor: colors.backdrop, opacity: backdropOpacity }],
746
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.TouchableOpacity, { style: s.flex, activeOpacity: 1, onPress: handleClose })
1059
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1060
+ import_react_native2.TouchableOpacity,
1061
+ {
1062
+ style: s.flex,
1063
+ activeOpacity: 1,
1064
+ onPress: handleClose,
1065
+ accessibilityRole: "button",
1066
+ accessibilityLabel: t.widget.close
1067
+ }
1068
+ )
747
1069
  }
748
1070
  ),
749
1071
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
750
1072
  import_react_native2.Animated.View,
751
1073
  {
1074
+ accessibilityViewIsModal: true,
1075
+ onAccessibilityEscape: handleClose,
752
1076
  style: [
753
1077
  s.sheet,
754
1078
  { backgroundColor: colors.bg, transform: [{ translateY }] }
755
1079
  ],
756
1080
  children: [
757
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.View, { ...panResponder.panHandlers, style: s.handleArea, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.View, { style: [s.handle, { backgroundColor: colors.sub }] }) }),
758
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react_native2.View, { style: [s.brandHeader, { backgroundColor: import_core3.MUSHI_BANNER_NEON.bg, borderBottomColor: import_core3.MUSHI_BANNER_NEON.border }], children: [
759
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [s.brandEyebrow, { color: import_core3.MUSHI_BANNER_NEON.fg }], children: "MUSHI \xB7 BETA" }),
760
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [s.brandTitle, { color: import_core3.MUSHI_BANNER_NEON.fg }], children: sheetTab === "assistant" ? assistantLabel : sheetTab === "inbox" ? t.flows.reports.title : t.widget.title })
761
- ] }),
762
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.View, { style: [s.tabRow, { borderBottomColor: colors.border }], children: sheetTabs.map((tab) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1081
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1082
+ import_react_native2.View,
1083
+ {
1084
+ ...panResponder.panHandlers,
1085
+ style: s.handleArea,
1086
+ accessible: true,
1087
+ accessibilityRole: "adjustable",
1088
+ accessibilityLabel: t.widget.close,
1089
+ accessibilityActions: [{ name: "decrement", label: t.widget.close }, { name: "escape" }],
1090
+ onAccessibilityAction: (e) => {
1091
+ if (e.nativeEvent.actionName === "decrement" || e.nativeEvent.actionName === "escape") handleClose();
1092
+ },
1093
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.View, { style: [s.handle, { backgroundColor: colors.muted }] })
1094
+ }
1095
+ ),
1096
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.View, { style: [s.header, { borderBottomColor: colors.border }], children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { accessibilityRole: "header", style: [s.headerTitle, { color: colors.fg }, font], children: headerTitle }) }),
1097
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.View, { style: [s.tabRow, { borderBottomColor: colors.border }], accessibilityRole: "tablist", children: sheetTabs.map((tab) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
763
1098
  import_react_native2.TouchableOpacity,
764
1099
  {
765
1100
  onPress: () => {
766
1101
  setSheetTab(tab);
767
- if (tab === "inbox") void loadInbox();
768
- else setSelectedReportId(null);
1102
+ if (tab === "inbox") void loadInbox({ silent: inboxLoaded });
1103
+ else closeThread();
769
1104
  },
770
1105
  style: [s.tabBtn, sheetTab === tab && { borderBottomColor: colors.accent }],
771
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [s.tabLabel, { color: sheetTab === tab ? colors.text : colors.sub }], children: tab === "report" ? t.widget.trigger : tab === "inbox" ? t.flows.reports.title : assistantLabel })
1106
+ accessibilityRole: "tab",
1107
+ accessibilityState: { selected: sheetTab === tab },
1108
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [s.tabLabel, { color: sheetTab === tab ? colors.fg : colors.muted }, font], children: tab === "report" ? t.widget.trigger : tab === "inbox" ? t.flows.reports.title : assistantLabel })
772
1109
  },
773
1110
  tab
774
1111
  )) }),
775
1112
  sheetTab === "assistant" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react_native2.ScrollView, { style: s.body, keyboardShouldPersistTaps: "handled", children: [
776
1113
  assistantTurns.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
777
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: { color: colors.sub, marginBottom: 12, lineHeight: 20 }, children: greeting }),
1114
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [{ color: colors.muted, marginBottom: 12, lineHeight: 20 }, font], children: greeting }),
778
1115
  assistantSuggestions.map((chip) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
779
1116
  import_react_native2.TouchableOpacity,
780
1117
  {
781
- style: [s.assistantChip, { borderColor: colors.border, backgroundColor: colors.card }],
1118
+ style: [s.assistantChip, { borderColor: colors.border, backgroundColor: colors.surface }],
782
1119
  onPress: () => void sendAssistant(chip),
783
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: { color: colors.text, fontSize: 13 }, children: chip })
1120
+ accessibilityRole: "button",
1121
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [{ color: colors.fg, fontSize: 13 }, font], children: chip })
784
1122
  },
785
1123
  chip
786
1124
  ))
@@ -791,25 +1129,34 @@ var MushiBottomSheet = ({
791
1129
  s.assistantBubble,
792
1130
  {
793
1131
  alignSelf: turn.role === "user" ? "flex-end" : "flex-start",
794
- backgroundColor: turn.role === "user" ? colors.accent : colors.card
1132
+ backgroundColor: turn.role === "user" ? colors.accent : colors.surface
795
1133
  }
796
1134
  ],
797
1135
  children: [
798
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: { color: turn.role === "user" ? colors.accentInk : colors.text }, children: turn.text }),
799
- turn.options?.map((opt) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.TouchableOpacity, { onPress: () => void sendAssistant(opt), style: { marginTop: 8 }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: { color: colors.accent, fontSize: 12 }, children: opt }) }, opt))
1136
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [{ color: turn.role === "user" ? colors.accentFg : colors.fg }, font], children: turn.text }),
1137
+ turn.options?.map((opt) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1138
+ import_react_native2.TouchableOpacity,
1139
+ {
1140
+ onPress: () => void sendAssistant(opt),
1141
+ style: { marginTop: 8 },
1142
+ accessibilityRole: "button",
1143
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [{ color: colors.accent, fontSize: 12 }, font], children: opt })
1144
+ },
1145
+ opt
1146
+ ))
800
1147
  ]
801
1148
  },
802
1149
  `${turn.role}-${idx}`
803
1150
  )),
804
- assistantSending ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: { color: colors.sub, marginTop: 8 }, accessibilityLiveRegion: "polite", children: t.assistant.thinking }) : null,
805
- assistantError ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: { color: pal.danger, marginTop: 8 }, accessibilityRole: "alert", children: assistantError }) : null,
1151
+ assistantSending ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [{ color: colors.muted, marginTop: 8 }, font], accessibilityLiveRegion: "polite", children: t.assistant.thinking }) : null,
1152
+ assistantError ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [{ color: colors.error, marginTop: 8 }, font], accessibilityRole: "alert", children: assistantError }) : null,
806
1153
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react_native2.View, { style: s.assistantComposer, children: [
807
1154
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
808
1155
  import_react_native2.TextInput,
809
1156
  {
810
- style: [s.input, { flex: 1, minHeight: 44, backgroundColor: colors.card, color: colors.text, borderColor: colors.border }],
1157
+ style: [s.input, { flex: 1, minHeight: 44, backgroundColor: colors.surface, color: colors.fg, borderColor: colors.border, borderRadius: radius }, font],
811
1158
  placeholder: t.assistant.inputPlaceholder,
812
- placeholderTextColor: colors.sub,
1159
+ placeholderTextColor: colors.muted,
813
1160
  value: assistantInput,
814
1161
  onChangeText: setAssistantInput,
815
1162
  editable: !assistantSending
@@ -818,45 +1165,19 @@ var MushiBottomSheet = ({
818
1165
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
819
1166
  import_react_native2.TouchableOpacity,
820
1167
  {
821
- style: [s.submitBtn, { backgroundColor: colors.accent, paddingHorizontal: 16 }],
1168
+ style: [s.submitBtn, { backgroundColor: colors.accent, paddingHorizontal: 16, borderRadius: radius }],
822
1169
  onPress: () => void sendAssistant(assistantInput),
823
1170
  disabled: !assistantInput.trim() || assistantSending,
824
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [s.submitText, { color: colors.accentInk }], children: "\u2191" })
1171
+ accessibilityRole: "button",
1172
+ accessibilityLabel: t.flows.thread.send,
1173
+ accessibilityState: { disabled: !assistantInput.trim() || assistantSending, busy: assistantSending },
1174
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [s.submitText, { color: colors.accentFg }], children: "\u2191" })
825
1175
  }
826
1176
  )
827
1177
  ] })
828
- ] }) : sheetTab === "inbox" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.ScrollView, { style: s.body, keyboardShouldPersistTaps: "handled", children: inboxLoading ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: { color: colors.sub }, children: t.flows.reports.loading }) : selectedReportId ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
829
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.TouchableOpacity, { onPress: () => setSelectedReportId(null), children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react_native2.Text, { style: { color: colors.accent, marginBottom: 8 }, children: [
830
- "\u2190 ",
831
- t.widget.back
832
- ] }) }),
833
- threadComments.map((c) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react_native2.View, { style: [s.threadBubble, { backgroundColor: colors.card }], children: [
834
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: { color: colors.sub, fontSize: 11 }, children: c.author_kind }),
835
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: { color: colors.text }, children: c.body })
836
- ] }, c.id)),
837
- VERIFIABLE_STATUSES.has(
838
- inboxReports.find((r) => r.id === selectedReportId)?.status ?? ""
839
- ) && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react_native2.View, { style: s.verifyRow, children: [
840
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.TouchableOpacity, { style: [s.verifyBtn, { backgroundColor: colors.accent }], onPress: () => submitFeedback("confirms"), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [s.submitText, { color: colors.accentInk }], children: t.flows.thread.confirmFixed }) }),
841
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.TouchableOpacity, { style: [s.verifyBtn, { backgroundColor: colors.border }], onPress: () => submitFeedback("not_fixed"), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [s.submitText, { color: colors.text }], children: t.flows.thread.notFixed }) })
842
- ] }),
843
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
844
- import_react_native2.TextInput,
845
- {
846
- style: [s.input, { backgroundColor: colors.card, color: colors.text, borderColor: colors.border, minHeight: 48 }],
847
- placeholder: t.flows.thread.replyPlaceholder,
848
- placeholderTextColor: colors.sub,
849
- value: replyText,
850
- onChangeText: setReplyText
851
- }
852
- ),
853
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.TouchableOpacity, { style: [s.submitBtn, { backgroundColor: colors.accent }], onPress: sendReply, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [s.submitText, { color: colors.accentInk }], children: t.flows.thread.send }) })
854
- ] }) : inboxReports.map((r) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react_native2.TouchableOpacity, { style: [s.inboxRow, { borderColor: colors.border }], onPress: () => openThread(r.id), children: [
855
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: { color: colors.text, fontWeight: "600" }, numberOfLines: 1, children: (r.summary ?? r.description ?? "Report").slice(0, 60) }),
856
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: { color: colors.sub, fontSize: 11 }, children: reporterStatusShort(r.status) })
857
- ] }, r.id)) }) : phase === "sent" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react_native2.View, { style: s.sentWrap, children: [
1178
+ ] }) : sheetTab === "inbox" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.ScrollView, { style: s.body, keyboardShouldPersistTaps: "handled", contentContainerStyle: { paddingBottom: 24 }, children: selectedReportId ? renderThread() : renderInboxList() }) : phase === "sent" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react_native2.View, { style: s.sentWrap, accessibilityLiveRegion: "polite", children: [
858
1179
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [s.sentEmoji], children: "\u2705" }),
859
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [s.sentText, { color: colors.text }], children: t.widget.submitted })
1180
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [s.sentText, { color: colors.fg }, font], children: t.widget.submitted })
860
1181
  ] }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
861
1182
  import_react_native2.ScrollView,
862
1183
  {
@@ -864,29 +1185,34 @@ var MushiBottomSheet = ({
864
1185
  keyboardShouldPersistTaps: "handled",
865
1186
  contentContainerStyle: { paddingBottom: 24 },
866
1187
  children: [
867
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [s.stepLabel, { color: colors.sub }], children: t.step1.heading }),
868
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.View, { style: s.catRow, children: CATEGORY_KEYS.map((key) => {
1188
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [s.stepLabel, { color: colors.muted }, font], children: t.step1.heading }),
1189
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.View, { style: s.catRow, accessibilityRole: "radiogroup", children: CATEGORY_KEYS.map((key) => {
869
1190
  const active = category === key;
870
1191
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
871
1192
  import_react_native2.TouchableOpacity,
872
1193
  {
873
1194
  onPress: () => setCategory(key),
874
1195
  activeOpacity: 0.7,
1196
+ accessibilityRole: "radio",
1197
+ accessibilityState: { selected: active, checked: active },
1198
+ accessibilityLabel: t.step1.categories[key],
875
1199
  style: [
876
1200
  s.catBtn,
877
1201
  {
878
- backgroundColor: active ? colors.accent : colors.card,
1202
+ backgroundColor: active ? colors.accent : colors.surface,
879
1203
  borderColor: active ? colors.accent : colors.border
880
1204
  }
881
1205
  ],
882
1206
  children: [
883
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: s.catEmoji, children: CATEGORY_EMOJI[key] }),
1207
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: s.catEmoji, importantForAccessibility: "no", children: CATEGORY_EMOJI[key] }),
884
1208
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
885
1209
  import_react_native2.Text,
886
1210
  {
1211
+ numberOfLines: 1,
887
1212
  style: [
888
1213
  s.catLabel,
889
- { color: active ? colors.accentInk : colors.text }
1214
+ { color: active ? colors.accentFg : colors.fg },
1215
+ font
890
1216
  ],
891
1217
  children: t.step1.categories[key]
892
1218
  }
@@ -902,13 +1228,16 @@ var MushiBottomSheet = ({
902
1228
  style: [
903
1229
  s.input,
904
1230
  {
905
- backgroundColor: colors.card,
906
- color: colors.text,
907
- borderColor: colors.border
908
- }
1231
+ backgroundColor: colors.surface,
1232
+ color: colors.fg,
1233
+ borderColor: colors.border,
1234
+ borderRadius: radius
1235
+ },
1236
+ font
909
1237
  ],
910
1238
  placeholder: t.step3.descriptionPlaceholder,
911
- placeholderTextColor: colors.sub,
1239
+ placeholderTextColor: colors.muted,
1240
+ accessibilityLabel: t.step3.descriptionPlaceholder,
912
1241
  multiline: true,
913
1242
  textAlignVertical: "top",
914
1243
  value: description,
@@ -921,19 +1250,19 @@ var MushiBottomSheet = ({
921
1250
  maxLength: 4e3
922
1251
  }
923
1252
  ),
924
- submitError ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: { color: colors.accent, marginTop: 6, fontSize: 12 }, children: submitError }) : null,
1253
+ submitError ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [{ color: colors.error, marginTop: 6, fontSize: 12 }, font], accessibilityRole: "alert", children: submitError }) : null,
925
1254
  activeScreenshot ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react_native2.View, { style: s.screenshotRow, children: [
926
1255
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
927
1256
  import_react_native2.Image,
928
1257
  {
929
1258
  source: { uri: activeScreenshot },
930
- style: [s.screenshotThumb, { backgroundColor: colors.text }],
1259
+ style: [s.screenshotThumb, { backgroundColor: colors.fg }],
931
1260
  accessibilityLabel: t.step3.screenshotPreviewAlt
932
1261
  }
933
1262
  ),
934
1263
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react_native2.View, { style: s.screenshotMeta, children: [
935
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [s.screenshotLabel, { color: colors.text }], children: t.step3.screenshotAttached.replace(" \u2713", "") }),
936
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [s.screenshotSub, { color: colors.sub }], children: screenshotSensitiveHint && screenshotSensitiveHint.trim() ? `\u26A0 ${screenshotSensitiveHint}` : t.step3.screenshotSensitiveHint.split("\u2014")[0]?.trim() ?? t.step3.screenshotSensitiveHint })
1264
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [s.screenshotLabel, { color: colors.fg }, font], children: t.step3.screenshotAttached.replace(" \u2713", "") }),
1265
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [s.screenshotSub, { color: colors.muted }, font], children: screenshotSensitiveHint && screenshotSensitiveHint.trim() ? `\u26A0 ${screenshotSensitiveHint}` : t.step3.screenshotSensitiveHint.split("\u2014")[0]?.trim() ?? t.step3.screenshotSensitiveHint })
937
1266
  ] }),
938
1267
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
939
1268
  import_react_native2.TouchableOpacity,
@@ -943,22 +1272,28 @@ var MushiBottomSheet = ({
943
1272
  onClearScreenshot?.();
944
1273
  },
945
1274
  hitSlop: { top: 8, bottom: 8, left: 8, right: 8 },
1275
+ accessibilityRole: "button",
946
1276
  accessibilityLabel: t.widget.close,
947
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [s.screenshotRemove, { color: colors.sub }], children: "\u2715" })
1277
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [s.screenshotRemove, { color: colors.muted }], children: "\u2715" })
948
1278
  }
949
1279
  )
950
1280
  ] }) : null,
1281
+ submitHint ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [{ color: colors.muted, fontSize: 12, marginBottom: 6 }, font], children: submitHint }) : null,
951
1282
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
952
1283
  import_react_native2.TouchableOpacity,
953
1284
  {
954
1285
  onPress: handleSubmit,
955
1286
  disabled: !canSubmit,
956
1287
  activeOpacity: 0.8,
1288
+ accessibilityRole: "button",
1289
+ accessibilityLabel: phase === "sending" ? t.widget.submitting : t.widget.submit,
1290
+ accessibilityHint: submitHint,
1291
+ accessibilityState: { disabled: !canSubmit, busy: phase === "sending" },
957
1292
  style: [
958
1293
  s.submitBtn,
959
- { backgroundColor: canSubmit ? colors.accent : colors.disabled }
1294
+ { backgroundColor: canSubmit ? colors.accent : colors.disabled, borderRadius: radius }
960
1295
  ],
961
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [s.submitText, { color: canSubmit ? colors.accentInk : colors.disabledText }], children: phase === "sending" ? t.widget.submitting : t.widget.submit })
1296
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [s.submitText, { color: canSubmit ? colors.accentFg : colors.disabledFg }, font], children: phase === "sending" ? t.widget.submitting : t.widget.submit })
962
1297
  }
963
1298
  )
964
1299
  ]
@@ -988,10 +1323,10 @@ var s = import_react_native2.StyleSheet.create({
988
1323
  left: 0,
989
1324
  right: 0,
990
1325
  minHeight: SHEET_HEIGHT,
991
- borderTopLeftRadius: import_core3.MUSHI_RADIUS.sheet,
992
- borderTopRightRadius: import_core3.MUSHI_RADIUS.sheet,
993
- paddingBottom: import_react_native2.Platform.OS === "ios" ? 34 : import_core3.MUSHI_SPACING.roomy,
994
- shadowColor: import_core3.MUSHI_SHADOW_INK,
1326
+ borderTopLeftRadius: import_core4.MUSHI_RADIUS.sheet,
1327
+ borderTopRightRadius: import_core4.MUSHI_RADIUS.sheet,
1328
+ paddingBottom: import_react_native2.Platform.OS === "ios" ? 34 : import_core4.MUSHI_SPACING.roomy,
1329
+ shadowColor: import_core4.MUSHI_SHADOW_INK,
995
1330
  shadowOffset: { width: 0, height: -3 },
996
1331
  shadowOpacity: 0.12,
997
1332
  shadowRadius: 8,
@@ -999,88 +1334,72 @@ var s = import_react_native2.StyleSheet.create({
999
1334
  },
1000
1335
  handleArea: {
1001
1336
  alignItems: "center",
1002
- paddingVertical: import_core3.MUSHI_SPACING.comfy
1337
+ paddingVertical: import_core4.MUSHI_SPACING.comfy
1003
1338
  },
1004
1339
  handle: {
1005
1340
  width: 40,
1006
1341
  height: 5,
1007
- borderRadius: import_core3.MUSHI_RADIUS.control,
1342
+ borderRadius: import_core4.MUSHI_RADIUS.control,
1008
1343
  opacity: 0.5
1009
1344
  },
1010
- brandHeader: {
1011
- paddingHorizontal: import_core3.MUSHI_SPACING.lounge,
1012
- paddingTop: import_core3.MUSHI_SPACING.comfy,
1013
- paddingBottom: import_core3.MUSHI_SPACING.comfy,
1014
- borderBottomWidth: 1.5
1345
+ header: {
1346
+ paddingHorizontal: import_core4.MUSHI_SPACING.lounge,
1347
+ paddingBottom: import_core4.MUSHI_SPACING.comfy
1015
1348
  },
1016
- brandEyebrow: {
1017
- fontSize: import_core3.MUSHI_TYPE.sizeLabel,
1018
- fontWeight: "700",
1019
- letterSpacing: 1.2,
1020
- marginBottom: 2,
1021
- opacity: 0.7
1022
- },
1023
- brandTitle: {
1349
+ headerTitle: {
1024
1350
  fontSize: 18,
1025
- fontWeight: "800",
1351
+ fontWeight: "700",
1026
1352
  letterSpacing: -0.2
1027
1353
  },
1028
1354
  body: {
1029
- paddingHorizontal: import_core3.MUSHI_SPACING.lounge,
1030
- paddingTop: import_core3.MUSHI_SPACING.tight
1031
- },
1032
- title: {
1033
- fontSize: 20,
1034
- fontWeight: "700",
1035
- marginBottom: import_core3.MUSHI_SPACING.roomy
1355
+ paddingHorizontal: import_core4.MUSHI_SPACING.lounge,
1356
+ paddingTop: import_core4.MUSHI_SPACING.tight
1036
1357
  },
1037
1358
  stepLabel: {
1038
- fontSize: import_core3.MUSHI_TYPE.sizeLabel,
1359
+ fontSize: import_core4.MUSHI_TYPE.sizeLabel,
1039
1360
  fontWeight: "600",
1040
1361
  letterSpacing: 0.2,
1041
- marginTop: import_core3.MUSHI_SPACING.snug,
1042
- marginBottom: import_core3.MUSHI_SPACING.comfy,
1362
+ marginTop: import_core4.MUSHI_SPACING.snug,
1363
+ marginBottom: import_core4.MUSHI_SPACING.comfy,
1043
1364
  textTransform: "uppercase"
1044
1365
  },
1045
1366
  catRow: {
1046
1367
  flexDirection: "row",
1047
- justifyContent: "space-between",
1048
- marginBottom: import_core3.MUSHI_SPACING.roomy
1368
+ flexWrap: "wrap",
1369
+ gap: 8,
1370
+ marginBottom: import_core4.MUSHI_SPACING.roomy
1049
1371
  },
1050
1372
  catBtn: {
1373
+ flexDirection: "row",
1051
1374
  alignItems: "center",
1052
- paddingVertical: import_core3.MUSHI_SPACING.comfy,
1053
- paddingHorizontal: 6,
1054
- borderRadius: import_core3.MUSHI_RADIUS.card,
1375
+ gap: 6,
1376
+ paddingVertical: 8,
1377
+ paddingHorizontal: 12,
1378
+ borderRadius: 999,
1055
1379
  borderWidth: 1,
1056
- flex: 1,
1057
- marginHorizontal: 3,
1058
1380
  minHeight: 44
1059
1381
  },
1060
1382
  catEmoji: {
1061
- fontSize: 22,
1062
- marginBottom: import_core3.MUSHI_SPACING.tight
1383
+ fontSize: 16
1063
1384
  },
1064
1385
  catLabel: {
1065
- fontSize: import_core3.MUSHI_TYPE.sizeLabel,
1386
+ fontSize: 14,
1066
1387
  fontWeight: "600"
1067
1388
  },
1068
1389
  input: {
1069
1390
  borderWidth: 1,
1070
- borderRadius: import_core3.MUSHI_RADIUS.card,
1071
1391
  padding: 14,
1072
1392
  fontSize: 15,
1073
1393
  minHeight: 100,
1074
- marginBottom: import_core3.MUSHI_SPACING.roomy
1394
+ marginBottom: import_core4.MUSHI_SPACING.roomy
1075
1395
  },
1076
1396
  submitBtn: {
1077
- borderRadius: import_core3.MUSHI_RADIUS.card,
1078
1397
  paddingVertical: 14,
1079
1398
  alignItems: "center",
1080
1399
  minHeight: 44
1081
1400
  },
1082
1401
  submitText: {
1083
- color: import_core3.MUSHI_INVERSE,
1402
+ color: import_core4.MUSHI_INVERSE,
1084
1403
  fontSize: 16,
1085
1404
  fontWeight: "700"
1086
1405
  },
@@ -1139,7 +1458,8 @@ var s = import_react_native2.StyleSheet.create({
1139
1458
  alignItems: "center",
1140
1459
  paddingVertical: 10,
1141
1460
  borderBottomWidth: 2,
1142
- borderBottomColor: "transparent"
1461
+ borderBottomColor: "transparent",
1462
+ minHeight: 44
1143
1463
  },
1144
1464
  tabLabel: {
1145
1465
  fontSize: 14,
@@ -1147,12 +1467,47 @@ var s = import_react_native2.StyleSheet.create({
1147
1467
  },
1148
1468
  inboxRow: {
1149
1469
  paddingVertical: 12,
1470
+ minHeight: 56,
1150
1471
  borderBottomWidth: import_react_native2.StyleSheet.hairlineWidth
1151
1472
  },
1473
+ headerCard: {
1474
+ borderWidth: import_react_native2.StyleSheet.hairlineWidth,
1475
+ padding: 12,
1476
+ marginBottom: 12
1477
+ },
1478
+ pill: {
1479
+ alignSelf: "flex-start",
1480
+ borderWidth: 1,
1481
+ borderRadius: 999,
1482
+ paddingHorizontal: 8,
1483
+ paddingVertical: 2,
1484
+ fontSize: 12,
1485
+ fontWeight: "600",
1486
+ overflow: "hidden"
1487
+ },
1488
+ skeleton: {
1489
+ height: 14,
1490
+ borderRadius: 6,
1491
+ marginBottom: 10
1492
+ },
1493
+ errorRow: {
1494
+ flexDirection: "row",
1495
+ alignItems: "center",
1496
+ gap: 8,
1497
+ marginBottom: 12
1498
+ },
1499
+ retryBtn: {
1500
+ borderWidth: 1,
1501
+ paddingHorizontal: 12,
1502
+ paddingVertical: 8,
1503
+ minHeight: 44,
1504
+ justifyContent: "center"
1505
+ },
1152
1506
  threadBubble: {
1153
1507
  borderRadius: 10,
1154
1508
  padding: 10,
1155
- marginBottom: 8
1509
+ marginBottom: 8,
1510
+ maxWidth: "88%"
1156
1511
  },
1157
1512
  verifyRow: {
1158
1513
  flexDirection: "row",
@@ -1161,9 +1516,26 @@ var s = import_react_native2.StyleSheet.create({
1161
1516
  },
1162
1517
  verifyBtn: {
1163
1518
  flex: 1,
1164
- borderRadius: 10,
1165
1519
  paddingVertical: 10,
1166
- alignItems: "center"
1520
+ alignItems: "center",
1521
+ minHeight: 44
1522
+ },
1523
+ composer: {
1524
+ flexDirection: "row",
1525
+ alignItems: "flex-end",
1526
+ gap: 8
1527
+ },
1528
+ composerInput: {
1529
+ flex: 1,
1530
+ minHeight: 44,
1531
+ maxHeight: 120,
1532
+ marginBottom: 0
1533
+ },
1534
+ sendBtn: {
1535
+ paddingHorizontal: 16,
1536
+ paddingVertical: 12,
1537
+ minHeight: 44,
1538
+ justifyContent: "center"
1167
1539
  },
1168
1540
  assistantBubble: {
1169
1541
  maxWidth: "88%",
@@ -1191,9 +1563,9 @@ var s = import_react_native2.StyleSheet.create({
1191
1563
  // src/components/MushiFloatingButton.tsx
1192
1564
  var import_react2 = require("react");
1193
1565
  var import_react_native3 = require("react-native");
1194
- var import_core4 = require("@mushi-mushi/core");
1566
+ var import_core5 = require("@mushi-mushi/core");
1195
1567
  var import_jsx_runtime2 = require("react/jsx-runtime");
1196
- var FAB_SIZE = import_core4.MUSHI_GEOMETRY.fabSize;
1568
+ var FAB_SIZE = import_core5.MUSHI_GEOMETRY.fabSize;
1197
1569
  var EDGE_PADDING = 12;
1198
1570
  function getScreenBounds() {
1199
1571
  const { width, height } = import_react_native3.Dimensions.get("window");
@@ -1243,7 +1615,7 @@ var MushiFloatingButton = ({
1243
1615
  const currentX = (0, import_react2.useRef)(baseX);
1244
1616
  const currentY = (0, import_react2.useRef)(0);
1245
1617
  const isDragging = (0, import_react2.useRef)(false);
1246
- const pal = (0, import_core4.mushiPalette)(dark ? "dark" : "light");
1618
+ const pal = (0, import_core5.mushiPalette)(dark ? "dark" : "light");
1247
1619
  const accentColor = accent ?? pal.accent;
1248
1620
  (0, import_react2.useEffect)(() => {
1249
1621
  if (!persist) return;
@@ -1354,7 +1726,7 @@ var MushiFloatingButton = ({
1354
1726
  onStartShouldSetResponder: () => !draggable,
1355
1727
  onResponderGrant: handlePressIn,
1356
1728
  onResponderRelease: handlePressOut,
1357
- children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native3.Animated.Text, { style: s2.emoji, children: import_core4.MUSHI_COPY.triggerText })
1729
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native3.Animated.Text, { style: s2.emoji, children: import_core5.MUSHI_COPY.triggerText })
1358
1730
  }
1359
1731
  );
1360
1732
  };
@@ -1380,14 +1752,14 @@ var s2 = import_react_native3.StyleSheet.create({
1380
1752
  // src/components/MushiBanner.tsx
1381
1753
  var import_react3 = require("react");
1382
1754
  var import_react_native4 = require("react-native");
1383
- var import_core5 = require("@mushi-mushi/core");
1755
+ var import_core6 = require("@mushi-mushi/core");
1384
1756
  var import_jsx_runtime3 = require("react/jsx-runtime");
1385
- var MUSHI_BANNER_DEFAULT_HEIGHT = import_core5.MUSHI_GEOMETRY.bannerHeight;
1757
+ var MUSHI_BANNER_DEFAULT_HEIGHT = import_core6.MUSHI_GEOMETRY.bannerHeight;
1386
1758
  var MushiBanner = ({
1387
1759
  onPress,
1388
1760
  onDismiss,
1389
- message = import_core5.MUSHI_COPY.bannerMessage,
1390
- label = import_core5.MUSHI_COPY.bannerLabel,
1761
+ message = import_core6.MUSHI_COPY.bannerMessage,
1762
+ label = import_core6.MUSHI_COPY.bannerLabel,
1391
1763
  onLayoutHeight
1392
1764
  }) => {
1393
1765
  const handleLayout = (0, import_react3.useCallback)(
@@ -1407,7 +1779,7 @@ var MushiBanner = ({
1407
1779
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_react_native4.TouchableOpacity, { style: s3.main, onPress, activeOpacity: 0.85, children: [
1408
1780
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native4.Text, { style: s3.eyebrow, children: label.toUpperCase() }),
1409
1781
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native4.Text, { style: s3.message, numberOfLines: 2, children: message }),
1410
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native4.Text, { style: s3.cta, children: import_core5.MUSHI_COPY.bugCta })
1782
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native4.Text, { style: s3.cta, children: import_core6.MUSHI_COPY.bugCta })
1411
1783
  ] }),
1412
1784
  onDismiss ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1413
1785
  import_react_native4.TouchableOpacity,
@@ -1430,13 +1802,13 @@ var s3 = import_react_native4.StyleSheet.create({
1430
1802
  left: 0,
1431
1803
  right: 0,
1432
1804
  bottom: 0,
1433
- minHeight: import_core5.MUSHI_GEOMETRY.bannerHeight,
1805
+ minHeight: import_core6.MUSHI_GEOMETRY.bannerHeight,
1434
1806
  flexDirection: "row",
1435
1807
  alignItems: "stretch",
1436
- backgroundColor: import_core5.MUSHI_BANNER_NEON.bg,
1808
+ backgroundColor: import_core6.MUSHI_BANNER_NEON.bg,
1437
1809
  borderTopWidth: 1,
1438
- borderTopColor: import_core5.MUSHI_BANNER_NEON.border,
1439
- zIndex: import_core5.MUSHI_Z.banner,
1810
+ borderTopColor: import_core6.MUSHI_BANNER_NEON.border,
1811
+ zIndex: import_core6.MUSHI_Z.banner,
1440
1812
  elevation: 8,
1441
1813
  paddingBottom: import_react_native4.Platform.OS === "ios" ? 0 : 0
1442
1814
  },
@@ -1444,27 +1816,27 @@ var s3 = import_react_native4.StyleSheet.create({
1444
1816
  flex: 1,
1445
1817
  flexDirection: "row",
1446
1818
  alignItems: "center",
1447
- gap: import_core5.MUSHI_SPACING.snug,
1448
- paddingHorizontal: import_core5.MUSHI_SPACING.comfy,
1449
- paddingVertical: import_core5.MUSHI_SPACING.snug,
1819
+ gap: import_core6.MUSHI_SPACING.snug,
1820
+ paddingHorizontal: import_core6.MUSHI_SPACING.comfy,
1821
+ paddingVertical: import_core6.MUSHI_SPACING.snug,
1450
1822
  minHeight: 44
1451
1823
  },
1452
1824
  eyebrow: {
1453
- fontSize: import_core5.MUSHI_TYPE.sizeLabel,
1825
+ fontSize: import_core6.MUSHI_TYPE.sizeLabel,
1454
1826
  fontWeight: "700",
1455
1827
  letterSpacing: 1.2,
1456
- color: import_core5.MUSHI_BANNER_NEON.fg
1828
+ color: import_core6.MUSHI_BANNER_NEON.fg
1457
1829
  },
1458
1830
  message: {
1459
1831
  flex: 1,
1460
1832
  fontSize: 12,
1461
1833
  fontWeight: "500",
1462
- color: import_core5.MUSHI_BANNER_NEON.fg
1834
+ color: import_core6.MUSHI_BANNER_NEON.fg
1463
1835
  },
1464
1836
  cta: {
1465
1837
  fontSize: 12,
1466
1838
  fontWeight: "700",
1467
- color: import_core5.MUSHI_BANNER_NEON.fg,
1839
+ color: import_core6.MUSHI_BANNER_NEON.fg,
1468
1840
  textDecorationLine: "underline"
1469
1841
  },
1470
1842
  dismiss: {
@@ -1474,18 +1846,341 @@ var s3 = import_react_native4.StyleSheet.create({
1474
1846
  dismissText: {
1475
1847
  fontSize: 20,
1476
1848
  lineHeight: 22,
1477
- color: import_core5.MUSHI_BANNER_NEON.fg,
1849
+ color: import_core6.MUSHI_BANNER_NEON.fg,
1478
1850
  opacity: 0.7
1479
1851
  }
1480
1852
  });
1481
1853
 
1854
+ // src/analytics/event-tracker.ts
1855
+ var import_core7 = require("@mushi-mushi/core");
1856
+ var CONSENT_BUFFER_MAX = 50;
1857
+ var SPILL_MAX = 200;
1858
+ var SPILL_TTL_MS = 24 * 60 * 60 * 1e3;
1859
+ var SURFACE = "mobile";
1860
+ async function defaultStorage() {
1861
+ try {
1862
+ const mod = await import("@react-native-async-storage/async-storage");
1863
+ return mod.default;
1864
+ } catch {
1865
+ return null;
1866
+ }
1867
+ }
1868
+ function createRNEventTracker(opts) {
1869
+ const cfg = opts.config ?? {};
1870
+ const enabled = cfg.enabled !== false;
1871
+ const consentMode = cfg.consent ?? "implied";
1872
+ const allowlist = cfg.propertyAllowlist ?? [];
1873
+ const spillKey = `@mushi:events_spill_${opts.projectId}`;
1874
+ const consentKey = `@mushi:analytics_consent_${opts.projectId}`;
1875
+ let buffer = [];
1876
+ let consentBuffer = [];
1877
+ let consent = consentMode === "required" ? "pending" : "granted";
1878
+ let userId = null;
1879
+ let userTraits = null;
1880
+ let pendingIdentify = false;
1881
+ let flushing = false;
1882
+ let destroyed = false;
1883
+ let timer = null;
1884
+ let storage = null;
1885
+ const consentListeners = /* @__PURE__ */ new Set();
1886
+ const loadStorage = opts.storage ?? defaultStorage;
1887
+ const resolveClient = () => typeof opts.client === "function" ? opts.client() : opts.client;
1888
+ async function readSpill() {
1889
+ if (!storage) return [];
1890
+ try {
1891
+ const raw = await storage.getItem(spillKey);
1892
+ if (!raw) return [];
1893
+ const parsed = JSON.parse(raw);
1894
+ if (!parsed || Date.now() - parsed.at > SPILL_TTL_MS) {
1895
+ await storage.removeItem(spillKey);
1896
+ return [];
1897
+ }
1898
+ return Array.isArray(parsed.events) ? parsed.events.slice(0, SPILL_MAX) : [];
1899
+ } catch {
1900
+ return [];
1901
+ }
1902
+ }
1903
+ async function writeSpill(events) {
1904
+ if (!storage) return;
1905
+ try {
1906
+ if (events.length === 0) await storage.removeItem(spillKey);
1907
+ else await storage.setItem(spillKey, JSON.stringify({ at: Date.now(), events: events.slice(-SPILL_MAX) }));
1908
+ } catch {
1909
+ }
1910
+ }
1911
+ async function flushNow() {
1912
+ if (destroyed && buffer.length === 0) return;
1913
+ await ready;
1914
+ if (flushing || consent !== "granted") return;
1915
+ if (buffer.length === 0 && !pendingIdentify) return;
1916
+ const client = resolveClient();
1917
+ if (!client) return;
1918
+ flushing = true;
1919
+ const batch = buffer.splice(0, import_core7.EVENT_PROPERTY_LIMITS.maxServerBatch);
1920
+ if (pendingIdentify) {
1921
+ batch.unshift({ name: "identify", ts: (/* @__PURE__ */ new Date()).toISOString(), properties: {} });
1922
+ pendingIdentify = false;
1923
+ }
1924
+ try {
1925
+ const anonId = await Promise.resolve(opts.getAnonId());
1926
+ const payload = {
1927
+ anon_id: anonId,
1928
+ user_id: userId ?? opts.getUserId?.() ?? null,
1929
+ user_traits: userTraits ?? opts.getUserTraits?.() ?? null,
1930
+ session_id: null,
1931
+ sdk_version: opts.sdkVersion ?? null,
1932
+ surface: SURFACE,
1933
+ events: batch
1934
+ };
1935
+ const res = await client.postProductEvents(payload);
1936
+ if (!res.ok) await writeSpill([...await readSpill(), ...batch.filter((e) => e.name !== "identify")]);
1937
+ else await writeSpill([]);
1938
+ } catch {
1939
+ await writeSpill([...await readSpill(), ...batch.filter((e) => e.name !== "identify")]);
1940
+ } finally {
1941
+ flushing = false;
1942
+ }
1943
+ if (buffer.length >= import_core7.EVENT_PROPERTY_LIMITS.maxClientBatch) void flushNow();
1944
+ }
1945
+ function enqueue(ev) {
1946
+ if (consent === "denied") return;
1947
+ if (consent === "pending") {
1948
+ if (consentBuffer.length < CONSENT_BUFFER_MAX) consentBuffer.push(ev);
1949
+ return;
1950
+ }
1951
+ buffer.push(ev);
1952
+ if (buffer.length >= import_core7.EVENT_PROPERTY_LIMITS.maxClientBatch) void flushNow();
1953
+ }
1954
+ const ready = enabled ? (async () => {
1955
+ storage = await loadStorage();
1956
+ if (destroyed) return;
1957
+ let stored = null;
1958
+ try {
1959
+ stored = storage ? await storage.getItem(consentKey) : null;
1960
+ } catch {
1961
+ stored = null;
1962
+ }
1963
+ if (stored === "denied") {
1964
+ consent = "denied";
1965
+ buffer = [];
1966
+ consentBuffer = [];
1967
+ pendingIdentify = false;
1968
+ await writeSpill([]);
1969
+ return;
1970
+ }
1971
+ if (stored === "granted" && consent === "pending") {
1972
+ consent = "granted";
1973
+ buffer.push(...consentBuffer);
1974
+ consentBuffer = [];
1975
+ }
1976
+ const spill = await readSpill();
1977
+ if (spill.length > 0) {
1978
+ buffer.unshift(...spill);
1979
+ await writeSpill([]);
1980
+ }
1981
+ })().catch(() => void 0) : Promise.resolve();
1982
+ if (enabled) {
1983
+ const interval = Math.max(1e3, cfg.flushIntervalMs ?? 5e3);
1984
+ timer = setInterval(() => {
1985
+ void flushNow();
1986
+ }, interval);
1987
+ }
1988
+ return {
1989
+ ready,
1990
+ track(name, properties, trackOpts) {
1991
+ if (!enabled || destroyed) return false;
1992
+ if (!(0, import_core7.isValidEventName)(name)) return false;
1993
+ const { properties: props } = (0, import_core7.sanitizeEventProperties)(properties, { allowlist, scrub: opts.scrub });
1994
+ let merged = props;
1995
+ if (trackOpts?.reserved) {
1996
+ const { properties: res } = (0, import_core7.sanitizeEventProperties)(trackOpts.reserved, { allowReserved: true, scrub: opts.scrub });
1997
+ merged = { ...props, ...res };
1998
+ }
1999
+ merged.$surface = SURFACE;
2000
+ const route = opts.getRoute?.();
2001
+ if (route && merged.$route === void 0) merged.$route = route;
2002
+ if (!(0, import_core7.propertiesWithinByteLimit)(merged)) return false;
2003
+ enqueue({
2004
+ name,
2005
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
2006
+ properties: merged,
2007
+ ...trackOpts?.dedupKey ? { dedup_key: trackOpts.dedupKey } : {}
2008
+ });
2009
+ return true;
2010
+ },
2011
+ setConsent(state) {
2012
+ void ready.then(() => storage?.setItem(consentKey, state)).catch(() => void 0);
2013
+ if (state === "denied") {
2014
+ consent = "denied";
2015
+ buffer = [];
2016
+ consentBuffer = [];
2017
+ pendingIdentify = false;
2018
+ void writeSpill([]);
2019
+ } else {
2020
+ const wasPending = consent === "pending";
2021
+ consent = "granted";
2022
+ if (wasPending && consentBuffer.length > 0) {
2023
+ buffer.push(...consentBuffer);
2024
+ consentBuffer = [];
2025
+ }
2026
+ if (buffer.length > 0 || pendingIdentify) void flushNow();
2027
+ }
2028
+ for (const listener of [...consentListeners]) {
2029
+ try {
2030
+ listener(state);
2031
+ } catch {
2032
+ }
2033
+ }
2034
+ },
2035
+ setIdentity(id, traits) {
2036
+ userId = id;
2037
+ userTraits = traits ?? null;
2038
+ if (id && enabled && consent !== "denied") {
2039
+ pendingIdentify = true;
2040
+ void flushNow();
2041
+ }
2042
+ },
2043
+ consentState() {
2044
+ return enabled ? consent : "denied";
2045
+ },
2046
+ onConsentChange(listener) {
2047
+ consentListeners.add(listener);
2048
+ return () => {
2049
+ consentListeners.delete(listener);
2050
+ };
2051
+ },
2052
+ async flush() {
2053
+ await ready;
2054
+ await flushNow();
2055
+ },
2056
+ async destroy() {
2057
+ if (timer != null) {
2058
+ clearInterval(timer);
2059
+ timer = null;
2060
+ }
2061
+ await flushNow().catch(() => void 0);
2062
+ destroyed = true;
2063
+ buffer = [];
2064
+ consentBuffer = [];
2065
+ pendingIdentify = false;
2066
+ }
2067
+ };
2068
+ }
2069
+
2070
+ // src/analytics/session-tracker.ts
2071
+ var import_core8 = require("@mushi-mushi/core");
2072
+ var HEARTBEAT_INTERVAL_MS = 6e4;
2073
+ var RESUME_WINDOW_MS = 30 * 6e4;
2074
+ function createRNSessionTracker(opts) {
2075
+ const clock = opts.now ?? Date.now;
2076
+ const resolveClient = () => typeof opts.client === "function" ? opts.client() : opts.client;
2077
+ let sessionId = opts.sessionId ?? `sess_${(0, import_core8.newUuid)()}`;
2078
+ let pageViewCount = 0;
2079
+ let started = false;
2080
+ let allowed = false;
2081
+ let foreground = true;
2082
+ let destroyed = false;
2083
+ let backgroundedAt = null;
2084
+ let heartbeat = null;
2085
+ function route() {
2086
+ return opts.getRoute?.() ?? null;
2087
+ }
2088
+ function send(kind, extra) {
2089
+ const client = resolveClient();
2090
+ if (!client || !allowed || destroyed) return;
2091
+ const base = {
2092
+ kind,
2093
+ session_id: sessionId,
2094
+ ts: new Date(clock()).toISOString(),
2095
+ page_view_count: pageViewCount,
2096
+ user_agent: opts.userAgent ?? null,
2097
+ sdk_version: opts.sdkVersion
2098
+ };
2099
+ void Promise.resolve(opts.getReporterToken()).then((token) => client.postSessionEvent({ ...base, reporter_token_hash: token, ...extra })).catch(() => void 0);
2100
+ }
2101
+ function stopHeartbeat() {
2102
+ if (heartbeat != null) {
2103
+ clearInterval(heartbeat);
2104
+ heartbeat = null;
2105
+ }
2106
+ }
2107
+ function startHeartbeat() {
2108
+ stopHeartbeat();
2109
+ heartbeat = setInterval(() => send("session_heartbeat", { route: route() }), HEARTBEAT_INTERVAL_MS);
2110
+ }
2111
+ function startSession() {
2112
+ started = true;
2113
+ pageViewCount = 1;
2114
+ send("session_start", { route: route() });
2115
+ startHeartbeat();
2116
+ }
2117
+ function activate() {
2118
+ if (allowed || destroyed) return;
2119
+ allowed = true;
2120
+ if (!foreground) return;
2121
+ if (!started) startSession();
2122
+ else startHeartbeat();
2123
+ }
2124
+ function deactivate() {
2125
+ allowed = false;
2126
+ stopHeartbeat();
2127
+ }
2128
+ const unsubscribe = opts.consent.onConsentChange(() => {
2129
+ if (opts.consent.consentState() === "granted") activate();
2130
+ else deactivate();
2131
+ });
2132
+ void opts.consent.ready.then(() => {
2133
+ if (opts.consent.consentState() === "granted") activate();
2134
+ }).catch(() => void 0);
2135
+ return {
2136
+ onAppStateChange(state) {
2137
+ if (destroyed) return;
2138
+ if (state === "active") {
2139
+ if (foreground) return;
2140
+ foreground = true;
2141
+ const away = backgroundedAt == null ? 0 : clock() - backgroundedAt;
2142
+ backgroundedAt = null;
2143
+ if (!allowed) return;
2144
+ if (!started || away >= RESUME_WINDOW_MS) {
2145
+ sessionId = `sess_${(0, import_core8.newUuid)()}`;
2146
+ startSession();
2147
+ } else {
2148
+ send("session_heartbeat", { route: route() });
2149
+ startHeartbeat();
2150
+ }
2151
+ return;
2152
+ }
2153
+ if (state !== "background" || !foreground) return;
2154
+ foreground = false;
2155
+ backgroundedAt = clock();
2156
+ stopHeartbeat();
2157
+ if (started) send("session_end", { route: route() });
2158
+ },
2159
+ pageView(next) {
2160
+ if (!allowed || !started || !foreground) return;
2161
+ pageViewCount += 1;
2162
+ send("page_view", { route: next });
2163
+ },
2164
+ destroy() {
2165
+ destroyed = true;
2166
+ deactivate();
2167
+ unsubscribe();
2168
+ }
2169
+ };
2170
+ }
2171
+
1482
2172
  // src/version.ts
1483
2173
  var MUSHI_SDK_PACKAGE = "@mushi-mushi/react-native";
1484
- var MUSHI_SDK_VERSION = "0.21.0" ? "0.21.0" : "0.17.0";
2174
+ var MUSHI_SDK_VERSION = "0.23.0" ? "0.23.0" : "0.17.0";
1485
2175
 
1486
2176
  // src/provider.tsx
1487
2177
  var import_jsx_runtime4 = require("react/jsx-runtime");
1488
2178
  var MushiContext = (0, import_react4.createContext)(null);
2179
+ function nativeUserAgent() {
2180
+ const os = import_react_native5.Platform?.OS ?? "native";
2181
+ const version = import_react_native5.Platform?.Version;
2182
+ return `${MUSHI_SDK_PACKAGE}/${MUSHI_SDK_VERSION} (${version != null ? `${os} ${String(version)}` : os})`;
2183
+ }
1489
2184
  var DEFAULT_SCREENSHOT_HINT = "Check the preview \u2014 remove it if any private info (balances, personal details) is visible.";
1490
2185
  function resolveScreenshotHint(v) {
1491
2186
  if (v === false) return null;
@@ -1528,9 +2223,9 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
1528
2223
  const networkRef = (0, import_react4.useRef)(null);
1529
2224
  const queueRef = (0, import_react4.useRef)(null);
1530
2225
  const apiClientRef = (0, import_react4.useRef)(null);
1531
- const sessionIdRef = (0, import_react4.useRef)(`sess_${(0, import_core6.newUuid)()}`);
2226
+ const sessionIdRef = (0, import_react4.useRef)(`sess_${(0, import_core9.newUuid)()}`);
1532
2227
  const breadcrumbsRef = (0, import_react4.useRef)(
1533
- (0, import_core6.createBreadcrumbBuffer)({ max: config.capture?.maxBreadcrumbs ?? 50 })
2228
+ (0, import_core9.createBreadcrumbBuffer)({ max: config.capture?.maxBreadcrumbs ?? 50 })
1534
2229
  );
1535
2230
  const [sheetVisible, setSheetVisible] = (0, import_react4.useState)(false);
1536
2231
  const [sheetPreferredTab, setSheetPreferredTab] = (0, import_react4.useState)("report");
@@ -1555,7 +2250,7 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
1555
2250
  if (config.endpoint && !config.apiEndpoint && __DEV__) {
1556
2251
  console.warn("[MushiProvider] `endpoint` is deprecated \u2014 use `apiEndpoint` instead.");
1557
2252
  }
1558
- const apiEndpoint = rawEndpoint ?? import_core6.DEFAULT_API_ENDPOINT;
2253
+ const apiEndpoint = rawEndpoint ?? import_core9.DEFAULT_API_ENDPOINT;
1559
2254
  (0, import_react4.useEffect)(() => {
1560
2255
  if (config.capture?.console !== false) {
1561
2256
  consoleRef.current = setupConsoleCapture(config.capture?.maxConsoleEntries);
@@ -1563,7 +2258,7 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
1563
2258
  if (config.capture?.network !== false) {
1564
2259
  networkRef.current = setupNetworkCapture(config.capture?.maxNetworkEntries, apiEndpoint);
1565
2260
  }
1566
- apiClientRef.current = (0, import_core6.createApiClient)({
2261
+ apiClientRef.current = (0, import_core9.createApiClient)({
1567
2262
  projectId: config.projectId,
1568
2263
  apiKey: config.apiKey,
1569
2264
  apiEndpoint,
@@ -1600,14 +2295,14 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
1600
2295
  if (existing && existing.startsWith("mushi_")) {
1601
2296
  reporterTokenRef.current = existing;
1602
2297
  } else {
1603
- const fresh = `mushi_${(0, import_core6.newUuid)()}`;
2298
+ const fresh = `mushi_${(0, import_core9.newUuid)()}`;
1604
2299
  reporterTokenRef.current = fresh;
1605
2300
  await saveReporterToken(fresh, secureStorage);
1606
2301
  }
1607
2302
  resolveReporterTokenReady();
1608
2303
  } catch {
1609
2304
  if (reporterTokenRef.current === `rn-${config.projectId}-anon`) {
1610
- reporterTokenRef.current = `mushi_${(0, import_core6.newUuid)()}`;
2305
+ reporterTokenRef.current = `mushi_${(0, import_core9.newUuid)()}`;
1611
2306
  }
1612
2307
  resolveReporterTokenReady();
1613
2308
  }
@@ -1727,7 +2422,7 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
1727
2422
  const merged = Object.keys(metadataRef.current).length > 0 ? { ...contextMeta, ...metadataRef.current } : Object.keys(contextMeta).length > 0 ? contextMeta : void 0;
1728
2423
  const breadcrumbs = breadcrumbsRef.current.getAll().map((c) => ({
1729
2424
  ...c,
1730
- message: (0, import_core6.scrubPii)(c.message)
2425
+ message: (0, import_core9.scrubPii)(c.message)
1731
2426
  }));
1732
2427
  const timeline = breadcrumbs.map((c) => ({
1733
2428
  ts: c.timestamp,
@@ -1736,10 +2431,10 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
1736
2431
  }));
1737
2432
  const fingerprintHash = getDeviceFingerprintHash(deviceInfo);
1738
2433
  const report = {
1739
- id: (0, import_core6.newUuid)(),
2434
+ id: (0, import_core9.newUuid)(),
1740
2435
  projectId: config.projectId,
1741
2436
  category: data.category,
1742
- description: (0, import_core6.scrubPii)(data.description),
2437
+ description: (0, import_core9.scrubPii)(data.description),
1743
2438
  environment: {
1744
2439
  userAgent: deviceInfo.systemName ?? "ReactNative",
1745
2440
  platform: deviceInfo.platform ?? "mobile",
@@ -1752,7 +2447,7 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
1752
2447
  },
1753
2448
  consoleLogs: (consoleRef.current?.getEntries() ?? []).map((entry) => ({
1754
2449
  ...entry,
1755
- message: (0, import_core6.scrubPii)(entry.message)
2450
+ message: (0, import_core9.scrubPii)(entry.message)
1756
2451
  })),
1757
2452
  networkLogs: networkRef.current?.getEntries().map((entry) => ({
1758
2453
  ...entry,
@@ -1782,7 +2477,7 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
1782
2477
  const code = result.error?.code ?? "";
1783
2478
  const isCredentialError = code === "HTTP_401" || code === "HTTP_403" || code.includes("UNAUTHORIZED") || code.includes("FORBIDDEN") || code.includes("INVALID_API_KEY");
1784
2479
  if (isCredentialError) {
1785
- const where = apiEndpoint.replace(/\/$/, "") === import_core6.DEFAULT_API_ENDPOINT ? "Get the correct values at: https://kensaur.us/mushi-mushi/admin/projects" : "Get the correct values from your Mushi console's Projects page.";
2480
+ const where = apiEndpoint.replace(/\/$/, "") === import_core9.DEFAULT_API_ENDPOINT ? "Get the correct values at: https://kensaur.us/mushi-mushi/admin/projects" : "Get the correct values from your Mushi console's Projects page.";
1786
2481
  console.error(
1787
2482
  '[Mushi] Credentials rejected. Check your projectId and apiKey (must have "report:write" scope). ' + where
1788
2483
  );
@@ -1828,6 +2523,63 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
1828
2523
  if (rewardsFlushRef.current) clearInterval(rewardsFlushRef.current);
1829
2524
  };
1830
2525
  }, [config.rewards?.enabled, config.rewards?.trackActivity, config.rewards?.flushIntervalMs]);
2526
+ const eventTrackerRef = (0, import_react4.useRef)(null);
2527
+ const sessionTrackerRef = (0, import_react4.useRef)(null);
2528
+ (0, import_react4.useEffect)(() => {
2529
+ const getRoute = () => screenRef.current?.route ?? screenRef.current?.name ?? null;
2530
+ const tracker = createRNEventTracker({
2531
+ projectId: config.projectId,
2532
+ client: () => apiClientRef.current,
2533
+ getAnonId: async () => {
2534
+ await reporterTokenReadyRef.current;
2535
+ return reporterTokenRef.current;
2536
+ },
2537
+ getUserId: () => userRef.current?.id ?? null,
2538
+ getRoute,
2539
+ sdkVersion: MUSHI_SDK_VERSION,
2540
+ config: config.analytics,
2541
+ scrub: import_core9.scrubPii
2542
+ });
2543
+ eventTrackerRef.current = tracker;
2544
+ const sessions = config.trackSessions === false ? null : createRNSessionTracker({
2545
+ client: () => apiClientRef.current,
2546
+ getReporterToken: async () => {
2547
+ await reporterTokenReadyRef.current;
2548
+ return reporterTokenRef.current;
2549
+ },
2550
+ getRoute,
2551
+ consent: tracker,
2552
+ sessionId: sessionIdRef.current,
2553
+ sdkVersion: MUSHI_SDK_VERSION,
2554
+ userAgent: nativeUserAgent()
2555
+ });
2556
+ sessionTrackerRef.current = sessions;
2557
+ let sub;
2558
+ try {
2559
+ if (typeof import_react_native5.AppState?.addEventListener === "function") {
2560
+ sub = import_react_native5.AppState.addEventListener("change", (state) => {
2561
+ sessions?.onAppStateChange(state);
2562
+ if (state !== "active") tracker.flush().catch(() => {
2563
+ });
2564
+ });
2565
+ }
2566
+ } catch {
2567
+ }
2568
+ return () => {
2569
+ sub?.remove();
2570
+ sessions?.destroy();
2571
+ sessionTrackerRef.current = null;
2572
+ eventTrackerRef.current = null;
2573
+ tracker.destroy().catch(() => {
2574
+ });
2575
+ };
2576
+ }, [
2577
+ config.projectId,
2578
+ config.analytics?.enabled,
2579
+ config.analytics?.consent,
2580
+ config.analytics?.flushIntervalMs,
2581
+ config.trackSessions
2582
+ ]);
1831
2583
  const instance = (0, import_react4.useMemo)(
1832
2584
  () => ({
1833
2585
  open,
@@ -1845,6 +2597,7 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
1845
2597
  name: traits?.name,
1846
2598
  provider: traits?.provider
1847
2599
  };
2600
+ eventTrackerRef.current?.setIdentity(userId, traits ?? null);
1848
2601
  },
1849
2602
  setUser(user) {
1850
2603
  userRef.current = {
@@ -1853,18 +2606,22 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
1853
2606
  name: user.name,
1854
2607
  provider: user.provider
1855
2608
  };
2609
+ eventTrackerRef.current?.setIdentity(user.id, { email: user.email, name: user.name, provider: user.provider });
1856
2610
  },
1857
2611
  identifyWithToken(token) {
1858
2612
  userTokenRef.current = token && typeof token === "string" ? token : null;
1859
2613
  if (userTokenRef.current) {
1860
- const claims = (0, import_core6.parseIdentityToken)(userTokenRef.current);
2614
+ const claims = (0, import_core9.parseIdentityToken)(userTokenRef.current);
1861
2615
  if (claims?.sub) {
1862
2616
  userRef.current = {
1863
2617
  id: claims.sub,
1864
2618
  ...claims.email ? { email: claims.email } : {},
1865
2619
  ...claims.name ? { name: claims.name } : {}
1866
2620
  };
2621
+ eventTrackerRef.current?.setIdentity(claims.sub, null);
1867
2622
  }
2623
+ } else {
2624
+ eventTrackerRef.current?.setIdentity(null, null);
1868
2625
  }
1869
2626
  },
1870
2627
  publishPageContext(context) {
@@ -1877,6 +2634,7 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
1877
2634
  const prev = screenRef.current?.name;
1878
2635
  screenRef.current = screen;
1879
2636
  if (screen.name && screen.name !== prev) {
2637
+ sessionTrackerRef.current?.pageView(screen.route ?? screen.name);
1880
2638
  breadcrumbsRef.current.add({
1881
2639
  category: "navigation",
1882
2640
  level: "info",
@@ -1908,6 +2666,23 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
1908
2666
  const res = await client.listReporterComments(reportId, reporterTokenRef.current);
1909
2667
  return res.ok ? res.data.comments ?? [] : [];
1910
2668
  },
2669
+ async loadMyThread(reportId) {
2670
+ const client = apiClientRef.current;
2671
+ if (!client) return null;
2672
+ try {
2673
+ await reporterTokenReadyRef.current;
2674
+ const res = await client.listReporterComments(reportId, reporterTokenRef.current);
2675
+ return res.ok ? res.data?.comments ?? [] : null;
2676
+ } catch {
2677
+ return null;
2678
+ }
2679
+ },
2680
+ async markReportRead(reportId) {
2681
+ const client = apiClientRef.current;
2682
+ if (!client) return 0;
2683
+ await reporterTokenReadyRef.current;
2684
+ return markReporterReportRead(client, reporterTokenRef.current, reportId);
2685
+ },
1911
2686
  async replyToReport(reportId, body) {
1912
2687
  const client = apiClientRef.current;
1913
2688
  if (!client) return null;
@@ -1938,6 +2713,13 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
1938
2713
  if (!config.rewards?.enabled) return;
1939
2714
  activityQueueRef.current.push({ action, metadata });
1940
2715
  },
2716
+ // Product analytics (Users & Funnels)
2717
+ track(event, properties) {
2718
+ return eventTrackerRef.current?.track(event, properties) ?? false;
2719
+ },
2720
+ setConsent(state) {
2721
+ eventTrackerRef.current?.setConsent(state);
2722
+ },
1941
2723
  async getTier() {
1942
2724
  const userId = userRef.current?.id;
1943
2725
  const client = apiClientRef.current;
@@ -2008,7 +2790,8 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
2008
2790
  assistantLabel: config.assistant?.label,
2009
2791
  assistantGreeting: config.assistant?.greeting,
2010
2792
  assistantSuggestions: config.assistant?.suggestions,
2011
- inboxPollIntervalMs: config.widget?.inboxPollIntervalMs ?? 0
2793
+ inboxPollIntervalMs: config.widget?.inboxPollIntervalMs ?? 0,
2794
+ theme: config.widget?.theme
2012
2795
  }
2013
2796
  )
2014
2797
  ] });