@burdenoff/microfe-conversations 2026.527.1 → 2026.528.1

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.
Files changed (29) hide show
  1. package/dist/conversations/ConversationsUserRoutes.js +6 -6
  2. package/dist/conversations/ConversationsUserRoutes.js.map +1 -1
  3. package/dist/conversations/components/ConversationRow.js +176 -0
  4. package/dist/conversations/components/ConversationRow.js.map +1 -0
  5. package/dist/conversations/components/EmptyState.js +10 -21
  6. package/dist/conversations/components/EmptyState.js.map +1 -1
  7. package/dist/conversations/components/InboxLayout.js +185 -0
  8. package/dist/conversations/components/InboxLayout.js.map +1 -0
  9. package/dist/conversations/components/LazyPageBoundary.js.map +1 -1
  10. package/dist/conversations/components/PageLayout.js +16 -34
  11. package/dist/conversations/components/PageLayout.js.map +1 -1
  12. package/dist/conversations/components/PageSection.js +10 -19
  13. package/dist/conversations/components/PageSection.js.map +1 -1
  14. package/dist/conversations/components/lazyRetry.js.map +1 -1
  15. package/dist/conversations/hooks/useConversationQueries.js +48 -34
  16. package/dist/conversations/hooks/useConversationQueries.js.map +1 -1
  17. package/dist/conversations/hooks/useMessageQueries.js.map +1 -1
  18. package/dist/conversations/pages/ConversationAdminPages.js.map +1 -1
  19. package/dist/conversations/pages/ConversationListPage.js +187 -226
  20. package/dist/conversations/pages/ConversationListPage.js.map +1 -1
  21. package/dist/conversations/pages/ConversationsLandingPage.js +93 -103
  22. package/dist/conversations/pages/ConversationsLandingPage.js.map +1 -1
  23. package/dist/conversations/pages/ConversationsSettingsPage.js +201 -182
  24. package/dist/conversations/pages/ConversationsSettingsPage.js.map +1 -1
  25. package/dist/generated/wspace-operations.js +72 -56
  26. package/dist/generated/wspace-operations.js.map +1 -1
  27. package/package.json +2 -2
  28. package/dist/conversations/components/ConversationCard.js +0 -119
  29. package/dist/conversations/components/ConversationCard.js.map +0 -1
@@ -3,8 +3,8 @@ import { LazyPageBoundary as t } from "./components/LazyPageBoundary.js";
3
3
  import { Navigate as n, Route as r, Routes as i } from "react-router-dom";
4
4
  import { jsx as a, jsxs as o } from "react/jsx-runtime";
5
5
  //#region src/conversations/ConversationsUserRoutes.tsx
6
- var s = e(() => import("./pages/ConversationListPage.js").then((e) => ({ default: e.ConversationListPage }))), c = e(() => import("./pages/ConversationsLandingPage.js").then((e) => ({ default: e.ConversationsLandingPage }))), l = e(() => import("./pages/ConversationDetailPage.js").then((e) => ({ default: e.ConversationDetailPage }))), u = e(() => import("./pages/NewConversationPage.js").then((e) => ({ default: e.NewConversationPage }))), d = e(() => import("./pages/ConversationsSettingsPage.js").then((e) => ({ default: e.ConversationsSettingsPage })));
7
- function f() {
6
+ var s = e(() => import("./pages/ConversationListPage.js").then((e) => ({ default: e.ConversationListPage }))), c = e(() => import("./pages/ConversationsLandingPage.js").then((e) => ({ default: e.ConversationsLandingPage }))), l = e(() => import("./pages/NewConversationPage.js").then((e) => ({ default: e.NewConversationPage }))), u = e(() => import("./pages/ConversationsSettingsPage.js").then((e) => ({ default: e.ConversationsSettingsPage })));
7
+ function d() {
8
8
  return /* @__PURE__ */ a(t, {
9
9
  source: "user",
10
10
  children: /* @__PURE__ */ o(i, { children: [
@@ -18,15 +18,15 @@ function f() {
18
18
  }),
19
19
  /* @__PURE__ */ a(r, {
20
20
  path: "new",
21
- element: /* @__PURE__ */ a(u, {})
21
+ element: /* @__PURE__ */ a(l, {})
22
22
  }),
23
23
  /* @__PURE__ */ a(r, {
24
24
  path: "settings",
25
- element: /* @__PURE__ */ a(d, {})
25
+ element: /* @__PURE__ */ a(u, {})
26
26
  }),
27
27
  /* @__PURE__ */ a(r, {
28
28
  path: ":conversationId",
29
- element: /* @__PURE__ */ a(l, {})
29
+ element: /* @__PURE__ */ a(c, {})
30
30
  }),
31
31
  /* @__PURE__ */ a(r, {
32
32
  path: "*",
@@ -39,6 +39,6 @@ function f() {
39
39
  });
40
40
  }
41
41
  //#endregion
42
- export { f as ConversationsUserRoutes };
42
+ export { d as ConversationsUserRoutes };
43
43
 
44
44
  //# sourceMappingURL=ConversationsUserRoutes.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"ConversationsUserRoutes.js","names":[],"sources":["../../src/conversations/ConversationsUserRoutes.tsx"],"sourcesContent":["import { Navigate, Route, Routes } from \"react-router-dom\";\n\nimport { LazyPageBoundary } from \"./components/LazyPageBoundary\";\nimport { lazyRetry } from \"./components/lazyRetry\";\nimport type { ConversationListPageProps } from \"./pages/ConversationListPage\";\n\nconst ConversationListPage = lazyRetry<ConversationListPageProps>(() =>\n import(\"./pages/ConversationListPage\").then((m) => ({\n default: m.ConversationListPage,\n })),\n);\nconst ConversationsLandingPage = lazyRetry<Record<string, never>>(() =>\n import(\"./pages/ConversationsLandingPage\").then((m) => ({\n default: m.ConversationsLandingPage,\n })),\n);\n\nconst ConversationDetailPage = lazyRetry<Record<string, never>>(() =>\n import(\"./pages/ConversationDetailPage\").then((m) => ({\n default: m.ConversationDetailPage,\n })),\n);\nconst NewConversationPage = lazyRetry<Record<string, never>>(() =>\n import(\"./pages/NewConversationPage\").then((m) => ({\n default: m.NewConversationPage,\n })),\n);\nconst ConversationsSettingsPage = lazyRetry<Record<string, never>>(() =>\n import(\"./pages/ConversationsSettingsPage\").then((m) => ({\n default: m.ConversationsSettingsPage,\n })),\n);\n\n/**\n * User-facing routes for the Conversations microfrontend.\n *\n * Routes (relative to basePath, e.g., /conversations):\n * - / -> Conversation list page (inbox tabs)\n * - /list -> Conversation list page\n * - /new -> Create new conversation\n * - /:conversationId -> Conversation detail (chat view)\n * - /settings -> Personal notification settings\n */\nexport function ConversationsUserRoutes() {\n return (\n <LazyPageBoundary source=\"user\">\n <Routes>\n <Route index element={<ConversationsLandingPage />} />\n <Route path=\"list\" element={<ConversationListPage />} />\n <Route path=\"new\" element={<NewConversationPage />} />\n <Route path=\"settings\" element={<ConversationsSettingsPage />} />\n <Route path=\":conversationId\" element={<ConversationDetailPage />} />\n <Route path=\"*\" element={<Navigate to=\".\" replace />} />\n </Routes>\n </LazyPageBoundary>\n );\n}\n"],"mappings":";;;;;AAMA,IAAM,IAAuB,QAC3B,OAAO,mCAAgC,MAAM,OAAO,EAClD,SAAS,EAAE,sBACZ,EAAE,CACJ,EACK,IAA2B,QAC/B,OAAO,uCAAoC,MAAM,OAAO,EACtD,SAAS,EAAE,0BACZ,EAAE,CACJ,EAEK,IAAyB,QAC7B,OAAO,qCAAkC,MAAM,OAAO,EACpD,SAAS,EAAE,wBACZ,EAAE,CACJ,EACK,IAAsB,QAC1B,OAAO,kCAA+B,MAAM,OAAO,EACjD,SAAS,EAAE,qBACZ,EAAE,CACJ,EACK,IAA4B,QAChC,OAAO,wCAAqC,MAAM,OAAO,EACvD,SAAS,EAAE,2BACZ,EAAE,CACJ;AAYD,SAAgB,IAA0B;AACxC,QACE,kBAAC,GAAD;EAAkB,QAAO;YACvB,kBAAC,GAAD,EAAA,UAAA;GACE,kBAAC,GAAD;IAAO,OAAA;IAAM,SAAS,kBAAC,GAAD,EAA4B,CAAA;IAAI,CAAA;GACtD,kBAAC,GAAD;IAAO,MAAK;IAAO,SAAS,kBAAC,GAAD,EAAwB,CAAA;IAAI,CAAA;GACxD,kBAAC,GAAD;IAAO,MAAK;IAAM,SAAS,kBAAC,GAAD,EAAuB,CAAA;IAAI,CAAA;GACtD,kBAAC,GAAD;IAAO,MAAK;IAAW,SAAS,kBAAC,GAAD,EAA6B,CAAA;IAAI,CAAA;GACjE,kBAAC,GAAD;IAAO,MAAK;IAAkB,SAAS,kBAAC,GAAD,EAA0B,CAAA;IAAI,CAAA;GACrE,kBAAC,GAAD;IAAO,MAAK;IAAI,SAAS,kBAAC,GAAD;KAAU,IAAG;KAAI,SAAA;KAAU,CAAA;IAAI,CAAA;GACjD,EAAA,CAAA;EACQ,CAAA"}
1
+ {"version":3,"file":"ConversationsUserRoutes.js","names":[],"sources":["../../src/conversations/ConversationsUserRoutes.tsx"],"sourcesContent":["import { Navigate, Route, Routes } from \"react-router-dom\";\n\nimport { LazyPageBoundary } from \"./components/LazyPageBoundary\";\nimport { lazyRetry } from \"./components/lazyRetry\";\nimport type { ConversationListPageProps } from \"./pages/ConversationListPage\";\n\nconst ConversationListPage = lazyRetry<ConversationListPageProps>(() =>\n import(\"./pages/ConversationListPage\").then((m) => ({\n default: m.ConversationListPage,\n })),\n);\nconst ConversationsLandingPage = lazyRetry<Record<string, never>>(() =>\n import(\"./pages/ConversationsLandingPage\").then((m) => ({\n default: m.ConversationsLandingPage,\n })),\n);\n\nconst NewConversationPage = lazyRetry<Record<string, never>>(() =>\n import(\"./pages/NewConversationPage\").then((m) => ({\n default: m.NewConversationPage,\n })),\n);\nconst ConversationsSettingsPage = lazyRetry<Record<string, never>>(() =>\n import(\"./pages/ConversationsSettingsPage\").then((m) => ({\n default: m.ConversationsSettingsPage,\n })),\n);\n\n/**\n * User-facing routes for the Conversations microfrontend.\n *\n * Routes (relative to basePath, e.g., /conversations):\n * - / -> Inbox (two-pane shell, no conversation selected)\n * - /:conversationId -> Inbox with conversation detail in right pane\n * - /list -> Legacy standalone list (kept for deep-linked callers)\n * - /new -> Create new conversation\n * - /settings -> Personal notification settings\n *\n * Both `/` and `/:conversationId` mount `ConversationsLandingPage`. The page\n * reads `useParams().conversationId` and renders the `ConversationDetailPage`\n * in the right pane when set, otherwise the inbox empty state. This keeps\n * the two-pane experience consistent across deep links and sidebar clicks\n * — no full-page nav happens when a user picks a conversation on md+ screens.\n */\nexport function ConversationsUserRoutes() {\n return (\n <LazyPageBoundary source=\"user\">\n <Routes>\n <Route index element={<ConversationsLandingPage />} />\n <Route path=\"list\" element={<ConversationListPage />} />\n <Route path=\"new\" element={<NewConversationPage />} />\n <Route path=\"settings\" element={<ConversationsSettingsPage />} />\n <Route path=\":conversationId\" element={<ConversationsLandingPage />} />\n <Route path=\"*\" element={<Navigate to=\".\" replace />} />\n </Routes>\n </LazyPageBoundary>\n );\n}\n"],"mappings":";;;;;AAMA,IAAM,IAAuB,QAC3B,OAAO,mCAAgC,MAAM,OAAO,EAClD,SAAS,EAAE,sBACZ,EAAE,CACJ,EACK,IAA2B,QAC/B,OAAO,uCAAoC,MAAM,OAAO,EACtD,SAAS,EAAE,0BACZ,EAAE,CACJ,EAEK,IAAsB,QAC1B,OAAO,kCAA+B,MAAM,OAAO,EACjD,SAAS,EAAE,qBACZ,EAAE,CACJ,EACK,IAA4B,QAChC,OAAO,wCAAqC,MAAM,OAAO,EACvD,SAAS,EAAE,2BACZ,EAAE,CACJ;AAkBD,SAAgB,IAA0B;AACxC,QACE,kBAAC,GAAD;EAAkB,QAAO;YACvB,kBAAC,GAAD,EAAA,UAAA;GACE,kBAAC,GAAD;IAAO,OAAA;IAAM,SAAS,kBAAC,GAAD,EAA4B,CAAA;IAAI,CAAA;GACtD,kBAAC,GAAD;IAAO,MAAK;IAAO,SAAS,kBAAC,GAAD,EAAwB,CAAA;IAAI,CAAA;GACxD,kBAAC,GAAD;IAAO,MAAK;IAAM,SAAS,kBAAC,GAAD,EAAuB,CAAA;IAAI,CAAA;GACtD,kBAAC,GAAD;IAAO,MAAK;IAAW,SAAS,kBAAC,GAAD,EAA6B,CAAA;IAAI,CAAA;GACjE,kBAAC,GAAD;IAAO,MAAK;IAAkB,SAAS,kBAAC,GAAD,EAA4B,CAAA;IAAI,CAAA;GACvE,kBAAC,GAAD;IAAO,MAAK;IAAI,SAAS,kBAAC,GAAD;KAAU,IAAG;KAAI,SAAA;KAAU,CAAA;IAAI,CAAA;GACjD,EAAA,CAAA;EACQ,CAAA"}
@@ -0,0 +1,176 @@
1
+ import { cn as e } from "../utils/cn.js";
2
+ import { useConversationActions as t } from "../hooks/useConversationQueries.js";
3
+ import { memo as n, useState as r } from "react";
4
+ import { ArchiveIcon as i, ArchiveRestore as a, Bell as o, BellOff as s, Bot as c, Hash as l, HeadphonesIcon as u, LogOut as d, Megaphone as f, MessageCircle as p, MoreHorizontal as m, Pin as h, PinOff as g, Users as _, VolumeX as v } from "lucide-react";
5
+ import { Fragment as y, jsx as b, jsxs as x } from "react/jsx-runtime";
6
+ import { useI18n as S } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
7
+ import { DropdownMenu as C, DropdownMenuContent as w, DropdownMenuItem as T, DropdownMenuSeparator as E, DropdownMenuTrigger as D } from "@burdenoff/fe-libs/ui";
8
+ //#region src/conversations/components/ConversationRow.tsx
9
+ function O({ type: e, size: t = 16, className: n }) {
10
+ let r = {
11
+ size: t,
12
+ className: n
13
+ };
14
+ switch (e) {
15
+ case "DIRECT": return /* @__PURE__ */ b(p, { ...r });
16
+ case "GROUP": return /* @__PURE__ */ b(_, { ...r });
17
+ case "CHANNEL": return /* @__PURE__ */ b(l, { ...r });
18
+ case "BROADCAST":
19
+ case "ANNOUNCEMENT": return /* @__PURE__ */ b(f, { ...r });
20
+ case "BOT": return /* @__PURE__ */ b(c, { ...r });
21
+ case "SUPPORT": return /* @__PURE__ */ b(u, { ...r });
22
+ default: return /* @__PURE__ */ b(p, { ...r });
23
+ }
24
+ }
25
+ function k(e, t) {
26
+ let n = /* @__PURE__ */ new Date(), r = new Date(e), i = n.getTime() - r.getTime(), a = Math.floor(i / 1e3 / 60), o = Math.floor(a / 60), s = Math.floor(o / 24);
27
+ return a < 1 ? t.now : a < 60 ? `${a}m` : o < 24 ? `${o}h` : s === 1 ? t.yesterday : s < 7 ? `${s}d` : r.toLocaleDateString(void 0, {
28
+ month: "short",
29
+ day: "numeric"
30
+ });
31
+ }
32
+ function A(e) {
33
+ return e.split(" ").slice(0, 2).map((e) => e[0]).join("").toUpperCase();
34
+ }
35
+ var j = n(function({ conversation: n, isActive: c = !1, onClick: u, onLeave: f }) {
36
+ let { title: p, type: j, state: M, lastMessage: N, unreadCount: P, isPinned: F, isMuted: I, memberCount: L, updatedAt: R, avatar: z } = n, B = P > 0, V = M === "ARCHIVED", { t: H } = S(), U = (e, t) => {
37
+ let n = H(e);
38
+ return n === e ? t : n;
39
+ }, W = {
40
+ now: U("conversations.now", "now"),
41
+ yesterday: U("conversations.yesterday", "Yesterday")
42
+ }, [G, K] = r(!1), { archiveConversation: q, unarchiveConversation: J, pinConversation: Y, unpinConversation: X, muteConversation: Z, unmuteConversation: Q } = t(), $ = (e) => {
43
+ e.stopPropagation();
44
+ };
45
+ return /* @__PURE__ */ x("div", {
46
+ "data-tour": "conversation-row",
47
+ className: e("group relative flex w-full min-w-0 items-start gap-3 rounded-md px-3 py-2.5 transition-colors", "cursor-pointer select-none", c ? "bg-bg-sunken border-l-2 border-l-action-primary-bg" : "border-l-2 border-l-transparent hover:bg-action-ghost-bg-hover", B && !c && "bg-bg-elevated/40"),
48
+ role: "button",
49
+ tabIndex: 0,
50
+ onClick: () => u?.(n),
51
+ onKeyDown: (e) => {
52
+ (e.key === "Enter" || e.key === " ") && (e.preventDefault(), u?.(n));
53
+ },
54
+ "aria-label": U("conversations.openConversation", "Open conversation") + ": " + p,
55
+ children: [
56
+ /* @__PURE__ */ b("div", {
57
+ className: e("flex-shrink-0 flex h-9 w-9 items-center justify-center rounded-full text-xs font-semibold overflow-hidden", j === "DIRECT" && "bg-status-info-bg-subtle text-status-info-text", j === "CHANNEL" && "bg-status-success-bg-subtle text-status-success-text", (j === "GROUP" || j === "BROADCAST" || j === "ANNOUNCEMENT" || j === "BOT" || j === "SUPPORT") && "bg-bg-sunken text-text-primary"),
58
+ "aria-hidden": "true",
59
+ children: z ? /* @__PURE__ */ b("img", {
60
+ src: z,
61
+ alt: "",
62
+ className: "h-full w-full object-cover"
63
+ }) : j === "DIRECT" ? A(p) : j === "CHANNEL" ? /* @__PURE__ */ b(l, { size: 16 }) : /* @__PURE__ */ b(O, {
64
+ type: j,
65
+ size: 16
66
+ })
67
+ }),
68
+ /* @__PURE__ */ x("div", {
69
+ className: "flex min-w-0 flex-1 flex-col gap-0.5",
70
+ children: [/* @__PURE__ */ x("div", {
71
+ className: "flex items-center justify-between gap-2",
72
+ children: [/* @__PURE__ */ x("div", {
73
+ className: "flex min-w-0 items-center gap-1.5",
74
+ children: [
75
+ /* @__PURE__ */ b("span", {
76
+ className: e("truncate text-sm", B ? "font-semibold text-text-primary" : "font-medium text-text-primary"),
77
+ children: p
78
+ }),
79
+ F && /* @__PURE__ */ b(h, {
80
+ size: 11,
81
+ className: "flex-shrink-0 text-text-secondary",
82
+ "aria-label": U("conversations.pinnedLabel", "Pinned")
83
+ }),
84
+ I && /* @__PURE__ */ b(v, {
85
+ size: 11,
86
+ className: "flex-shrink-0 text-text-secondary",
87
+ "aria-label": U("conversations.mutedLabel", "Muted")
88
+ })
89
+ ]
90
+ }), /* @__PURE__ */ b("span", {
91
+ className: e("flex-shrink-0 text-[11px]", B ? "font-medium text-action-primary-bg" : "text-text-secondary"),
92
+ children: k(N?.createdAt ?? R, W)
93
+ })]
94
+ }), /* @__PURE__ */ x("div", {
95
+ className: "flex items-center justify-between gap-2",
96
+ children: [/* @__PURE__ */ b("p", {
97
+ className: e("min-w-0 truncate break-all text-xs", B ? "text-text-primary/80" : "text-text-secondary"),
98
+ children: N ? /* @__PURE__ */ x(y, { children: [N.sender.name && /* @__PURE__ */ x("span", {
99
+ className: "font-medium",
100
+ children: [N.sender.name, ": "]
101
+ }), N.content || U("conversations.sentAttachment", "Sent an attachment")] }) : /* @__PURE__ */ b("span", {
102
+ className: "italic",
103
+ children: U("conversations.noMessages", "No messages yet")
104
+ })
105
+ }), /* @__PURE__ */ x("div", {
106
+ className: "flex flex-shrink-0 items-center gap-1.5",
107
+ children: [j !== "DIRECT" && /* @__PURE__ */ x("span", {
108
+ className: "inline-flex items-center gap-0.5 text-[10px] text-text-secondary",
109
+ "aria-label": `${L} ${U("conversations.membersLabel", "members")}`,
110
+ children: [/* @__PURE__ */ b(_, {
111
+ size: 10,
112
+ "aria-hidden": "true"
113
+ }), L]
114
+ }), B && /* @__PURE__ */ b("span", {
115
+ className: "inline-flex h-[18px] min-w-[18px] items-center justify-center rounded-full bg-action-primary-bg px-1 text-[10px] font-bold text-action-primary-text",
116
+ "aria-label": `${P} ${U("conversations.unreadCountLabel", "unread")}`,
117
+ children: P > 99 ? "99+" : P
118
+ })]
119
+ })]
120
+ })]
121
+ }),
122
+ /* @__PURE__ */ x(C, {
123
+ open: G,
124
+ onOpenChange: K,
125
+ children: [/* @__PURE__ */ b(D, {
126
+ asChild: !0,
127
+ children: /* @__PURE__ */ b("button", {
128
+ type: "button",
129
+ onClick: $,
130
+ onKeyDown: $,
131
+ "aria-label": U("conversations.rowMenuLabel", "Conversation actions"),
132
+ className: e("absolute right-2 top-2 flex h-7 w-7 items-center justify-center rounded-md transition-opacity", "text-text-secondary hover:bg-action-ghost-bg-hover hover:text-text-primary", "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-action-primary-border", G ? "opacity-100" : "opacity-0 group-hover:opacity-100 group-focus-within:opacity-100"),
133
+ children: /* @__PURE__ */ b(m, {
134
+ size: 14,
135
+ "aria-hidden": "true"
136
+ })
137
+ })
138
+ }), /* @__PURE__ */ x(w, {
139
+ align: "end",
140
+ className: "min-w-[180px]",
141
+ onClick: $,
142
+ children: [
143
+ /* @__PURE__ */ b(T, {
144
+ onSelect: () => {
145
+ F ? X(n.id) : Y(n.id);
146
+ },
147
+ children: F ? /* @__PURE__ */ x(y, { children: [/* @__PURE__ */ b(g, { size: 14 }), U("conversations.unpin", "Unpin")] }) : /* @__PURE__ */ x(y, { children: [/* @__PURE__ */ b(h, { size: 14 }), U("conversations.pin", "Pin")] })
148
+ }),
149
+ /* @__PURE__ */ b(T, {
150
+ onSelect: () => {
151
+ I ? Q(n.id) : Z(n.id);
152
+ },
153
+ children: I ? /* @__PURE__ */ x(y, { children: [/* @__PURE__ */ b(o, { size: 14 }), U("conversations.unmute", "Unmute")] }) : /* @__PURE__ */ x(y, { children: [/* @__PURE__ */ b(s, { size: 14 }), U("conversations.mute", "Mute")] })
154
+ }),
155
+ /* @__PURE__ */ b(E, {}),
156
+ /* @__PURE__ */ b(T, {
157
+ onSelect: () => {
158
+ V ? J(n.id) : q(n.id);
159
+ },
160
+ children: V ? /* @__PURE__ */ x(y, { children: [/* @__PURE__ */ b(a, { size: 14 }), U("conversations.unarchive", "Unarchive")] }) : /* @__PURE__ */ x(y, { children: [/* @__PURE__ */ b(i, { size: 14 }), U("conversations.archive", "Archive")] })
161
+ }),
162
+ (j === "GROUP" || j === "CHANNEL") && f && /* @__PURE__ */ x(y, { children: [/* @__PURE__ */ b(E, {}), /* @__PURE__ */ x(T, {
163
+ onSelect: () => f(n),
164
+ className: "text-status-error-text focus:text-status-error-text",
165
+ children: [/* @__PURE__ */ b(d, { size: 14 }), U("conversations.leave", "Leave")]
166
+ })] })
167
+ ]
168
+ })]
169
+ })
170
+ ]
171
+ });
172
+ });
173
+ //#endregion
174
+ export { j as ConversationRow };
175
+
176
+ //# sourceMappingURL=ConversationRow.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ConversationRow.js","names":[],"sources":["../../../src/conversations/components/ConversationRow.tsx"],"sourcesContent":["import { memo, useState } from \"react\";\nimport {\n ArchiveIcon,\n ArchiveRestore,\n Bell,\n BellOff,\n Bot,\n Hash,\n HeadphonesIcon,\n LogOut,\n Megaphone,\n MessageCircle,\n MoreHorizontal,\n Pin,\n PinOff,\n Users,\n VolumeX,\n} from \"lucide-react\";\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n} from \"@burdenoff/fe-libs/ui\";\nimport { useI18n } from \"@burdenoff/fe-libs/shared/providers/shell/I18nProvider\";\nimport { cn } from \"../utils/cn\";\nimport { useConversationActions } from \"../hooks/useConversationQueries\";\nimport type { Conversation } from \"../types\";\n\nexport interface ConversationRowProps {\n conversation: Conversation;\n isActive?: boolean;\n onClick?: (conversation: Conversation) => void;\n /** Optional leave handler — only the host has the membership mutation. */\n onLeave?: (conversation: Conversation) => void;\n}\n\nfunction ConversationTypeIcon({\n type,\n size = 16,\n className,\n}: {\n type: Conversation[\"type\"];\n size?: number;\n className?: string;\n}) {\n const props = { size, className };\n switch (type) {\n case \"DIRECT\":\n return <MessageCircle {...props} />;\n case \"GROUP\":\n return <Users {...props} />;\n case \"CHANNEL\":\n return <Hash {...props} />;\n case \"BROADCAST\":\n case \"ANNOUNCEMENT\":\n return <Megaphone {...props} />;\n case \"BOT\":\n return <Bot {...props} />;\n case \"SUPPORT\":\n return <HeadphonesIcon {...props} />;\n default:\n return <MessageCircle {...props} />;\n }\n}\n\nfunction formatRelativeTime(\n dateString: string,\n labels: { now: string; yesterday: string },\n): string {\n const now = new Date();\n const date = new Date(dateString);\n const diffMs = now.getTime() - date.getTime();\n const diffMin = Math.floor(diffMs / 1000 / 60);\n const diffHr = Math.floor(diffMin / 60);\n const diffDay = Math.floor(diffHr / 24);\n\n if (diffMin < 1) return labels.now;\n if (diffMin < 60) return `${diffMin}m`;\n if (diffHr < 24) return `${diffHr}h`;\n if (diffDay === 1) return labels.yesterday;\n if (diffDay < 7) return `${diffDay}d`;\n return date.toLocaleDateString(undefined, { month: \"short\", day: \"numeric\" });\n}\n\nfunction getInitials(title: string): string {\n return title\n .split(\" \")\n .slice(0, 2)\n .map((word) => word[0])\n .join(\"\")\n .toUpperCase();\n}\n\n/**\n * ConversationRow — inbox-style list row.\n *\n * Visual hierarchy:\n * [avatar] Title · 2h\n * last message preview (badge)\n *\n * On hover the kebab (3-dots) menu reveals so the user can pin/mute/archive\n * without opening the conversation first. Wires straight into\n * `useConversationActions` for archive/unarchive/pin/mute.\n */\nexport const ConversationRow = memo(function ConversationRow({\n conversation,\n isActive = false,\n onClick,\n onLeave,\n}: ConversationRowProps) {\n const {\n title,\n type,\n state,\n lastMessage,\n unreadCount,\n isPinned,\n isMuted,\n memberCount,\n updatedAt,\n avatar,\n } = conversation;\n\n const hasUnread = unreadCount > 0;\n const isArchived = state === \"ARCHIVED\";\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const value = t(key);\n return value === key ? fallback : value;\n };\n const timeLabels = {\n now: tr(\"conversations.now\", \"now\"),\n yesterday: tr(\"conversations.yesterday\", \"Yesterday\"),\n };\n\n // Track menu open so the kebab stays visible even when the cursor leaves\n // the row (otherwise the radix portal closes on every hover-out).\n const [menuOpen, setMenuOpen] = useState(false);\n\n const {\n archiveConversation,\n unarchiveConversation,\n pinConversation,\n unpinConversation,\n muteConversation,\n unmuteConversation,\n } = useConversationActions();\n\n // Stop click bubbling from the kebab into the row's click handler — opening\n // the menu must not also open the conversation.\n const stop = (event: React.MouseEvent | React.KeyboardEvent) => {\n event.stopPropagation();\n };\n\n return (\n <div\n data-tour=\"conversation-row\"\n className={cn(\n \"group relative flex w-full min-w-0 items-start gap-3 rounded-md px-3 py-2.5 transition-colors\",\n \"cursor-pointer select-none\",\n isActive\n ? \"bg-bg-sunken border-l-2 border-l-action-primary-bg\"\n : \"border-l-2 border-l-transparent hover:bg-action-ghost-bg-hover\",\n hasUnread && !isActive && \"bg-bg-elevated/40\",\n )}\n role=\"button\"\n tabIndex={0}\n onClick={() => onClick?.(conversation)}\n onKeyDown={(event) => {\n if (event.key === \"Enter\" || event.key === \" \") {\n event.preventDefault();\n onClick?.(conversation);\n }\n }}\n aria-label={\n tr(\"conversations.openConversation\", \"Open conversation\") + \": \" + title\n }\n >\n {/* Avatar / type icon */}\n <div\n className={cn(\n \"flex-shrink-0 flex h-9 w-9 items-center justify-center rounded-full text-xs font-semibold overflow-hidden\",\n type === \"DIRECT\" && \"bg-status-info-bg-subtle text-status-info-text\",\n type === \"CHANNEL\" &&\n \"bg-status-success-bg-subtle text-status-success-text\",\n (type === \"GROUP\" ||\n type === \"BROADCAST\" ||\n type === \"ANNOUNCEMENT\" ||\n type === \"BOT\" ||\n type === \"SUPPORT\") &&\n \"bg-bg-sunken text-text-primary\",\n )}\n aria-hidden=\"true\"\n >\n {avatar ? (\n <img src={avatar} alt=\"\" className=\"h-full w-full object-cover\" />\n ) : type === \"DIRECT\" ? (\n getInitials(title)\n ) : type === \"CHANNEL\" ? (\n <Hash size={16} />\n ) : (\n <ConversationTypeIcon type={type} size={16} />\n )}\n </div>\n\n {/* Body */}\n <div className=\"flex min-w-0 flex-1 flex-col gap-0.5\">\n <div className=\"flex items-center justify-between gap-2\">\n <div className=\"flex min-w-0 items-center gap-1.5\">\n <span\n className={cn(\n \"truncate text-sm\",\n hasUnread\n ? \"font-semibold text-text-primary\"\n : \"font-medium text-text-primary\",\n )}\n >\n {title}\n </span>\n {isPinned && (\n <Pin\n size={11}\n className=\"flex-shrink-0 text-text-secondary\"\n aria-label={tr(\"conversations.pinnedLabel\", \"Pinned\")}\n />\n )}\n {isMuted && (\n <VolumeX\n size={11}\n className=\"flex-shrink-0 text-text-secondary\"\n aria-label={tr(\"conversations.mutedLabel\", \"Muted\")}\n />\n )}\n </div>\n\n <span\n className={cn(\n \"flex-shrink-0 text-[11px]\",\n hasUnread\n ? \"font-medium text-action-primary-bg\"\n : \"text-text-secondary\",\n )}\n >\n {formatRelativeTime(\n lastMessage?.createdAt ?? updatedAt,\n timeLabels,\n )}\n </span>\n </div>\n\n <div className=\"flex items-center justify-between gap-2\">\n <p\n className={cn(\n \"min-w-0 truncate break-all text-xs\",\n hasUnread ? \"text-text-primary/80\" : \"text-text-secondary\",\n )}\n >\n {lastMessage ? (\n <>\n {lastMessage.sender.name && (\n <span className=\"font-medium\">\n {lastMessage.sender.name}\n {\": \"}\n </span>\n )}\n {lastMessage.content ||\n tr(\"conversations.sentAttachment\", \"Sent an attachment\")}\n </>\n ) : (\n <span className=\"italic\">\n {tr(\"conversations.noMessages\", \"No messages yet\")}\n </span>\n )}\n </p>\n\n <div className=\"flex flex-shrink-0 items-center gap-1.5\">\n {type !== \"DIRECT\" && (\n <span\n className=\"inline-flex items-center gap-0.5 text-[10px] text-text-secondary\"\n aria-label={`${memberCount} ${tr(\"conversations.membersLabel\", \"members\")}`}\n >\n <Users size={10} aria-hidden=\"true\" />\n {memberCount}\n </span>\n )}\n {hasUnread && (\n <span\n className=\"inline-flex h-[18px] min-w-[18px] items-center justify-center rounded-full bg-action-primary-bg px-1 text-[10px] font-bold text-action-primary-text\"\n aria-label={`${unreadCount} ${tr(\"conversations.unreadCountLabel\", \"unread\")}`}\n >\n {unreadCount > 99 ? \"99+\" : unreadCount}\n </span>\n )}\n </div>\n </div>\n </div>\n\n {/* Kebab menu — hidden until hover or menu open */}\n <DropdownMenu open={menuOpen} onOpenChange={setMenuOpen}>\n <DropdownMenuTrigger asChild>\n <button\n type=\"button\"\n onClick={stop}\n onKeyDown={stop}\n aria-label={tr(\n \"conversations.rowMenuLabel\",\n \"Conversation actions\",\n )}\n className={cn(\n \"absolute right-2 top-2 flex h-7 w-7 items-center justify-center rounded-md transition-opacity\",\n \"text-text-secondary hover:bg-action-ghost-bg-hover hover:text-text-primary\",\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-action-primary-border\",\n menuOpen\n ? \"opacity-100\"\n : \"opacity-0 group-hover:opacity-100 group-focus-within:opacity-100\",\n )}\n >\n <MoreHorizontal size={14} aria-hidden=\"true\" />\n </button>\n </DropdownMenuTrigger>\n <DropdownMenuContent\n align=\"end\"\n className=\"min-w-[180px]\"\n onClick={stop}\n >\n <DropdownMenuItem\n onSelect={() => {\n void (isPinned\n ? unpinConversation(conversation.id)\n : pinConversation(conversation.id));\n }}\n >\n {isPinned ? (\n <>\n <PinOff size={14} />\n {tr(\"conversations.unpin\", \"Unpin\")}\n </>\n ) : (\n <>\n <Pin size={14} />\n {tr(\"conversations.pin\", \"Pin\")}\n </>\n )}\n </DropdownMenuItem>\n <DropdownMenuItem\n onSelect={() => {\n void (isMuted\n ? unmuteConversation(conversation.id)\n : muteConversation(conversation.id));\n }}\n >\n {isMuted ? (\n <>\n <Bell size={14} />\n {tr(\"conversations.unmute\", \"Unmute\")}\n </>\n ) : (\n <>\n <BellOff size={14} />\n {tr(\"conversations.mute\", \"Mute\")}\n </>\n )}\n </DropdownMenuItem>\n <DropdownMenuSeparator />\n <DropdownMenuItem\n onSelect={() => {\n void (isArchived\n ? unarchiveConversation(conversation.id)\n : archiveConversation(conversation.id));\n }}\n >\n {isArchived ? (\n <>\n <ArchiveRestore size={14} />\n {tr(\"conversations.unarchive\", \"Unarchive\")}\n </>\n ) : (\n <>\n <ArchiveIcon size={14} />\n {tr(\"conversations.archive\", \"Archive\")}\n </>\n )}\n </DropdownMenuItem>\n {(type === \"GROUP\" || type === \"CHANNEL\") && onLeave && (\n <>\n <DropdownMenuSeparator />\n <DropdownMenuItem\n onSelect={() => onLeave(conversation)}\n className=\"text-status-error-text focus:text-status-error-text\"\n >\n <LogOut size={14} />\n {tr(\"conversations.leave\", \"Leave\")}\n </DropdownMenuItem>\n </>\n )}\n </DropdownMenuContent>\n </DropdownMenu>\n </div>\n );\n});\n"],"mappings":"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"}
@@ -1,27 +1,16 @@
1
- import { cn as e } from "../utils/cn.js";
2
- import { jsx as t, jsxs as n } from "react/jsx-runtime";
1
+ import { jsx as e } from "react/jsx-runtime";
2
+ import { EmptyState as t } from "@burdenoff/fe-libs/ui";
3
3
  //#region src/conversations/components/EmptyState.tsx
4
- function r({ icon: r, title: i, description: a, action: o, className: s }) {
5
- return /* @__PURE__ */ n("div", {
6
- className: e("flex flex-col items-center justify-center", "py-12 sm:py-16 px-4", "text-center", "animate-in fade-in-0 zoom-in-95 duration-300", s),
7
- children: [
8
- r && /* @__PURE__ */ t("div", {
9
- className: "mb-4 p-4 rounded-2xl bg-muted/50 text-muted-foreground",
10
- children: r
11
- }),
12
- /* @__PURE__ */ t("h3", {
13
- className: "text-lg font-semibold text-foreground mb-1",
14
- children: i
15
- }),
16
- a && /* @__PURE__ */ t("p", {
17
- className: "text-sm text-muted-foreground max-w-sm mb-4",
18
- children: a
19
- }),
20
- o
21
- ]
4
+ function n({ icon: n, title: r, description: i, action: a, className: o }) {
5
+ return /* @__PURE__ */ e(t, {
6
+ icon: n,
7
+ title: r,
8
+ description: i,
9
+ action: a,
10
+ className: o
22
11
  });
23
12
  }
24
13
  //#endregion
25
- export { r as EmptyState };
14
+ export { n as EmptyState };
26
15
 
27
16
  //# sourceMappingURL=EmptyState.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"EmptyState.js","names":[],"sources":["../../../src/conversations/components/EmptyState.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport { cn } from \"../utils/cn\";\n\n/**\n * EmptyState - A consistent empty state component\n */\nexport function EmptyState({\n icon,\n title,\n description,\n action,\n className,\n}: {\n icon?: ReactNode;\n title: string;\n description?: string;\n action?: ReactNode;\n className?: string;\n}) {\n return (\n <div\n className={cn(\n \"flex flex-col items-center justify-center\",\n \"py-12 sm:py-16 px-4\",\n \"text-center\",\n \"animate-in fade-in-0 zoom-in-95 duration-300\",\n className,\n )}\n >\n {icon && (\n <div className=\"mb-4 p-4 rounded-2xl bg-muted/50 text-muted-foreground\">\n {icon}\n </div>\n )}\n <h3 className=\"text-lg font-semibold text-foreground mb-1\">{title}</h3>\n {description && (\n <p className=\"text-sm text-muted-foreground max-w-sm mb-4\">\n {description}\n </p>\n )}\n {action}\n </div>\n );\n}\n"],"mappings":";;;AAMA,SAAgB,EAAW,EACzB,SACA,UACA,gBACA,WACA,gBAOC;AACD,QACE,kBAAC,OAAD;EACE,WAAW,EACT,6CACA,uBACA,eACA,gDACA,EACD;YAPH;GASG,KACC,kBAAC,OAAD;IAAK,WAAU;cACZ;IACG,CAAA;GAER,kBAAC,MAAD;IAAI,WAAU;cAA8C;IAAW,CAAA;GACtE,KACC,kBAAC,KAAD;IAAG,WAAU;cACV;IACC,CAAA;GAEL;GACG"}
1
+ {"version":3,"file":"EmptyState.js","names":[],"sources":["../../../src/conversations/components/EmptyState.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport { EmptyState as CanonicalEmptyState } from \"@burdenoff/fe-libs/ui\";\n\n/**\n * Local wrapper around `EmptyState` from `@burdenoff/fe-libs/ui`. Keeps the\n * existing microfe-conversations call sites stable while routing all\n * empty-state rendering through the canonical primitive.\n */\nexport function EmptyState({\n icon,\n title,\n description,\n action,\n className,\n}: {\n icon?: ReactNode;\n title: string;\n description?: string;\n action?: ReactNode;\n className?: string;\n}) {\n return (\n <CanonicalEmptyState\n icon={icon}\n title={title}\n description={description}\n action={action}\n className={className}\n />\n );\n}\n"],"mappings":";;;AAQA,SAAgB,EAAW,EACzB,SACA,UACA,gBACA,WACA,gBAOC;AACD,QACE,kBAAC,GAAD;EACQ;EACC;EACM;EACL;EACG;EACX,CAAA"}
@@ -0,0 +1,185 @@
1
+ import { useConversations as e } from "../providers/ConversationsProvider.js";
2
+ import { joinPath as t } from "../utils/navigation.js";
3
+ import { useTr as n } from "../utils/i18n.js";
4
+ import { cn as r } from "../utils/cn.js";
5
+ import { useConversationsStore as i } from "../store/conversationsStore.js";
6
+ import { useCallback as a, useMemo as o } from "react";
7
+ import { ArrowLeft as s, MessageSquarePlus as c, Search as l, X as u } from "lucide-react";
8
+ import { useLocation as d, useNavigate as f, useParams as p } from "react-router-dom";
9
+ import { jsx as m, jsxs as h } from "react/jsx-runtime";
10
+ import { Tabs as g } from "@burdenoff/fe-libs/chrome";
11
+ import { Button as _ } from "@burdenoff/fe-libs/ui";
12
+ //#region src/conversations/components/InboxLayout.tsx
13
+ function v({ activeTab: v, onTabChange: b, archivedEnabled: x = !0, searchQuery: S, onSearchChange: C, filterChips: w, listSlot: T, detailSlot: E, emptyDetailSlot: D }) {
14
+ let O = f(), k = n(), { basePath: A } = e(), { setActiveConversationId: j } = i(), M = p(), N = d(), P = !!M.conversationId && !!E, F = o(() => {
15
+ let e = [
16
+ {
17
+ id: "all",
18
+ label: k("conversations.tabs.all", "All")
19
+ },
20
+ {
21
+ id: "direct",
22
+ label: k("conversations.tabs.direct", "Direct")
23
+ },
24
+ {
25
+ id: "groups",
26
+ label: k("conversations.tabs.groups", "Groups")
27
+ },
28
+ {
29
+ id: "channels",
30
+ label: k("conversations.tabs.channels", "Channels")
31
+ }
32
+ ];
33
+ return x && e.push({
34
+ id: "archived",
35
+ label: k("conversations.tabs.archived", "Archived")
36
+ }), e;
37
+ }, [x, k]), I = a(() => {
38
+ O(t(A || "/conversations", "new"));
39
+ }, [A, O]), L = a(() => {
40
+ j(null), O(`${A || "/conversations"}${N.search}`);
41
+ }, [
42
+ A,
43
+ N.search,
44
+ O,
45
+ j
46
+ ]);
47
+ return /* @__PURE__ */ h("div", {
48
+ "data-tour": "conversations-inbox",
49
+ className: "flex h-full w-full min-h-0 min-w-0 overflow-hidden bg-bg-canvas",
50
+ children: [/* @__PURE__ */ h("aside", {
51
+ "aria-label": k("conversations.listPaneLabel", "Conversations list"),
52
+ className: r("flex flex-col min-h-0 border-r border-border-default bg-bg-surface", "w-full md:w-[320px] md:flex-shrink-0", P ? "hidden md:flex" : "flex"),
53
+ children: [/* @__PURE__ */ h("div", {
54
+ className: "sticky top-0 z-10 flex flex-col gap-3 border-b border-border-subtle bg-bg-surface px-4 pt-4 pb-3",
55
+ children: [
56
+ /* @__PURE__ */ h("div", {
57
+ className: "flex items-center justify-between gap-2",
58
+ children: [/* @__PURE__ */ m("h1", {
59
+ className: "text-base font-semibold text-text-primary",
60
+ children: k("conversations.title", "Conversations")
61
+ }), /* @__PURE__ */ h(_, {
62
+ "data-tour": "conversation-new-button",
63
+ type: "button",
64
+ size: "sm",
65
+ onClick: I,
66
+ "aria-label": k("conversations.createNewConversation", "Create a new conversation"),
67
+ children: [/* @__PURE__ */ m(c, { size: 14 }), /* @__PURE__ */ m("span", {
68
+ className: "hidden sm:inline",
69
+ children: k("conversations.newConversation", "New")
70
+ })]
71
+ })]
72
+ }),
73
+ /* @__PURE__ */ h("div", {
74
+ className: "relative",
75
+ children: [
76
+ /* @__PURE__ */ m(l, {
77
+ size: 14,
78
+ className: "pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-text-secondary",
79
+ "aria-hidden": "true"
80
+ }),
81
+ /* @__PURE__ */ m("input", {
82
+ "data-tour": "conversation-search",
83
+ type: "search",
84
+ value: S,
85
+ onChange: (e) => C(e.target.value),
86
+ placeholder: k("conversations.search", "Search conversations..."),
87
+ "aria-label": k("conversations.searchAria", "Search conversations"),
88
+ className: r("w-full rounded-md border border-border-default bg-bg-elevated", "py-1.5 pl-8 pr-8 text-sm text-text-primary", "placeholder:text-text-secondary", "focus:border-action-primary-border focus:outline-none focus:ring-2 focus:ring-action-primary-border/40")
89
+ }),
90
+ S.length > 0 && /* @__PURE__ */ m("button", {
91
+ type: "button",
92
+ onClick: () => C(""),
93
+ "aria-label": k("conversations.clearSearch", "Clear conversation search"),
94
+ className: "absolute right-2 top-1/2 -translate-y-1/2 rounded p-0.5 text-text-secondary hover:bg-action-ghost-bg-hover hover:text-text-primary",
95
+ children: /* @__PURE__ */ m(u, { size: 12 })
96
+ })
97
+ ]
98
+ }),
99
+ /* @__PURE__ */ m("div", {
100
+ "data-tour": "conversations-tabs",
101
+ children: /* @__PURE__ */ m(g, {
102
+ items: F,
103
+ activeTabId: v,
104
+ onTabChange: (e) => b(e),
105
+ variant: "underline",
106
+ size: "sm"
107
+ })
108
+ }),
109
+ w && w.length > 0 && /* @__PURE__ */ m("div", {
110
+ "data-tour": "conversations-filter-chips",
111
+ className: "flex flex-wrap gap-1.5",
112
+ children: w.map((e) => /* @__PURE__ */ h("button", {
113
+ type: "button",
114
+ onClick: e.onToggle,
115
+ "aria-pressed": e.active,
116
+ className: r("inline-flex items-center gap-1 rounded-full border px-2.5 py-0.5 text-xs font-medium transition-colors", e.active ? "border-action-primary-border bg-action-primary-bg/10 text-action-primary-bg" : "border-border-default text-text-secondary hover:bg-action-ghost-bg-hover hover:text-text-primary"),
117
+ children: [e.icon, e.label]
118
+ }, e.id))
119
+ })
120
+ ]
121
+ }), /* @__PURE__ */ m("div", {
122
+ "data-tour": "conversations-list-scroll",
123
+ className: "flex-1 min-h-0 overflow-y-auto overflow-x-hidden",
124
+ children: T
125
+ })]
126
+ }), /* @__PURE__ */ h("section", {
127
+ "aria-label": k("conversations.detailPaneLabel", "Conversation detail"),
128
+ className: r("flex flex-col min-h-0 flex-1 min-w-0 bg-bg-surface", P ? "flex" : "hidden md:flex"),
129
+ children: [P && /* @__PURE__ */ m("div", {
130
+ className: "md:hidden border-b border-border-subtle px-3 py-2",
131
+ children: /* @__PURE__ */ h(_, {
132
+ variant: "ghost",
133
+ size: "sm",
134
+ onClick: L,
135
+ "aria-label": k("conversations.backToInbox", "Back to conversations list"),
136
+ children: [/* @__PURE__ */ m(s, { size: 14 }), k("conversations.backToInbox", "Back to inbox")]
137
+ })
138
+ }), /* @__PURE__ */ m("div", {
139
+ className: "flex-1 min-h-0 min-w-0 overflow-y-auto overflow-x-hidden",
140
+ children: E ?? D ?? /* @__PURE__ */ m(y, {
141
+ onNew: I,
142
+ title: k("conversations.detailEmpty.title", "Select a conversation"),
143
+ description: k("conversations.detailEmpty.description", "Pick a conversation from the list — or start a new one."),
144
+ newLabel: k("conversations.detailEmpty.newCta", "Start a new conversation")
145
+ })
146
+ })]
147
+ })]
148
+ });
149
+ }
150
+ function y({ title: e, description: t, newLabel: n, onNew: r }) {
151
+ return /* @__PURE__ */ m("div", {
152
+ className: "flex h-full items-center justify-center p-8",
153
+ children: /* @__PURE__ */ h("div", {
154
+ className: "flex max-w-sm flex-col items-center gap-3 text-center",
155
+ children: [
156
+ /* @__PURE__ */ m("div", {
157
+ className: "flex size-16 items-center justify-center rounded-full bg-bg-sunken",
158
+ children: /* @__PURE__ */ m(c, {
159
+ size: 28,
160
+ className: "text-text-secondary",
161
+ "aria-hidden": "true"
162
+ })
163
+ }),
164
+ /* @__PURE__ */ m("h2", {
165
+ className: "text-lg font-semibold text-text-primary",
166
+ children: e
167
+ }),
168
+ /* @__PURE__ */ m("p", {
169
+ className: "text-sm text-text-secondary",
170
+ children: t
171
+ }),
172
+ /* @__PURE__ */ h(_, {
173
+ type: "button",
174
+ onClick: r,
175
+ className: "mt-2",
176
+ children: [/* @__PURE__ */ m(c, { size: 14 }), n]
177
+ })
178
+ ]
179
+ })
180
+ });
181
+ }
182
+ //#endregion
183
+ export { v as InboxLayout };
184
+
185
+ //# sourceMappingURL=InboxLayout.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"InboxLayout.js","names":[],"sources":["../../../src/conversations/components/InboxLayout.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport { useCallback, useMemo } from \"react\";\nimport { useLocation, useNavigate, useParams } from \"react-router-dom\";\nimport { Tabs, type TabItem } from \"@burdenoff/fe-libs/chrome\";\nimport { Button } from \"@burdenoff/fe-libs/ui\";\nimport { ArrowLeft, MessageSquarePlus, Search, X } from \"lucide-react\";\nimport { cn } from \"../utils/cn\";\nimport { useConversations } from \"../providers/ConversationsProvider\";\nimport { useConversationsStore } from \"../store/conversationsStore\";\nimport { useTr } from \"../utils/i18n\";\nimport { joinPath } from \"../utils/navigation\";\n\n/**\n * Identifier for each inbox tab. Drives the conversation list filter and is\n * mirrored to / read from the `?tab=` URL search param.\n */\nexport type InboxTabId = \"all\" | \"direct\" | \"groups\" | \"channels\" | \"archived\";\n\nexport const INBOX_TAB_IDS: InboxTabId[] = [\n \"all\",\n \"direct\",\n \"groups\",\n \"channels\",\n \"archived\",\n];\n\nexport interface FilterChipDescriptor {\n id: \"unread\" | \"pinned\";\n label: string;\n active: boolean;\n onToggle: () => void;\n icon?: ReactNode;\n}\n\nexport interface InboxLayoutProps {\n /**\n * Currently active inbox tab. Drives which list view is rendered in the\n * left pane.\n */\n activeTab: InboxTabId;\n /** Tab change handler. Used by the host page to sync URL state. */\n onTabChange: (id: InboxTabId) => void;\n /**\n * Whether the Archived tab is exposed. Gated behind the `conversations.archived-view`\n * feature flag — when false the tab is hidden from the bar entirely.\n */\n archivedEnabled?: boolean;\n /** Search input value (controlled). */\n searchQuery: string;\n /** Search input change handler. */\n onSearchChange: (value: string) => void;\n /**\n * Quick toggleable filter chips rendered below the tabs. Pass an empty\n * array (or omit) to hide the chip row entirely — useful on the Archived\n * tab where pin/unread filters don't make sense.\n */\n filterChips?: FilterChipDescriptor[];\n /** Scrollable list content rendered in the left pane below the chrome. */\n listSlot: ReactNode;\n /**\n * Right-pane content. When a conversation is selected the parent should\n * pass the conversation detail subtree here. When nothing is selected the\n * empty-state slot below is used instead.\n */\n detailSlot?: ReactNode;\n /**\n * Empty-state rendered in the right pane when `detailSlot` is null /\n * undefined. Defaults to a generic \"Select a conversation\" prompt.\n */\n emptyDetailSlot?: ReactNode;\n}\n\n/**\n * Two-pane inbox shell for the conversations microfrontend.\n *\n * Left pane: fixed 320px (collapses to full-width below `md` when no\n * conversation is selected, the way Slack / Linear / Discord behave on\n * mobile). Holds:\n * - sticky header (title + New CTA)\n * - search input\n * - tabs (All / Direct / Groups / Channels / Archived)\n * - filter chips (Unread / Pinned)\n * - scrollable conversation list\n *\n * Right pane: detail subtree OR empty state.\n *\n * The component is presentation-only — URL <-> tab sync, query\n * construction, and conversation selection all live in the host page.\n */\nexport function InboxLayout({\n activeTab,\n onTabChange,\n archivedEnabled = true,\n searchQuery,\n onSearchChange,\n filterChips,\n listSlot,\n detailSlot,\n emptyDetailSlot,\n}: InboxLayoutProps) {\n const navigate = useNavigate();\n const tr = useTr();\n const { basePath } = useConversations();\n const { setActiveConversationId } = useConversationsStore();\n const params = useParams<{ conversationId?: string }>();\n const location = useLocation();\n // A conversation is \"open\" whenever the URL targets a detail route\n // (/:conversationId). We can't rely solely on a passed detailSlot —\n // routing decides whether to mount the detail page.\n const conversationOpen =\n Boolean(params.conversationId) && Boolean(detailSlot);\n\n const tabs: TabItem[] = useMemo(() => {\n const items: TabItem[] = [\n { id: \"all\", label: tr(\"conversations.tabs.all\", \"All\") },\n { id: \"direct\", label: tr(\"conversations.tabs.direct\", \"Direct\") },\n { id: \"groups\", label: tr(\"conversations.tabs.groups\", \"Groups\") },\n { id: \"channels\", label: tr(\"conversations.tabs.channels\", \"Channels\") },\n ];\n if (archivedEnabled) {\n items.push({\n id: \"archived\",\n label: tr(\"conversations.tabs.archived\", \"Archived\"),\n });\n }\n return items;\n }, [archivedEnabled, tr]);\n\n const handleNewConversation = useCallback(() => {\n navigate(joinPath(basePath || \"/conversations\", \"new\"));\n }, [basePath, navigate]);\n\n const handleBackToList = useCallback(() => {\n // Clear the open conversation id so the empty state shows and routing\n // returns to the inbox shell. We keep the current tab via location.search.\n setActiveConversationId(null);\n navigate(`${basePath || \"/conversations\"}${location.search}`);\n }, [basePath, location.search, navigate, setActiveConversationId]);\n\n return (\n <div\n data-tour=\"conversations-inbox\"\n className=\"flex h-full w-full min-h-0 min-w-0 overflow-hidden bg-bg-canvas\"\n >\n {/* Left pane: list. On md+ always visible as a fixed sidebar. Below md,\n hidden when a conversation is open (full-screen detail mode). */}\n <aside\n aria-label={tr(\"conversations.listPaneLabel\", \"Conversations list\")}\n className={cn(\n \"flex flex-col min-h-0 border-r border-border-default bg-bg-surface\",\n \"w-full md:w-[320px] md:flex-shrink-0\",\n conversationOpen ? \"hidden md:flex\" : \"flex\",\n )}\n >\n {/* Sticky header: title + new CTA */}\n <div className=\"sticky top-0 z-10 flex flex-col gap-3 border-b border-border-subtle bg-bg-surface px-4 pt-4 pb-3\">\n <div className=\"flex items-center justify-between gap-2\">\n <h1 className=\"text-base font-semibold text-text-primary\">\n {tr(\"conversations.title\", \"Conversations\")}\n </h1>\n <Button\n data-tour=\"conversation-new-button\"\n type=\"button\"\n size=\"sm\"\n onClick={handleNewConversation}\n aria-label={tr(\n \"conversations.createNewConversation\",\n \"Create a new conversation\",\n )}\n >\n <MessageSquarePlus size={14} />\n <span className=\"hidden sm:inline\">\n {tr(\"conversations.newConversation\", \"New\")}\n </span>\n </Button>\n </div>\n\n {/* Search */}\n <div className=\"relative\">\n <Search\n size={14}\n className=\"pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-text-secondary\"\n aria-hidden=\"true\"\n />\n <input\n data-tour=\"conversation-search\"\n type=\"search\"\n value={searchQuery}\n onChange={(event) => onSearchChange(event.target.value)}\n placeholder={tr(\n \"conversations.search\",\n \"Search conversations...\",\n )}\n aria-label={tr(\n \"conversations.searchAria\",\n \"Search conversations\",\n )}\n className={cn(\n \"w-full rounded-md border border-border-default bg-bg-elevated\",\n \"py-1.5 pl-8 pr-8 text-sm text-text-primary\",\n \"placeholder:text-text-secondary\",\n \"focus:border-action-primary-border focus:outline-none focus:ring-2 focus:ring-action-primary-border/40\",\n )}\n />\n {searchQuery.length > 0 && (\n <button\n type=\"button\"\n onClick={() => onSearchChange(\"\")}\n aria-label={tr(\n \"conversations.clearSearch\",\n \"Clear conversation search\",\n )}\n className=\"absolute right-2 top-1/2 -translate-y-1/2 rounded p-0.5 text-text-secondary hover:bg-action-ghost-bg-hover hover:text-text-primary\"\n >\n <X size={12} />\n </button>\n )}\n </div>\n\n {/* Tabs */}\n <div data-tour=\"conversations-tabs\">\n <Tabs\n items={tabs}\n activeTabId={activeTab}\n onTabChange={(id: string) => onTabChange(id as InboxTabId)}\n variant=\"underline\"\n size=\"sm\"\n />\n </div>\n\n {/* Filter chips */}\n {filterChips && filterChips.length > 0 && (\n <div\n data-tour=\"conversations-filter-chips\"\n className=\"flex flex-wrap gap-1.5\"\n >\n {filterChips.map((chip) => (\n <button\n key={chip.id}\n type=\"button\"\n onClick={chip.onToggle}\n aria-pressed={chip.active}\n className={cn(\n \"inline-flex items-center gap-1 rounded-full border px-2.5 py-0.5 text-xs font-medium transition-colors\",\n chip.active\n ? \"border-action-primary-border bg-action-primary-bg/10 text-action-primary-bg\"\n : \"border-border-default text-text-secondary hover:bg-action-ghost-bg-hover hover:text-text-primary\",\n )}\n >\n {chip.icon}\n {chip.label}\n </button>\n ))}\n </div>\n )}\n </div>\n\n {/* Scrollable list */}\n <div\n data-tour=\"conversations-list-scroll\"\n className=\"flex-1 min-h-0 overflow-y-auto overflow-x-hidden\"\n >\n {listSlot}\n </div>\n </aside>\n\n {/* Right pane: detail OR empty state */}\n <section\n aria-label={tr(\"conversations.detailPaneLabel\", \"Conversation detail\")}\n className={cn(\n \"flex flex-col min-h-0 flex-1 min-w-0 bg-bg-surface\",\n conversationOpen ? \"flex\" : \"hidden md:flex\",\n )}\n >\n {conversationOpen && (\n // Mobile-only back button. On md+ the list and detail render\n // side-by-side so the back arrow would be useless.\n <div className=\"md:hidden border-b border-border-subtle px-3 py-2\">\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={handleBackToList}\n aria-label={tr(\n \"conversations.backToInbox\",\n \"Back to conversations list\",\n )}\n >\n <ArrowLeft size={14} />\n {tr(\"conversations.backToInbox\", \"Back to inbox\")}\n </Button>\n </div>\n )}\n <div className=\"flex-1 min-h-0 min-w-0 overflow-y-auto overflow-x-hidden\">\n {detailSlot ?? emptyDetailSlot ?? (\n <InboxEmptyDetail\n onNew={handleNewConversation}\n title={tr(\n \"conversations.detailEmpty.title\",\n \"Select a conversation\",\n )}\n description={tr(\n \"conversations.detailEmpty.description\",\n \"Pick a conversation from the list — or start a new one.\",\n )}\n newLabel={tr(\n \"conversations.detailEmpty.newCta\",\n \"Start a new conversation\",\n )}\n />\n )}\n </div>\n </section>\n </div>\n );\n}\n\ninterface InboxEmptyDetailProps {\n title: string;\n description: string;\n newLabel: string;\n onNew: () => void;\n}\n\n/**\n * Default empty-state rendered in the detail pane when no conversation is\n * currently selected. Lifted out so the host page (or tests) can override\n * the copy via `emptyDetailSlot`.\n */\nfunction InboxEmptyDetail({\n title,\n description,\n newLabel,\n onNew,\n}: InboxEmptyDetailProps) {\n return (\n <div className=\"flex h-full items-center justify-center p-8\">\n <div className=\"flex max-w-sm flex-col items-center gap-3 text-center\">\n <div className=\"flex size-16 items-center justify-center rounded-full bg-bg-sunken\">\n <MessageSquarePlus\n size={28}\n className=\"text-text-secondary\"\n aria-hidden=\"true\"\n />\n </div>\n <h2 className=\"text-lg font-semibold text-text-primary\">{title}</h2>\n <p className=\"text-sm text-text-secondary\">{description}</p>\n <Button type=\"button\" onClick={onNew} className=\"mt-2\">\n <MessageSquarePlus size={14} />\n {newLabel}\n </Button>\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"LazyPageBoundary.js","names":[],"sources":["../../../src/conversations/components/LazyPageBoundary.tsx"],"sourcesContent":["import {\n Component,\n Suspense,\n type ErrorInfo,\n type ReactNode,\n} from \"react\";\nimport { Loader2, AlertTriangle, RefreshCw } from \"lucide-react\";\nimport { useTr } from \"../utils/i18n\";\nimport { isChunkLoadError } from \"./lazyRetry\";\n\n/**\n * Fallback spinner rendered while a lazy-loaded page chunk is resolving.\n */\nfunction PageLoader() {\n return (\n <div className=\"flex items-center justify-center min-h-[400px]\">\n <Loader2 className=\"size-8 animate-spin text-primary\" />\n </div>\n );\n}\n\nfunction LazyErrorUI({\n errorMessage,\n onRetry,\n}: {\n errorMessage: string | null;\n onRetry: () => void;\n}) {\n const tr = useTr();\n\n return (\n <div className=\"flex flex-col items-center justify-center min-h-[400px] gap-4 text-center p-6\">\n <div className=\"p-3 rounded-full bg-destructive/10\">\n <AlertTriangle className=\"size-8 text-destructive\" />\n </div>\n <div className=\"space-y-2\">\n <h3 className=\"text-lg font-semibold text-foreground\">\n {tr(\"conversations.failedToLoadPage\", \"Failed to load page\")}\n </h3>\n <p className=\"text-sm text-muted-foreground max-w-md\">\n {tr(\n \"conversations.pageChunkLoadFailed\",\n \"The page chunk could not be loaded. This may be due to a network issue.\",\n )}\n </p>\n </div>\n <button\n type=\"button\"\n onClick={onRetry}\n className=\"inline-flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-md bg-primary text-primary-foreground hover:bg-primary/90 transition-colors\"\n >\n <RefreshCw className=\"size-4\" />\n {tr(\"conversations.tryAgain\", \"Try Again\")}\n </button>\n {errorMessage && (\n <details className=\"mt-2 max-w-md w-full text-left\">\n <summary className=\"cursor-pointer text-xs text-muted-foreground hover:text-foreground select-none\">\n {tr(\"conversations.errorDetails\", \"Error details\")}\n </summary>\n <pre className=\"mt-2 p-2 rounded bg-muted text-[11px] text-muted-foreground whitespace-pre-wrap break-all\">\n {errorMessage}\n </pre>\n </details>\n )}\n </div>\n );\n}\n\ntype LazyErrorBoundaryProps = {\n source?: string;\n children: (retryKey: number) => ReactNode;\n};\n\ntype LazyErrorBoundaryState = {\n hasError: boolean;\n error: Error | null;\n retryKey: number;\n};\n\n/**\n * Session-scoped flag used to prevent a reload loop when a chunk-load\n * failure is persistent (broken deploy, CDN outage, missing hash file).\n * First chunk-load retry triggers a page reload and sets the flag; if\n * the page mounts again with the same error, `handleRetry` sees the\n * flag and skips the second reload so the user stays on the error UI\n * and can take a different action instead of looping indefinitely.\n */\nconst RELOAD_FLAG_PREFIX = \"conversations-lazyreload-\";\n\nfunction reloadFlagKey(source?: string): string {\n return `${RELOAD_FLAG_PREFIX}${source ?? \"default\"}`;\n}\n\nfunction readSessionFlag(key: string): boolean {\n try {\n if (typeof sessionStorage === \"undefined\") return false;\n return sessionStorage.getItem(key) !== null;\n } catch {\n return false;\n }\n}\n\nfunction setSessionFlag(key: string): void {\n try {\n if (typeof sessionStorage === \"undefined\") return;\n sessionStorage.setItem(key, \"1\");\n } catch {\n // sessionStorage access can throw in some privacy / sandboxed modes\n }\n}\n\nfunction clearSessionFlag(key: string): void {\n try {\n if (typeof sessionStorage === \"undefined\") return;\n sessionStorage.removeItem(key);\n } catch {\n // ignore\n }\n}\n\n/**\n * Error boundary for lazy-loaded page chunks.\n *\n * On retry it branches by error type:\n * - **Chunk-load failures** (`ChunkLoadError`, \"Loading chunk X failed\",\n * \"Failed to fetch dynamically imported module\", etc.) — the rejected\n * promise is cached on the `LazyExoticComponent` itself; the only\n * reliable recovery is a full page reload, which re-creates all lazy\n * refs from scratch.\n * - **Other render errors** — clear the error state and bump an internal\n * `retryKey`; the enclosing `LazyPageBoundary` keys its `<Suspense>`\n * on that counter so the subtree unmounts and remounts. This resets\n * component state and helps recover from transient render issues.\n *\n * Accepts an optional `source` label used in the console tag so user-\n * route and admin-route chunks can be distinguished in telemetry.\n */\nclass LazyErrorBoundary extends Component<\n LazyErrorBoundaryProps,\n LazyErrorBoundaryState\n> {\n state: LazyErrorBoundaryState = {\n hasError: false,\n error: null,\n retryKey: 0,\n };\n\n static getDerivedStateFromError(error: Error) {\n return { hasError: true, error };\n }\n\n componentDidMount() {\n // Subtree mounted without catching an error — the previous session\n // state (if any) is stale. Clear the reload-guard flag so a future\n // chunk failure can still trigger one reload before giving up.\n if (!this.state.hasError) {\n clearSessionFlag(reloadFlagKey(this.props.source));\n }\n }\n\n componentDidUpdate(\n _prevProps: LazyErrorBoundaryProps,\n prevState: LazyErrorBoundaryState,\n ) {\n // Error → recovered (state reset / retryKey bump → clean render)\n // also invalidates the reload-guard flag.\n if (prevState.hasError && !this.state.hasError) {\n clearSessionFlag(reloadFlagKey(this.props.source));\n }\n }\n\n componentDidCatch(error: Error, errorInfo: ErrorInfo) {\n const tag = this.props.source\n ? `[Conversations:${this.props.source}]`\n : \"[Conversations]\";\n // Log the raw error details (including the message) to the console\n // only — user-facing copy is always a localised fallback so that\n // bundler-internal strings like \"ChunkLoadError: Loading chunk 42 failed\"\n // never surface to end users.\n console.error(`${tag} Lazy page load failed:`, error, errorInfo);\n }\n\n handleRetry = () => {\n if (isChunkLoadError(this.state.error)) {\n // React.lazy caches rejected promises on the LazyExoticComponent\n // itself — keying Suspense or resetting boundary state does NOT\n // clear it. A full reload is the only reliable recovery.\n //\n // Guard against a reload loop: if the flag is already set we\n // reloaded on a prior iteration and the chunk still failed.\n // Leave the flag set and do nothing — the error UI is already\n // visible and `componentDidMount` / `componentDidUpdate` are\n // the only places that should clear the flag (they fire on a\n // successful render, signalling that the chunk is finally\n // healthy). Clearing here would let a subsequent click trigger\n // another reload and loop indefinitely.\n const flagKey = reloadFlagKey(this.props.source);\n if (readSessionFlag(flagKey)) {\n return;\n }\n setSessionFlag(flagKey);\n if (typeof window !== \"undefined\") {\n window.location.reload();\n }\n return;\n }\n this.setState((s) => ({\n hasError: false,\n error: null,\n retryKey: s.retryKey + 1,\n }));\n };\n\n render() {\n if (this.state.hasError) {\n // The primary copy is always the localised fallback so bundler-\n // internal strings (\"ChunkLoadError: Loading chunk 42 failed\",\n // \"Failed to fetch dynamically imported module\", etc.) never lead\n // the UI. The raw message is surfaced behind a collapsible\n // \"Error details\" affordance so support / dev users can still\n // diagnose persistent failures without reloading the tab.\n return (\n <LazyErrorUI\n errorMessage={this.state.error?.message ?? null}\n onRetry={this.handleRetry}\n />\n );\n }\n return this.props.children(this.state.retryKey);\n }\n}\n\n/**\n * Wraps a subtree with `<LazyErrorBoundary>` + `<Suspense>` so that any\n * `React.lazy()` / `lazyRetry()` page chunk below it gets a consistent\n * loading spinner and retry UI.\n *\n * The Suspense is keyed by the boundary's internal `retryKey` counter\n * so non-chunk render errors can be recovered by unmounting + remounting\n * the subtree. Chunk-load errors bypass this path and trigger a full\n * page reload (see `LazyErrorBoundary.handleRetry` — React.lazy's\n * promise cache cannot be cleared without creating a fresh lazy ref).\n */\nexport function LazyPageBoundary({\n children,\n source,\n}: {\n children: ReactNode;\n source?: string;\n}) {\n return (\n <LazyErrorBoundary source={source}>\n {(retryKey) => (\n <Suspense key={retryKey} fallback={<PageLoader />}>\n {children}\n </Suspense>\n )}\n </LazyErrorBoundary>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"LazyPageBoundary.js","names":[],"sources":["../../../src/conversations/components/LazyPageBoundary.tsx"],"sourcesContent":["import { Component, Suspense, type ErrorInfo, type ReactNode } from \"react\";\nimport { Loader2, AlertTriangle, RefreshCw } from \"lucide-react\";\nimport { useTr } from \"../utils/i18n\";\nimport { isChunkLoadError } from \"./lazyRetry\";\n\n/**\n * Fallback spinner rendered while a lazy-loaded page chunk is resolving.\n */\nfunction PageLoader() {\n return (\n <div className=\"flex items-center justify-center min-h-[400px]\">\n <Loader2 className=\"size-8 animate-spin text-primary\" />\n </div>\n );\n}\n\nfunction LazyErrorUI({\n errorMessage,\n onRetry,\n}: {\n errorMessage: string | null;\n onRetry: () => void;\n}) {\n const tr = useTr();\n\n return (\n <div className=\"flex flex-col items-center justify-center min-h-[400px] gap-4 text-center p-6\">\n <div className=\"p-3 rounded-full bg-destructive/10\">\n <AlertTriangle className=\"size-8 text-destructive\" />\n </div>\n <div className=\"space-y-2\">\n <h3 className=\"text-lg font-semibold text-foreground\">\n {tr(\"conversations.failedToLoadPage\", \"Failed to load page\")}\n </h3>\n <p className=\"text-sm text-muted-foreground max-w-md\">\n {tr(\n \"conversations.pageChunkLoadFailed\",\n \"The page chunk could not be loaded. This may be due to a network issue.\",\n )}\n </p>\n </div>\n <button\n type=\"button\"\n onClick={onRetry}\n className=\"inline-flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-md bg-primary text-primary-foreground hover:bg-primary/90 transition-colors\"\n >\n <RefreshCw className=\"size-4\" />\n {tr(\"conversations.tryAgain\", \"Try Again\")}\n </button>\n {errorMessage && (\n <details className=\"mt-2 max-w-md w-full text-left\">\n <summary className=\"cursor-pointer text-xs text-muted-foreground hover:text-foreground select-none\">\n {tr(\"conversations.errorDetails\", \"Error details\")}\n </summary>\n <pre className=\"mt-2 p-2 rounded bg-muted text-[11px] text-muted-foreground whitespace-pre-wrap break-all\">\n {errorMessage}\n </pre>\n </details>\n )}\n </div>\n );\n}\n\ntype LazyErrorBoundaryProps = {\n source?: string;\n children: (retryKey: number) => ReactNode;\n};\n\ntype LazyErrorBoundaryState = {\n hasError: boolean;\n error: Error | null;\n retryKey: number;\n};\n\n/**\n * Session-scoped flag used to prevent a reload loop when a chunk-load\n * failure is persistent (broken deploy, CDN outage, missing hash file).\n * First chunk-load retry triggers a page reload and sets the flag; if\n * the page mounts again with the same error, `handleRetry` sees the\n * flag and skips the second reload so the user stays on the error UI\n * and can take a different action instead of looping indefinitely.\n */\nconst RELOAD_FLAG_PREFIX = \"conversations-lazyreload-\";\n\nfunction reloadFlagKey(source?: string): string {\n return `${RELOAD_FLAG_PREFIX}${source ?? \"default\"}`;\n}\n\nfunction readSessionFlag(key: string): boolean {\n try {\n if (typeof sessionStorage === \"undefined\") return false;\n return sessionStorage.getItem(key) !== null;\n } catch {\n return false;\n }\n}\n\nfunction setSessionFlag(key: string): void {\n try {\n if (typeof sessionStorage === \"undefined\") return;\n sessionStorage.setItem(key, \"1\");\n } catch {\n // sessionStorage access can throw in some privacy / sandboxed modes\n }\n}\n\nfunction clearSessionFlag(key: string): void {\n try {\n if (typeof sessionStorage === \"undefined\") return;\n sessionStorage.removeItem(key);\n } catch {\n // ignore\n }\n}\n\n/**\n * Error boundary for lazy-loaded page chunks.\n *\n * On retry it branches by error type:\n * - **Chunk-load failures** (`ChunkLoadError`, \"Loading chunk X failed\",\n * \"Failed to fetch dynamically imported module\", etc.) — the rejected\n * promise is cached on the `LazyExoticComponent` itself; the only\n * reliable recovery is a full page reload, which re-creates all lazy\n * refs from scratch.\n * - **Other render errors** — clear the error state and bump an internal\n * `retryKey`; the enclosing `LazyPageBoundary` keys its `<Suspense>`\n * on that counter so the subtree unmounts and remounts. This resets\n * component state and helps recover from transient render issues.\n *\n * Accepts an optional `source` label used in the console tag so user-\n * route and admin-route chunks can be distinguished in telemetry.\n */\nclass LazyErrorBoundary extends Component<\n LazyErrorBoundaryProps,\n LazyErrorBoundaryState\n> {\n state: LazyErrorBoundaryState = {\n hasError: false,\n error: null,\n retryKey: 0,\n };\n\n static getDerivedStateFromError(error: Error) {\n return { hasError: true, error };\n }\n\n componentDidMount() {\n // Subtree mounted without catching an error — the previous session\n // state (if any) is stale. Clear the reload-guard flag so a future\n // chunk failure can still trigger one reload before giving up.\n if (!this.state.hasError) {\n clearSessionFlag(reloadFlagKey(this.props.source));\n }\n }\n\n componentDidUpdate(\n _prevProps: LazyErrorBoundaryProps,\n prevState: LazyErrorBoundaryState,\n ) {\n // Error → recovered (state reset / retryKey bump → clean render)\n // also invalidates the reload-guard flag.\n if (prevState.hasError && !this.state.hasError) {\n clearSessionFlag(reloadFlagKey(this.props.source));\n }\n }\n\n componentDidCatch(error: Error, errorInfo: ErrorInfo) {\n const tag = this.props.source\n ? `[Conversations:${this.props.source}]`\n : \"[Conversations]\";\n // Log the raw error details (including the message) to the console\n // only — user-facing copy is always a localised fallback so that\n // bundler-internal strings like \"ChunkLoadError: Loading chunk 42 failed\"\n // never surface to end users.\n console.error(`${tag} Lazy page load failed:`, error, errorInfo);\n }\n\n handleRetry = () => {\n if (isChunkLoadError(this.state.error)) {\n // React.lazy caches rejected promises on the LazyExoticComponent\n // itself — keying Suspense or resetting boundary state does NOT\n // clear it. A full reload is the only reliable recovery.\n //\n // Guard against a reload loop: if the flag is already set we\n // reloaded on a prior iteration and the chunk still failed.\n // Leave the flag set and do nothing — the error UI is already\n // visible and `componentDidMount` / `componentDidUpdate` are\n // the only places that should clear the flag (they fire on a\n // successful render, signalling that the chunk is finally\n // healthy). Clearing here would let a subsequent click trigger\n // another reload and loop indefinitely.\n const flagKey = reloadFlagKey(this.props.source);\n if (readSessionFlag(flagKey)) {\n return;\n }\n setSessionFlag(flagKey);\n if (typeof window !== \"undefined\") {\n window.location.reload();\n }\n return;\n }\n this.setState((s) => ({\n hasError: false,\n error: null,\n retryKey: s.retryKey + 1,\n }));\n };\n\n render() {\n if (this.state.hasError) {\n // The primary copy is always the localised fallback so bundler-\n // internal strings (\"ChunkLoadError: Loading chunk 42 failed\",\n // \"Failed to fetch dynamically imported module\", etc.) never lead\n // the UI. The raw message is surfaced behind a collapsible\n // \"Error details\" affordance so support / dev users can still\n // diagnose persistent failures without reloading the tab.\n return (\n <LazyErrorUI\n errorMessage={this.state.error?.message ?? null}\n onRetry={this.handleRetry}\n />\n );\n }\n return this.props.children(this.state.retryKey);\n }\n}\n\n/**\n * Wraps a subtree with `<LazyErrorBoundary>` + `<Suspense>` so that any\n * `React.lazy()` / `lazyRetry()` page chunk below it gets a consistent\n * loading spinner and retry UI.\n *\n * The Suspense is keyed by the boundary's internal `retryKey` counter\n * so non-chunk render errors can be recovered by unmounting + remounting\n * the subtree. Chunk-load errors bypass this path and trigger a full\n * page reload (see `LazyErrorBoundary.handleRetry` — React.lazy's\n * promise cache cannot be cleared without creating a fresh lazy ref).\n */\nexport function LazyPageBoundary({\n children,\n source,\n}: {\n children: ReactNode;\n source?: string;\n}) {\n return (\n <LazyErrorBoundary source={source}>\n {(retryKey) => (\n <Suspense key={retryKey} fallback={<PageLoader />}>\n {children}\n </Suspense>\n )}\n </LazyErrorBoundary>\n );\n}\n"],"mappings":"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"}