@burdenoff/microfe-conversations 2026.522.1 → 2026.523.2

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 (47) hide show
  1. package/dist/conversations/ConversationsUserRoutes.d.ts.map +1 -1
  2. package/dist/conversations/ConversationsUserRoutes.js +17 -19
  3. package/dist/conversations/ConversationsUserRoutes.js.map +1 -1
  4. package/dist/conversations/components/ConversationCard.d.ts.map +1 -1
  5. package/dist/conversations/components/ConversationCard.js +2 -1
  6. package/dist/conversations/components/ConversationCard.js.map +1 -1
  7. package/dist/conversations/components/LazyPageBoundary.d.ts +3 -3
  8. package/dist/conversations/components/LazyPageBoundary.d.ts.map +1 -1
  9. package/dist/conversations/components/LazyPageBoundary.js.map +1 -1
  10. package/dist/conversations/components/MessageBubble.d.ts.map +1 -1
  11. package/dist/conversations/components/MessageBubble.js +22 -16
  12. package/dist/conversations/components/MessageBubble.js.map +1 -1
  13. package/dist/conversations/components/PageLayout.d.ts +3 -1
  14. package/dist/conversations/components/PageLayout.d.ts.map +1 -1
  15. package/dist/conversations/components/PageLayout.js +2 -1
  16. package/dist/conversations/components/PageLayout.js.map +1 -1
  17. package/dist/conversations/hooks/useConversationQueries.js +1 -1
  18. package/dist/conversations/hooks/useConversationQueries.js.map +1 -1
  19. package/dist/conversations/hooks/useMessageQueries.d.ts +1 -0
  20. package/dist/conversations/hooks/useMessageQueries.d.ts.map +1 -1
  21. package/dist/conversations/hooks/useMessageQueries.js +48 -42
  22. package/dist/conversations/hooks/useMessageQueries.js.map +1 -1
  23. package/dist/conversations/index.js +1 -1
  24. package/dist/conversations/pages/ConversationAdminPages.d.ts.map +1 -1
  25. package/dist/conversations/pages/ConversationAdminPages.js +139 -138
  26. package/dist/conversations/pages/ConversationAdminPages.js.map +1 -1
  27. package/dist/conversations/pages/ConversationDetailPage.d.ts.map +1 -1
  28. package/dist/conversations/pages/ConversationDetailPage.js +256 -230
  29. package/dist/conversations/pages/ConversationDetailPage.js.map +1 -1
  30. package/dist/conversations/pages/ConversationListPage.d.ts +1 -2
  31. package/dist/conversations/pages/ConversationListPage.d.ts.map +1 -1
  32. package/dist/conversations/pages/ConversationListPage.js +138 -125
  33. package/dist/conversations/pages/ConversationListPage.js.map +1 -1
  34. package/dist/conversations/pages/ConversationsLandingPage.d.ts.map +1 -1
  35. package/dist/conversations/pages/ConversationsLandingPage.js +3 -1
  36. package/dist/conversations/pages/ConversationsLandingPage.js.map +1 -1
  37. package/dist/conversations/pages/ConversationsSettingsPage.d.ts.map +1 -1
  38. package/dist/conversations/pages/ConversationsSettingsPage.js +5 -4
  39. package/dist/conversations/pages/ConversationsSettingsPage.js.map +1 -1
  40. package/dist/conversations/pages/NewConversationPage.d.ts.map +1 -1
  41. package/dist/conversations/pages/NewConversationPage.js +4 -0
  42. package/dist/conversations/pages/NewConversationPage.js.map +1 -1
  43. package/dist/conversations/providers/ConversationsProvider.d.ts.map +1 -1
  44. package/dist/conversations/providers/ConversationsProvider.js +10 -8
  45. package/dist/conversations/providers/ConversationsProvider.js.map +1 -1
  46. package/dist/index.js +5 -5
  47. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"ConversationsUserRoutes.d.ts","sourceRoot":"","sources":["../../src/conversations/ConversationsUserRoutes.tsx"],"names":[],"mappings":"AAsBA;;;;;;;;;GASG;AACH,wBAAgB,uBAAuB,4CAatC"}
1
+ {"version":3,"file":"ConversationsUserRoutes.d.ts","sourceRoot":"","sources":["../../src/conversations/ConversationsUserRoutes.tsx"],"names":[],"mappings":"AAgCA;;;;;;;;;GASG;AACH,wBAAgB,uBAAuB,4CAatC"}
@@ -1,37 +1,35 @@
1
1
  import { LazyPageBoundary as e, lazyRetry as t } from "./components/LazyPageBoundary.js";
2
- import { ConversationListPage as n } from "./pages/ConversationListPage.js";
3
- import { ConversationsLandingPage as r } from "./pages/ConversationsLandingPage.js";
4
- import { Navigate as i, Route as a, Routes as o } from "react-router-dom";
5
- import { jsx as s, jsxs as c } from "react/jsx-runtime";
2
+ import { Navigate as n, Route as r, Routes as i } from "react-router-dom";
3
+ import { jsx as a, jsxs as o } from "react/jsx-runtime";
6
4
  //#region src/conversations/ConversationsUserRoutes.tsx
7
- var l = t(() => import("./pages/ConversationDetailPage.js").then((e) => ({ default: e.ConversationDetailPage }))), u = t(() => import("./pages/NewConversationPage.js").then((e) => ({ default: e.NewConversationPage }))), d = t(() => import("./pages/ConversationsSettingsPage.js").then((e) => ({ default: e.ConversationsSettingsPage })));
5
+ var s = t(() => import("./pages/ConversationListPage.js").then((e) => ({ default: e.ConversationListPage }))), c = t(() => import("./pages/ConversationsLandingPage.js").then((e) => ({ default: e.ConversationsLandingPage }))), l = t(() => import("./pages/ConversationDetailPage.js").then((e) => ({ default: e.ConversationDetailPage }))), u = t(() => import("./pages/NewConversationPage.js").then((e) => ({ default: e.NewConversationPage }))), d = t(() => import("./pages/ConversationsSettingsPage.js").then((e) => ({ default: e.ConversationsSettingsPage })));
8
6
  function f() {
9
- return /* @__PURE__ */ s(e, {
7
+ return /* @__PURE__ */ a(e, {
10
8
  source: "user",
11
- children: /* @__PURE__ */ c(o, { children: [
12
- /* @__PURE__ */ s(a, {
9
+ children: /* @__PURE__ */ o(i, { children: [
10
+ /* @__PURE__ */ a(r, {
13
11
  index: !0,
14
- element: /* @__PURE__ */ s(r, {})
12
+ element: /* @__PURE__ */ a(c, {})
15
13
  }),
16
- /* @__PURE__ */ s(a, {
14
+ /* @__PURE__ */ a(r, {
17
15
  path: "list",
18
- element: /* @__PURE__ */ s(n, {})
16
+ element: /* @__PURE__ */ a(s, {})
19
17
  }),
20
- /* @__PURE__ */ s(a, {
18
+ /* @__PURE__ */ a(r, {
21
19
  path: "new",
22
- element: /* @__PURE__ */ s(u, {})
20
+ element: /* @__PURE__ */ a(u, {})
23
21
  }),
24
- /* @__PURE__ */ s(a, {
22
+ /* @__PURE__ */ a(r, {
25
23
  path: "settings",
26
- element: /* @__PURE__ */ s(d, {})
24
+ element: /* @__PURE__ */ a(d, {})
27
25
  }),
28
- /* @__PURE__ */ s(a, {
26
+ /* @__PURE__ */ a(r, {
29
27
  path: ":conversationId",
30
- element: /* @__PURE__ */ s(l, {})
28
+ element: /* @__PURE__ */ a(l, {})
31
29
  }),
32
- /* @__PURE__ */ s(a, {
30
+ /* @__PURE__ */ a(r, {
33
31
  path: "*",
34
- element: /* @__PURE__ */ s(i, {
32
+ element: /* @__PURE__ */ a(n, {
35
33
  to: ".",
36
34
  replace: !0
37
35
  })
@@ -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, lazyRetry } from \"./components/LazyPageBoundary\";\nimport { ConversationListPage } from \"./pages/ConversationListPage\";\nimport { ConversationsLandingPage } from \"./pages/ConversationsLandingPage\";\n\nconst ConversationDetailPage = lazyRetry(() =>\n import(\"./pages/ConversationDetailPage\").then((m) => ({\n default: m.ConversationDetailPage,\n })),\n);\nconst NewConversationPage = lazyRetry(() =>\n import(\"./pages/NewConversationPage\").then((m) => ({\n default: m.NewConversationPage,\n })),\n);\nconst ConversationsSettingsPage = lazyRetry(() =>\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,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, lazyRetry } from \"./components/LazyPageBoundary\";\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":";;;;AAKA,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 +1 @@
1
- {"version":3,"file":"ConversationCard.d.ts","sourceRoot":"","sources":["../../../src/conversations/components/ConversationCard.tsx"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAE7C,UAAU,qBAAqB;IAC7B,YAAY,EAAE,YAAY,CAAC;IAC3B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,CAAC,YAAY,EAAE,YAAY,KAAK,IAAI,CAAC;CAChD;AAkED;;;GAGG;AACH,eAAO,MAAM,gBAAgB,6DA8J3B,CAAC"}
1
+ {"version":3,"file":"ConversationCard.d.ts","sourceRoot":"","sources":["../../../src/conversations/components/ConversationCard.tsx"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAE7C,UAAU,qBAAqB;IAC7B,YAAY,EAAE,YAAY,CAAC;IAC3B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,CAAC,YAAY,EAAE,YAAY,KAAK,IAAI,CAAC;CAChD;AAkED;;;GAGG;AACH,eAAO,MAAM,gBAAgB,6DA+J3B,CAAC"}
@@ -35,11 +35,12 @@ var _ = t(function({ conversation: t, isActive: n = !1, onClick: r }) {
35
35
  yesterday: T("conversations.yesterday", "Yesterday")
36
36
  };
37
37
  return /* @__PURE__ */ f("button", {
38
+ "data-tour": "conversation-card",
38
39
  type: "button",
39
40
  onClick: () => r?.(t),
40
41
  className: e("w-full flex items-start gap-3 px-4 py-3 text-left rounded-lg", "transition-all duration-150 ease-in-out", "hover:bg-muted/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", n && "bg-primary/5 border-l-2 border-l-primary", !n && "border-l-2 border-l-transparent", C && !n && "bg-muted/20"),
41
42
  children: [/* @__PURE__ */ d("div", {
42
- className: e("flex-shrink-0 w-10 h-10 rounded-full flex items-center justify-center text-sm font-semibold", a === "DIRECT" ? "bg-status-info-bg-subtle text-status-info-text" : a === "CHANNEL" ? "bg-status-success-bg-subtle text-status-success-text" : a === "GROUP" ? "bg-accent-purple-subtle text-accent-purple" : "bg-muted text-muted-foreground"),
43
+ className: e("flex-shrink-0 w-10 h-10 rounded-full flex items-center justify-center text-sm font-semibold", a === "DIRECT" ? "bg-status-info-bg-subtle text-status-info-text" : a === "CHANNEL" ? "bg-status-success-bg-subtle text-status-success-text" : a === "GROUP" ? "bg-accent text-accent-foreground" : "bg-muted text-muted-foreground"),
43
44
  children: t.avatar ? /* @__PURE__ */ d("img", {
44
45
  src: t.avatar,
45
46
  alt: i,
@@ -1 +1 @@
1
- {"version":3,"file":"ConversationCard.js","names":[],"sources":["../../../src/conversations/components/ConversationCard.tsx"],"sourcesContent":["import { memo } from \"react\";\nimport { useI18n } from \"@burdenoff/fe-libs/shared/providers/shell/I18nProvider\";\nimport {\n MessageCircle,\n Hash,\n Users,\n Megaphone,\n Bot,\n HeadphonesIcon,\n Pin,\n VolumeX,\n} from \"lucide-react\";\nimport { cn } from \"../utils/cn\";\nimport type { Conversation } from \"../types\";\n\ninterface ConversationCardProps {\n conversation: Conversation;\n isActive?: boolean;\n onClick?: (conversation: Conversation) => void;\n}\n\n/**\n * Conversation type icon mapping\n */\nfunction ConversationTypeIcon({\n type,\n size = 16,\n}: {\n type: Conversation[\"type\"];\n size?: number;\n}) {\n switch (type) {\n case \"DIRECT\":\n return <MessageCircle size={size} />;\n case \"GROUP\":\n return <Users size={size} />;\n case \"CHANNEL\":\n return <Hash size={size} />;\n case \"BROADCAST\":\n case \"ANNOUNCEMENT\":\n return <Megaphone size={size} />;\n case \"BOT\":\n return <Bot size={size} />;\n case \"SUPPORT\":\n return <HeadphonesIcon size={size} />;\n default:\n return <MessageCircle size={size} />;\n }\n}\n\n/**\n * Format a timestamp relative to now (e.g., \"2m ago\", \"3h\", \"Yesterday\")\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 diffSec = Math.floor(diffMs / 1000);\n const diffMin = Math.floor(diffSec / 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\n/**\n * Generate initials from a conversation title\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 * ConversationCard - Displays a conversation in the list view.\n * Shows avatar, title, last message preview, unread count, and metadata.\n */\nexport const ConversationCard = memo(function ConversationCard({\n conversation,\n isActive = false,\n onClick,\n}: ConversationCardProps) {\n const {\n title,\n type,\n lastMessage,\n unreadCount,\n isPinned,\n isMuted,\n memberCount,\n updatedAt,\n sourceContext,\n } = conversation;\n\n const hasUnread = unreadCount > 0;\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 return (\n <button\n type=\"button\"\n onClick={() => onClick?.(conversation)}\n className={cn(\n \"w-full flex items-start gap-3 px-4 py-3 text-left rounded-lg\",\n \"transition-all duration-150 ease-in-out\",\n \"hover:bg-muted/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\",\n isActive && \"bg-primary/5 border-l-2 border-l-primary\",\n !isActive && \"border-l-2 border-l-transparent\",\n hasUnread && !isActive && \"bg-muted/20\",\n )}\n >\n {/* Avatar / Type Icon */}\n <div\n className={cn(\n \"flex-shrink-0 w-10 h-10 rounded-full flex items-center justify-center text-sm font-semibold\",\n type === \"DIRECT\"\n ? \"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 ? \"bg-accent-purple-subtle text-accent-purple\"\n : \"bg-muted text-muted-foreground\",\n )}\n >\n {conversation.avatar ? (\n <img\n src={conversation.avatar}\n alt={title}\n className=\"w-full h-full rounded-full object-cover\"\n />\n ) : type === \"DIRECT\" ? (\n getInitials(title)\n ) : (\n <ConversationTypeIcon type={type} size={18} />\n )}\n </div>\n\n {/* Content */}\n <div className=\"flex-1 min-w-0\">\n {/* Title row */}\n <div className=\"flex items-center justify-between gap-2\">\n <div className=\"flex items-center gap-1.5 min-w-0\">\n {type !== \"DIRECT\" && (\n <ConversationTypeIcon type={type} size={12} />\n )}\n <span\n className={cn(\n \"text-sm truncate\",\n hasUnread\n ? \"font-semibold text-foreground\"\n : \"font-medium text-foreground\",\n )}\n >\n {title}\n </span>\n {isPinned && (\n <Pin size={12} className=\"flex-shrink-0 text-muted-foreground\" />\n )}\n {isMuted && (\n <VolumeX\n size={12}\n className=\"flex-shrink-0 text-muted-foreground\"\n />\n )}\n </div>\n\n {/* Timestamp */}\n <span\n className={cn(\n \"text-xs flex-shrink-0\",\n hasUnread ? \"text-primary font-medium\" : \"text-muted-foreground\",\n )}\n >\n {lastMessage\n ? formatRelativeTime(lastMessage.createdAt, timeLabels)\n : formatRelativeTime(updatedAt, timeLabels)}\n </span>\n </div>\n\n {/* Last message preview */}\n <div className=\"flex items-center justify-between gap-2 mt-0.5\">\n <p\n className={cn(\n \"text-xs truncate\",\n hasUnread ? \"text-foreground/80\" : \"text-muted-foreground\",\n )}\n >\n {lastMessage ? (\n <>\n {lastMessage.sender.name && (\n <span className=\"font-medium\">\n {lastMessage.sender.name}:{\" \"}\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 {/* Unread badge + member count */}\n <div className=\"flex items-center gap-1.5 flex-shrink-0\">\n {type !== \"DIRECT\" && (\n <span className=\"text-[10px] text-muted-foreground flex items-center gap-0.5\">\n <Users size={10} />\n {memberCount}\n </span>\n )}\n {hasUnread && (\n <span className=\"inline-flex items-center justify-center min-w-[18px] h-[18px] px-1 text-[10px] font-bold text-primary-foreground bg-primary rounded-full\">\n {unreadCount > 99 ? \"99+\" : unreadCount}\n </span>\n )}\n </div>\n </div>\n\n {sourceContext?.pagePath ? (\n <p className=\"mt-1 truncate text-[11px] text-muted-foreground\">\n {sourceContext.contextTitle || sourceContext.pagePath}\n </p>\n ) : null}\n </div>\n </button>\n );\n});\n"],"mappings":"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"}
1
+ {"version":3,"file":"ConversationCard.js","names":[],"sources":["../../../src/conversations/components/ConversationCard.tsx"],"sourcesContent":["import { memo } from \"react\";\nimport { useI18n } from \"@burdenoff/fe-libs/shared/providers/shell/I18nProvider\";\nimport {\n MessageCircle,\n Hash,\n Users,\n Megaphone,\n Bot,\n HeadphonesIcon,\n Pin,\n VolumeX,\n} from \"lucide-react\";\nimport { cn } from \"../utils/cn\";\nimport type { Conversation } from \"../types\";\n\ninterface ConversationCardProps {\n conversation: Conversation;\n isActive?: boolean;\n onClick?: (conversation: Conversation) => void;\n}\n\n/**\n * Conversation type icon mapping\n */\nfunction ConversationTypeIcon({\n type,\n size = 16,\n}: {\n type: Conversation[\"type\"];\n size?: number;\n}) {\n switch (type) {\n case \"DIRECT\":\n return <MessageCircle size={size} />;\n case \"GROUP\":\n return <Users size={size} />;\n case \"CHANNEL\":\n return <Hash size={size} />;\n case \"BROADCAST\":\n case \"ANNOUNCEMENT\":\n return <Megaphone size={size} />;\n case \"BOT\":\n return <Bot size={size} />;\n case \"SUPPORT\":\n return <HeadphonesIcon size={size} />;\n default:\n return <MessageCircle size={size} />;\n }\n}\n\n/**\n * Format a timestamp relative to now (e.g., \"2m ago\", \"3h\", \"Yesterday\")\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 diffSec = Math.floor(diffMs / 1000);\n const diffMin = Math.floor(diffSec / 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\n/**\n * Generate initials from a conversation title\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 * ConversationCard - Displays a conversation in the list view.\n * Shows avatar, title, last message preview, unread count, and metadata.\n */\nexport const ConversationCard = memo(function ConversationCard({\n conversation,\n isActive = false,\n onClick,\n}: ConversationCardProps) {\n const {\n title,\n type,\n lastMessage,\n unreadCount,\n isPinned,\n isMuted,\n memberCount,\n updatedAt,\n sourceContext,\n } = conversation;\n\n const hasUnread = unreadCount > 0;\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 return (\n <button\n data-tour=\"conversation-card\"\n type=\"button\"\n onClick={() => onClick?.(conversation)}\n className={cn(\n \"w-full flex items-start gap-3 px-4 py-3 text-left rounded-lg\",\n \"transition-all duration-150 ease-in-out\",\n \"hover:bg-muted/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\",\n isActive && \"bg-primary/5 border-l-2 border-l-primary\",\n !isActive && \"border-l-2 border-l-transparent\",\n hasUnread && !isActive && \"bg-muted/20\",\n )}\n >\n {/* Avatar / Type Icon */}\n <div\n className={cn(\n \"flex-shrink-0 w-10 h-10 rounded-full flex items-center justify-center text-sm font-semibold\",\n type === \"DIRECT\"\n ? \"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 ? \"bg-accent text-accent-foreground\"\n : \"bg-muted text-muted-foreground\",\n )}\n >\n {conversation.avatar ? (\n <img\n src={conversation.avatar}\n alt={title}\n className=\"w-full h-full rounded-full object-cover\"\n />\n ) : type === \"DIRECT\" ? (\n getInitials(title)\n ) : (\n <ConversationTypeIcon type={type} size={18} />\n )}\n </div>\n\n {/* Content */}\n <div className=\"flex-1 min-w-0\">\n {/* Title row */}\n <div className=\"flex items-center justify-between gap-2\">\n <div className=\"flex items-center gap-1.5 min-w-0\">\n {type !== \"DIRECT\" && (\n <ConversationTypeIcon type={type} size={12} />\n )}\n <span\n className={cn(\n \"text-sm truncate\",\n hasUnread\n ? \"font-semibold text-foreground\"\n : \"font-medium text-foreground\",\n )}\n >\n {title}\n </span>\n {isPinned && (\n <Pin size={12} className=\"flex-shrink-0 text-muted-foreground\" />\n )}\n {isMuted && (\n <VolumeX\n size={12}\n className=\"flex-shrink-0 text-muted-foreground\"\n />\n )}\n </div>\n\n {/* Timestamp */}\n <span\n className={cn(\n \"text-xs flex-shrink-0\",\n hasUnread ? \"text-primary font-medium\" : \"text-muted-foreground\",\n )}\n >\n {lastMessage\n ? formatRelativeTime(lastMessage.createdAt, timeLabels)\n : formatRelativeTime(updatedAt, timeLabels)}\n </span>\n </div>\n\n {/* Last message preview */}\n <div className=\"flex items-center justify-between gap-2 mt-0.5\">\n <p\n className={cn(\n \"text-xs truncate\",\n hasUnread ? \"text-foreground/80\" : \"text-muted-foreground\",\n )}\n >\n {lastMessage ? (\n <>\n {lastMessage.sender.name && (\n <span className=\"font-medium\">\n {lastMessage.sender.name}:{\" \"}\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 {/* Unread badge + member count */}\n <div className=\"flex items-center gap-1.5 flex-shrink-0\">\n {type !== \"DIRECT\" && (\n <span className=\"text-[10px] text-muted-foreground flex items-center gap-0.5\">\n <Users size={10} />\n {memberCount}\n </span>\n )}\n {hasUnread && (\n <span className=\"inline-flex items-center justify-center min-w-[18px] h-[18px] px-1 text-[10px] font-bold text-primary-foreground bg-primary rounded-full\">\n {unreadCount > 99 ? \"99+\" : unreadCount}\n </span>\n )}\n </div>\n </div>\n\n {sourceContext?.pagePath ? (\n <p className=\"mt-1 truncate text-[11px] text-muted-foreground\">\n {sourceContext.contextTitle || sourceContext.pagePath}\n </p>\n ) : null}\n </div>\n </button>\n );\n});\n"],"mappings":"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"}
@@ -1,8 +1,8 @@
1
1
  import { ComponentType, LazyExoticComponent, ReactNode } from 'react';
2
2
 
3
- export declare function lazyRetry<T extends ComponentType<unknown>>(factory: () => Promise<{
4
- default: T;
5
- }>): LazyExoticComponent<T>;
3
+ export declare function lazyRetry<TProps extends object = object>(factory: () => Promise<{
4
+ default: ComponentType<TProps>;
5
+ }>): LazyExoticComponent<ComponentType<TProps>>;
6
6
  /**
7
7
  * Fallback spinner rendered while a lazy-loaded page chunk is resolving.
8
8
  */
@@ -1 +1 @@
1
- {"version":3,"file":"LazyPageBoundary.d.ts","sourceRoot":"","sources":["../../../src/conversations/components/LazyPageBoundary.tsx"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,aAAa,EAElB,KAAK,mBAAmB,EACxB,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AA8Ef,wBAAgB,SAAS,CAAC,CAAC,SAAS,aAAa,CAAC,OAAO,CAAC,EACxD,OAAO,EAAE,MAAM,OAAO,CAAC;IAAE,OAAO,EAAE,CAAC,CAAA;CAAE,CAAC,GACrC,mBAAmB,CAAC,CAAC,CAAC,CAaxB;AAED;;GAEG;AACH,wBAAgB,UAAU,4CAMzB;AAqND;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAAC,EAC/B,QAAQ,EACR,MAAM,GACP,EAAE;IACD,QAAQ,EAAE,SAAS,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,2CAUA"}
1
+ {"version":3,"file":"LazyPageBoundary.d.ts","sourceRoot":"","sources":["../../../src/conversations/components/LazyPageBoundary.tsx"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,aAAa,EAElB,KAAK,mBAAmB,EACxB,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AA8Ef,wBAAgB,SAAS,CAAC,MAAM,SAAS,MAAM,GAAG,MAAM,EACtD,OAAO,EAAE,MAAM,OAAO,CAAC;IAAE,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;CAAE,CAAC,GACzD,mBAAmB,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC,CAe5C;AAED;;GAEG;AACH,wBAAgB,UAAU,4CAMzB;AAqND;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAAC,EAC/B,QAAQ,EACR,MAAM,GACP,EAAE;IACD,QAAQ,EAAE,SAAS,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,2CAUA"}
@@ -1 +1 @@
1
- {"version":3,"file":"LazyPageBoundary.js","names":[],"sources":["../../../src/conversations/components/LazyPageBoundary.tsx"],"sourcesContent":["import {\n Component,\n Suspense,\n lazy,\n type ComponentType,\n type ErrorInfo,\n type LazyExoticComponent,\n type ReactNode,\n} from \"react\";\nimport { Loader2, AlertTriangle, RefreshCw } from \"lucide-react\";\nimport { useTr } from \"../utils/i18n\";\n\n/**\n * Heuristic match for chunk-load failures raised by the runtime bundler.\n *\n * Covers:\n * - Webpack: `error.name === \"ChunkLoadError\"`, `\"Loading chunk X failed\"`\n * - Vite / modern ESM: `\"Failed to fetch dynamically imported module\"`\n * - Safari module import failures: `\"Importing a module script failed\"`\n * - Generic `TypeError: Failed to fetch` thrown from a dynamic import whose\n * module path was stripped by the browser (observed in some Vite+proxy\n * configurations where the URL never reaches the origin)\n *\n * This is inherently heuristic — different bundlers and browsers phrase\n * chunk-load failures differently and new phrasings can appear. We err on\n * the side of matching rather than missing; a false positive triggers a\n * page reload which is still a reasonable recovery for other transient\n * network-side failures. A false negative leaves the boundary in its\n * state-reset path, which is also safe (just less effective than reload\n * for a true chunk failure).\n */\nfunction isChunkLoadError(error: Error | null | undefined): boolean {\n if (!error) return false;\n if (error.name === \"ChunkLoadError\") return true;\n const message = error.message ?? \"\";\n return (\n message.includes(\"Loading chunk\") ||\n message.includes(\"Failed to fetch dynamically imported module\") ||\n message.includes(\"Importing a module script failed\") ||\n // Vite + some CDN/proxy combos surface as \"TypeError: Failed to fetch\"\n // on a dynamic `import()` call. Match the bare phrase as a last-resort\n // hint; there's no cleaner signal short of probing the stack trace.\n message.includes(\"Failed to fetch\")\n );\n}\n\n/**\n * Retry-aware `React.lazy` wrapper.\n *\n * `React.lazy` calls the factory once and caches the resulting promise.\n * If that promise rejects, the cached rejection is replayed on every\n * subsequent render of the same `LazyExoticComponent` — a plain\n * error-boundary reset simply re-throws the cached error without\n * issuing a new network request.\n *\n * `lazyRetry` gives the initial chunk load one silent auto-retry — but\n * **only for transient chunk-load failures** detected by `isChunkLoadError`.\n * Non-transient errors (import syntax errors, missing exports, bundler\n * configuration bugs) are rethrown after the first failure so the\n * `LazyErrorBoundary` activates immediately instead of doubling the\n * latency with a guaranteed-to-fail second network round-trip.\n *\n * For a user-driven retry after *both* attempts fail, see\n * `LazyPageBoundary` below: the only reliable way to discard the cached\n * rejection on a `LazyExoticComponent` is a full page reload, so the\n * boundary's \"Try again\" button triggers `window.location.reload()` for\n * detected chunk-load failures. For non-chunk render errors the boundary\n * instead resets its state and bumps a `retryKey` so the suspense\n * subtree re-mounts cleanly.\n *\n * @example\n * const AdminModerationPage = lazyRetry(() =>\n * import(\"./pages/AdminModerationPage\").then((m) => ({\n * default: m.AdminModerationPage,\n * })),\n * );\n */\n/**\n * Small delay before the silent auto-retry in `lazyRetry`. Fires between\n * the first failed `import()` and the second attempt so stale CDN / edge\n * caches have a chance to revalidate before the second request lands.\n * A true network outage still surfaces after this delay; a cache-miss\n * typically recovers.\n */\nconst LAZY_RETRY_DELAY_MS = 300;\n\nexport function lazyRetry<T extends ComponentType<unknown>>(\n factory: () => Promise<{ default: T }>,\n): LazyExoticComponent<T> {\n return lazy(() =>\n factory().catch((err: unknown) => {\n if (err instanceof Error && isChunkLoadError(err)) {\n return new Promise<{ default: T }>((resolve, reject) => {\n setTimeout(() => {\n factory().then(resolve, reject);\n }, LAZY_RETRY_DELAY_MS);\n });\n }\n throw err;\n }),\n );\n}\n\n/**\n * Fallback spinner rendered while a lazy-loaded page chunk is resolving.\n */\nexport function PageLoader() {\n return (\n <div className=\"flex items-center justify-center min-h-[400px]\">\n <Loader2 className=\"h-8 w-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=\"h-8 w-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=\"h-4 w-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 {\n Component,\n Suspense,\n lazy,\n type ComponentType,\n type ErrorInfo,\n type LazyExoticComponent,\n type ReactNode,\n} from \"react\";\nimport { Loader2, AlertTriangle, RefreshCw } from \"lucide-react\";\nimport { useTr } from \"../utils/i18n\";\n\n/**\n * Heuristic match for chunk-load failures raised by the runtime bundler.\n *\n * Covers:\n * - Webpack: `error.name === \"ChunkLoadError\"`, `\"Loading chunk X failed\"`\n * - Vite / modern ESM: `\"Failed to fetch dynamically imported module\"`\n * - Safari module import failures: `\"Importing a module script failed\"`\n * - Generic `TypeError: Failed to fetch` thrown from a dynamic import whose\n * module path was stripped by the browser (observed in some Vite+proxy\n * configurations where the URL never reaches the origin)\n *\n * This is inherently heuristic — different bundlers and browsers phrase\n * chunk-load failures differently and new phrasings can appear. We err on\n * the side of matching rather than missing; a false positive triggers a\n * page reload which is still a reasonable recovery for other transient\n * network-side failures. A false negative leaves the boundary in its\n * state-reset path, which is also safe (just less effective than reload\n * for a true chunk failure).\n */\nfunction isChunkLoadError(error: Error | null | undefined): boolean {\n if (!error) return false;\n if (error.name === \"ChunkLoadError\") return true;\n const message = error.message ?? \"\";\n return (\n message.includes(\"Loading chunk\") ||\n message.includes(\"Failed to fetch dynamically imported module\") ||\n message.includes(\"Importing a module script failed\") ||\n // Vite + some CDN/proxy combos surface as \"TypeError: Failed to fetch\"\n // on a dynamic `import()` call. Match the bare phrase as a last-resort\n // hint; there's no cleaner signal short of probing the stack trace.\n message.includes(\"Failed to fetch\")\n );\n}\n\n/**\n * Retry-aware `React.lazy` wrapper.\n *\n * `React.lazy` calls the factory once and caches the resulting promise.\n * If that promise rejects, the cached rejection is replayed on every\n * subsequent render of the same `LazyExoticComponent` — a plain\n * error-boundary reset simply re-throws the cached error without\n * issuing a new network request.\n *\n * `lazyRetry` gives the initial chunk load one silent auto-retry — but\n * **only for transient chunk-load failures** detected by `isChunkLoadError`.\n * Non-transient errors (import syntax errors, missing exports, bundler\n * configuration bugs) are rethrown after the first failure so the\n * `LazyErrorBoundary` activates immediately instead of doubling the\n * latency with a guaranteed-to-fail second network round-trip.\n *\n * For a user-driven retry after *both* attempts fail, see\n * `LazyPageBoundary` below: the only reliable way to discard the cached\n * rejection on a `LazyExoticComponent` is a full page reload, so the\n * boundary's \"Try again\" button triggers `window.location.reload()` for\n * detected chunk-load failures. For non-chunk render errors the boundary\n * instead resets its state and bumps a `retryKey` so the suspense\n * subtree re-mounts cleanly.\n *\n * @example\n * const AdminModerationPage = lazyRetry(() =>\n * import(\"./pages/AdminModerationPage\").then((m) => ({\n * default: m.AdminModerationPage,\n * })),\n * );\n */\n/**\n * Small delay before the silent auto-retry in `lazyRetry`. Fires between\n * the first failed `import()` and the second attempt so stale CDN / edge\n * caches have a chance to revalidate before the second request lands.\n * A true network outage still surfaces after this delay; a cache-miss\n * typically recovers.\n */\nconst LAZY_RETRY_DELAY_MS = 300;\n\nexport function lazyRetry<TProps extends object = object>(\n factory: () => Promise<{ default: ComponentType<TProps> }>,\n): LazyExoticComponent<ComponentType<TProps>> {\n return lazy(() =>\n factory().catch((err: unknown) => {\n if (err instanceof Error && isChunkLoadError(err)) {\n return new Promise<{ default: ComponentType<TProps> }>(\n (resolve, reject) => {\n setTimeout(() => {\n factory().then(resolve, reject);\n }, LAZY_RETRY_DELAY_MS);\n },\n );\n }\n throw err;\n }),\n );\n}\n\n/**\n * Fallback spinner rendered while a lazy-loaded page chunk is resolving.\n */\nexport function PageLoader() {\n return (\n <div className=\"flex items-center justify-center min-h-[400px]\">\n <Loader2 className=\"h-8 w-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=\"h-8 w-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=\"h-4 w-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 +1 @@
1
- {"version":3,"file":"MessageBubble.d.ts","sourceRoot":"","sources":["../../../src/conversations/components/MessageBubble.tsx"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AAExC,UAAU,kBAAkB;IAC1B,OAAO,EAAE,OAAO,CAAC;IACjB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;IACrC,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACrD,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;IACpC,QAAQ,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,WAAW,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,KAAK,IAAI,CAAC;CAC9D;AAiDD;;;;GAIG;AACH,eAAO,MAAM,aAAa,0DA6SxB,CAAC"}
1
+ {"version":3,"file":"MessageBubble.d.ts","sourceRoot":"","sources":["../../../src/conversations/components/MessageBubble.tsx"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AAExC,UAAU,kBAAkB;IAC1B,OAAO,EAAE,OAAO,CAAC;IACjB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;IACrC,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACrD,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;IACpC,QAAQ,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,WAAW,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,KAAK,IAAI,CAAC;CAC9D;AAiDD;;;;GAIG;AACH,eAAO,MAAM,aAAa,0DAoTxB,CAAC"}
@@ -23,7 +23,7 @@ function v({ status: e, failedLabel: t }) {
23
23
  });
24
24
  case "READ": return /* @__PURE__ */ m(i, {
25
25
  size: 12,
26
- className: "text-accent-blue"
26
+ className: "text-primary"
27
27
  });
28
28
  case "FAILED": return /* @__PURE__ */ m("span", {
29
29
  className: "text-[10px] text-destructive font-medium",
@@ -39,17 +39,17 @@ var b = t(function({ message: t, isOwnMessage: r = !1, showAvatar: i = !0, showS
39
39
  let [O, k] = n(!1), [A, j] = n(!1), [M, N] = n(!1), { t: P } = g(), F = (e, t) => {
40
40
  let n = P(e);
41
41
  return n === e ? t : n;
42
- }, { sender: I, content: L, type: R, status: z, reactions: B, attachments: V, isEdited: H, createdAt: U, isPinned: W } = t;
43
- return R === "SYSTEM" ? /* @__PURE__ */ m("div", {
42
+ }, { sender: I, content: L, type: R, status: z, reactions: B, attachments: V, isEdited: H, createdAt: U, isPinned: W } = t, G = R === "SYSTEM", K = M || O || A;
43
+ return G ? /* @__PURE__ */ m("div", {
44
44
  className: "flex justify-center my-2 px-4",
45
45
  children: /* @__PURE__ */ m("span", {
46
46
  className: "text-xs text-muted-foreground bg-muted/50 px-3 py-1 rounded-full",
47
47
  children: L
48
48
  })
49
49
  }) : /* @__PURE__ */ h("div", {
50
+ onMouseEnter: () => N(!0),
51
+ onMouseLeave: () => N(!1),
50
52
  className: e("group flex gap-2 px-4 py-1", r ? "flex-row-reverse" : "flex-row"),
51
- onMouseEnter: () => k(!0),
52
- onMouseLeave: () => k(!1),
53
53
  children: [
54
54
  i ? /* @__PURE__ */ m("div", {
55
55
  className: e("flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center text-xs font-semibold mt-0.5", r ? "bg-primary/10 text-primary" : "bg-muted text-muted-foreground"),
@@ -147,15 +147,21 @@ var b = t(function({ message: t, isOwnMessage: r = !1, showAvatar: i = !0, showS
147
147
  })
148
148
  ]
149
149
  }),
150
- (O || A || M) && /* @__PURE__ */ h("div", {
151
- className: e("relative flex items-center gap-0.5 self-start mt-1", "animate-in fade-in-0 zoom-in-95 duration-150"),
150
+ /* @__PURE__ */ h("div", {
151
+ "data-tour": "conversation-message-actions",
152
+ tabIndex: 0,
153
+ onFocus: () => N(!0),
154
+ onBlur: (e) => {
155
+ e.currentTarget.contains(e.relatedTarget) || N(!1);
156
+ },
157
+ className: e("relative mt-1 flex items-center gap-0.5 self-start rounded-md opacity-100 transition-opacity duration-150 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 sm:opacity-0 sm:group-focus-within:opacity-100 sm:group-hover:opacity-100", K && "sm:opacity-100"),
152
158
  children: [
153
159
  t.permissions.canReact && /* @__PURE__ */ m(x, {
154
160
  icon: /* @__PURE__ */ m(d, { size: 14 }),
155
161
  label: F("conversations.react", "React"),
156
- pressed: A,
162
+ pressed: O,
157
163
  onClick: () => {
158
- j((e) => !e), N(!1);
164
+ k((e) => !e), j(!1);
159
165
  }
160
166
  }),
161
167
  t.permissions.canReply && /* @__PURE__ */ m(x, {
@@ -176,12 +182,12 @@ var b = t(function({ message: t, isOwnMessage: r = !1, showAvatar: i = !0, showS
176
182
  /* @__PURE__ */ m(x, {
177
183
  icon: /* @__PURE__ */ m(s, { size: 14 }),
178
184
  label: F("conversations.more", "More"),
179
- pressed: M,
185
+ pressed: A,
180
186
  onClick: () => {
181
- N((e) => !e), j(!1);
187
+ j((e) => !e), k(!1);
182
188
  }
183
189
  }),
184
- A && /* @__PURE__ */ m("div", {
190
+ O && /* @__PURE__ */ m("div", {
185
191
  className: "absolute top-full z-10 mt-1 flex items-center gap-1 rounded-lg border border-border bg-popover p-1 shadow-lg",
186
192
  children: [
187
193
  "👍",
@@ -192,19 +198,19 @@ var b = t(function({ message: t, isOwnMessage: r = !1, showAvatar: i = !0, showS
192
198
  ].map((e) => /* @__PURE__ */ m("button", {
193
199
  type: "button",
194
200
  onClick: () => {
195
- w?.(t.id, e), j(!1);
201
+ w?.(t.id, e), k(!1);
196
202
  },
197
203
  className: "rounded-md px-2 py-1 text-sm hover:bg-muted/80",
198
204
  "aria-label": `${F("conversations.reactWith", "React with")} ${e}`,
199
205
  children: e
200
206
  }, e))
201
207
  }),
202
- M && t.permissions.canPin && D && /* @__PURE__ */ m("div", {
208
+ A && t.permissions.canPin && D && /* @__PURE__ */ m("div", {
203
209
  className: "absolute right-0 top-full z-10 mt-1 min-w-40 rounded-lg border border-border bg-popover p-1 shadow-lg",
204
210
  children: /* @__PURE__ */ h("button", {
205
211
  type: "button",
206
212
  onClick: () => {
207
- D(t.id, t.isPinned), N(!1);
213
+ D(t.id, t.isPinned), j(!1);
208
214
  },
209
215
  className: "flex w-full items-center gap-2 rounded-md px-3 py-2 text-left text-sm text-foreground hover:bg-muted/80",
210
216
  children: [/* @__PURE__ */ m(l, { size: 14 }), t.isPinned ? F("conversations.unpinMessage", "Unpin message") : F("conversations.pinMessage", "Pin message")]
@@ -222,7 +228,7 @@ function x({ icon: e, label: t, onClick: n, pressed: r }) {
222
228
  "aria-label": t,
223
229
  "aria-pressed": r,
224
230
  title: t,
225
- className: "p-1 rounded-md text-muted-foreground hover:text-foreground hover:bg-muted/80 transition-colors",
231
+ className: "flex min-h-9 min-w-9 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted/80 hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
226
232
  children: e
227
233
  });
228
234
  }
@@ -1 +1 @@
1
- {"version":3,"file":"MessageBubble.js","names":[],"sources":["../../../src/conversations/components/MessageBubble.tsx"],"sourcesContent":["import { memo, useState } from \"react\";\nimport { useI18n } from \"@burdenoff/fe-libs/shared/providers/shell/I18nProvider\";\nimport {\n FileText,\n Image as ImageIcon,\n Reply,\n MoreHorizontal,\n Pencil,\n Trash2,\n Pin,\n SmilePlus,\n Check,\n CheckCheck,\n} from \"lucide-react\";\nimport { cn } from \"../utils/cn\";\nimport type { Message } from \"../types\";\n\ninterface MessageBubbleProps {\n message: Message;\n isOwnMessage?: boolean;\n showAvatar?: boolean;\n showSender?: boolean;\n onReply?: (message: Message) => void;\n onReact?: (messageId: string, emoji: string) => void;\n onEdit?: (message: Message) => void;\n onDelete?: (messageId: string) => void;\n onTogglePin?: (messageId: string, isPinned: boolean) => void;\n}\n\n/**\n * Format a message timestamp for display\n */\nfunction formatMessageTime(dateString: string): string {\n const date = new Date(dateString);\n return date.toLocaleTimeString(undefined, {\n hour: \"2-digit\",\n minute: \"2-digit\",\n });\n}\n\n/**\n * Message status indicator\n */\nfunction MessageStatusIcon({\n status,\n failedLabel,\n}: {\n status: Message[\"status\"];\n failedLabel: string;\n}) {\n switch (status) {\n case \"SENDING\":\n return (\n <span className=\"w-3 h-3 rounded-full border border-muted-foreground animate-pulse\" />\n );\n case \"SENT\":\n return <Check size={12} className=\"text-muted-foreground\" />;\n case \"DELIVERED\":\n return <CheckCheck size={12} className=\"text-muted-foreground\" />;\n case \"READ\":\n return <CheckCheck size={12} className=\"text-accent-blue\" />;\n case \"FAILED\":\n return (\n <span className=\"text-[10px] text-destructive font-medium\">\n {failedLabel}\n </span>\n );\n default:\n return null;\n }\n}\n\nfunction isImageAttachment(mimeType: string): boolean {\n return mimeType.startsWith(\"image/\");\n}\n\n/**\n * MessageBubble - Displays a single message in a chat view.\n * Supports own messages (right-aligned) and other messages (left-aligned).\n * Shows sender info, content, timestamp, reactions, and action buttons.\n */\nexport const MessageBubble = memo(function MessageBubble({\n message,\n isOwnMessage = false,\n showAvatar = true,\n showSender = true,\n onReply,\n onReact,\n onEdit,\n onDelete,\n onTogglePin,\n}: MessageBubbleProps) {\n const [isHovered, setIsHovered] = useState(false);\n const [showReactionPicker, setShowReactionPicker] = useState(false);\n const [showMoreMenu, setShowMoreMenu] = useState(false);\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\n const {\n sender,\n content,\n type,\n status,\n reactions,\n attachments,\n isEdited,\n createdAt,\n isPinned,\n } = message;\n\n const isSystem = type === \"SYSTEM\";\n\n // System messages render differently\n if (isSystem) {\n return (\n <div className=\"flex justify-center my-2 px-4\">\n <span className=\"text-xs text-muted-foreground bg-muted/50 px-3 py-1 rounded-full\">\n {content}\n </span>\n </div>\n );\n }\n\n return (\n <div\n className={cn(\n \"group flex gap-2 px-4 py-1\",\n isOwnMessage ? \"flex-row-reverse\" : \"flex-row\",\n )}\n onMouseEnter={() => setIsHovered(true)}\n onMouseLeave={() => setIsHovered(false)}\n >\n {/* Avatar */}\n {showAvatar ? (\n <div\n className={cn(\n \"flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center text-xs font-semibold mt-0.5\",\n isOwnMessage\n ? \"bg-primary/10 text-primary\"\n : \"bg-muted text-muted-foreground\",\n )}\n >\n {sender.avatarUrl ? (\n <img\n src={sender.avatarUrl}\n alt={sender.name}\n className=\"w-full h-full rounded-full object-cover\"\n />\n ) : (\n sender.name\n .split(\" \")\n .map((w) => w[0])\n .join(\"\")\n .toUpperCase()\n .slice(0, 2)\n )}\n </div>\n ) : (\n <div className=\"w-8 flex-shrink-0\" />\n )}\n\n {/* Message content */}\n <div\n className={cn(\n \"flex flex-col max-w-[85%] sm:max-w-[70%]\",\n isOwnMessage ? \"items-end\" : \"items-start\",\n )}\n >\n {/* Sender name + pin indicator */}\n {showSender && !isOwnMessage && (\n <div className=\"flex items-center gap-1.5 mb-0.5\">\n <span className=\"text-xs font-semibold text-foreground\">\n {sender.name}\n </span>\n {isPinned && <Pin size={10} className=\"text-status-warning-text\" />}\n </div>\n )}\n\n {/* Bubble */}\n <div\n className={cn(\n \"relative rounded-2xl px-3.5 py-2 text-sm leading-relaxed\",\n isOwnMessage\n ? \"bg-primary text-primary-foreground rounded-tr-md\"\n : \"bg-muted/70 text-foreground rounded-tl-md\",\n )}\n >\n {/* Text content */}\n <p className=\"whitespace-pre-wrap break-words\">{content}</p>\n\n {/* Attachments */}\n {attachments.length > 0 && (\n <div className=\"mt-2 grid gap-2\">\n {attachments.map((att) => {\n const attachmentClass = cn(\n \"flex min-w-0 items-center gap-2 rounded-lg px-2 py-1.5 text-xs transition-colors\",\n isOwnMessage\n ? \"bg-primary-foreground/10 text-primary-foreground hover:bg-primary-foreground/15\"\n : \"bg-background/50 text-foreground hover:bg-background/70\",\n );\n const attachmentContent = (\n <>\n <div className=\"flex size-10 flex-shrink-0 items-center justify-center overflow-hidden rounded-md bg-background/40\">\n {att.thumbnail && isImageAttachment(att.mimeType) ? (\n <img\n src={att.thumbnail}\n alt={att.fileName}\n className=\"h-full w-full object-contain\"\n />\n ) : isImageAttachment(att.mimeType) ? (\n <ImageIcon size={16} />\n ) : (\n <FileText size={16} />\n )}\n </div>\n <div className=\"min-w-0 flex-1\">\n <div className=\"truncate font-medium\">{att.fileName}</div>\n <div className=\"truncate text-[10px] opacity-70\">\n {formatFileSize(att.fileSize)} - {att.mimeType}\n </div>\n </div>\n </>\n );\n\n return att.downloadUrl ? (\n <a\n key={att.id}\n href={att.downloadUrl}\n target=\"_blank\"\n rel=\"noreferrer\"\n className={attachmentClass}\n title={att.fileName}\n >\n {attachmentContent}\n </a>\n ) : (\n <div\n key={att.id}\n className={attachmentClass}\n title={att.fileName}\n >\n {attachmentContent}\n </div>\n );\n })}\n </div>\n )}\n </div>\n\n {/* Reactions */}\n {reactions.length > 0 && (\n <div className=\"flex flex-wrap gap-1 mt-1\">\n {reactions.map((reaction) => (\n <button\n key={reaction.emoji}\n type=\"button\"\n onClick={() => onReact?.(message.id, reaction.emoji)}\n className={cn(\n \"inline-flex items-center gap-1 px-1.5 py-0.5 rounded-full text-xs\",\n \"border transition-colors\",\n reaction.currentUserReacted\n ? \"bg-primary/10 border-primary/30 text-primary\"\n : \"bg-muted/50 border-border text-muted-foreground hover:bg-muted\",\n )}\n >\n <span>{reaction.emoji}</span>\n <span className=\"font-medium\">{reaction.count}</span>\n </button>\n ))}\n </div>\n )}\n\n {/* Timestamp + status row */}\n <div className=\"flex items-center gap-1.5 mt-0.5 px-1\">\n <span className=\"text-[10px] text-muted-foreground\">\n {formatMessageTime(createdAt)}\n </span>\n {isEdited && (\n <span className=\"text-[10px] text-muted-foreground italic\">\n {tr(\"conversations.edited\", \"edited\")}\n </span>\n )}\n {isOwnMessage && (\n <MessageStatusIcon\n status={status}\n failedLabel={tr(\"conversations.failed\", \"Failed\")}\n />\n )}\n </div>\n </div>\n\n {/* Action buttons (shown on hover) */}\n {(isHovered || showReactionPicker || showMoreMenu) && (\n <div\n className={cn(\n \"relative flex items-center gap-0.5 self-start mt-1\",\n \"animate-in fade-in-0 zoom-in-95 duration-150\",\n )}\n >\n {message.permissions.canReact && (\n <ActionButton\n icon={<SmilePlus size={14} />}\n label={tr(\"conversations.react\", \"React\")}\n pressed={showReactionPicker}\n onClick={() => {\n setShowReactionPicker((value) => !value);\n setShowMoreMenu(false);\n }}\n />\n )}\n {message.permissions.canReply && (\n <ActionButton\n icon={<Reply size={14} />}\n label={tr(\"conversations.reply\", \"Reply\")}\n onClick={() => onReply?.(message)}\n />\n )}\n {message.permissions.canEdit && (\n <ActionButton\n icon={<Pencil size={14} />}\n label={tr(\"conversations.edit\", \"Edit\")}\n onClick={() => onEdit?.(message)}\n />\n )}\n {message.permissions.canDelete && (\n <ActionButton\n icon={<Trash2 size={14} />}\n label={tr(\"conversations.delete\", \"Delete\")}\n onClick={() => onDelete?.(message.id)}\n />\n )}\n <ActionButton\n icon={<MoreHorizontal size={14} />}\n label={tr(\"conversations.more\", \"More\")}\n pressed={showMoreMenu}\n onClick={() => {\n setShowMoreMenu((value) => !value);\n setShowReactionPicker(false);\n }}\n />\n {showReactionPicker && (\n <div className=\"absolute top-full z-10 mt-1 flex items-center gap-1 rounded-lg border border-border bg-popover p-1 shadow-lg\">\n {[\"👍\", \"❤️\", \"🎉\", \"👀\", \"✅\"].map((emoji) => (\n <button\n key={emoji}\n type=\"button\"\n onClick={() => {\n onReact?.(message.id, emoji);\n setShowReactionPicker(false);\n }}\n className=\"rounded-md px-2 py-1 text-sm hover:bg-muted/80\"\n aria-label={`${tr(\"conversations.reactWith\", \"React with\")} ${emoji}`}\n >\n {emoji}\n </button>\n ))}\n </div>\n )}\n {showMoreMenu && message.permissions.canPin && onTogglePin && (\n <div className=\"absolute right-0 top-full z-10 mt-1 min-w-40 rounded-lg border border-border bg-popover p-1 shadow-lg\">\n <button\n type=\"button\"\n onClick={() => {\n onTogglePin(message.id, message.isPinned);\n setShowMoreMenu(false);\n }}\n className=\"flex w-full items-center gap-2 rounded-md px-3 py-2 text-left text-sm text-foreground hover:bg-muted/80\"\n >\n <Pin size={14} />\n {message.isPinned\n ? tr(\"conversations.unpinMessage\", \"Unpin message\")\n : tr(\"conversations.pinMessage\", \"Pin message\")}\n </button>\n </div>\n )}\n </div>\n )}\n </div>\n );\n});\n\n/**\n * Small icon button for message actions\n */\nfunction ActionButton({\n icon,\n label,\n onClick,\n pressed,\n}: {\n icon: React.ReactNode;\n label: string;\n onClick: () => void;\n pressed?: boolean;\n}) {\n return (\n <button\n type=\"button\"\n onClick={onClick}\n aria-label={label}\n aria-pressed={pressed}\n title={label}\n className=\"p-1 rounded-md text-muted-foreground hover:text-foreground hover:bg-muted/80 transition-colors\"\n >\n {icon}\n </button>\n );\n}\n\n/**\n * Format file size in human-readable form\n */\nfunction formatFileSize(bytes: number): string {\n if (bytes === 0) return \"0 B\";\n const k = 1024;\n const sizes = [\"B\", \"KB\", \"MB\", \"GB\"];\n const i = Math.floor(Math.log(bytes) / Math.log(k));\n return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"MessageBubble.js","names":[],"sources":["../../../src/conversations/components/MessageBubble.tsx"],"sourcesContent":["import { memo, useState } from \"react\";\nimport { useI18n } from \"@burdenoff/fe-libs/shared/providers/shell/I18nProvider\";\nimport {\n FileText,\n Image as ImageIcon,\n Reply,\n MoreHorizontal,\n Pencil,\n Trash2,\n Pin,\n SmilePlus,\n Check,\n CheckCheck,\n} from \"lucide-react\";\nimport { cn } from \"../utils/cn\";\nimport type { Message } from \"../types\";\n\ninterface MessageBubbleProps {\n message: Message;\n isOwnMessage?: boolean;\n showAvatar?: boolean;\n showSender?: boolean;\n onReply?: (message: Message) => void;\n onReact?: (messageId: string, emoji: string) => void;\n onEdit?: (message: Message) => void;\n onDelete?: (messageId: string) => void;\n onTogglePin?: (messageId: string, isPinned: boolean) => void;\n}\n\n/**\n * Format a message timestamp for display\n */\nfunction formatMessageTime(dateString: string): string {\n const date = new Date(dateString);\n return date.toLocaleTimeString(undefined, {\n hour: \"2-digit\",\n minute: \"2-digit\",\n });\n}\n\n/**\n * Message status indicator\n */\nfunction MessageStatusIcon({\n status,\n failedLabel,\n}: {\n status: Message[\"status\"];\n failedLabel: string;\n}) {\n switch (status) {\n case \"SENDING\":\n return (\n <span className=\"w-3 h-3 rounded-full border border-muted-foreground animate-pulse\" />\n );\n case \"SENT\":\n return <Check size={12} className=\"text-muted-foreground\" />;\n case \"DELIVERED\":\n return <CheckCheck size={12} className=\"text-muted-foreground\" />;\n case \"READ\":\n return <CheckCheck size={12} className=\"text-primary\" />;\n case \"FAILED\":\n return (\n <span className=\"text-[10px] text-destructive font-medium\">\n {failedLabel}\n </span>\n );\n default:\n return null;\n }\n}\n\nfunction isImageAttachment(mimeType: string): boolean {\n return mimeType.startsWith(\"image/\");\n}\n\n/**\n * MessageBubble - Displays a single message in a chat view.\n * Supports own messages (right-aligned) and other messages (left-aligned).\n * Shows sender info, content, timestamp, reactions, and action buttons.\n */\nexport const MessageBubble = memo(function MessageBubble({\n message,\n isOwnMessage = false,\n showAvatar = true,\n showSender = true,\n onReply,\n onReact,\n onEdit,\n onDelete,\n onTogglePin,\n}: MessageBubbleProps) {\n const [showReactionPicker, setShowReactionPicker] = useState(false);\n const [showMoreMenu, setShowMoreMenu] = useState(false);\n const [showActions, setShowActions] = useState(false);\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\n const {\n sender,\n content,\n type,\n status,\n reactions,\n attachments,\n isEdited,\n createdAt,\n isPinned,\n } = message;\n\n const isSystem = type === \"SYSTEM\";\n const actionsVisible = showActions || showReactionPicker || showMoreMenu;\n\n // System messages render differently\n if (isSystem) {\n return (\n <div className=\"flex justify-center my-2 px-4\">\n <span className=\"text-xs text-muted-foreground bg-muted/50 px-3 py-1 rounded-full\">\n {content}\n </span>\n </div>\n );\n }\n\n return (\n <div\n onMouseEnter={() => setShowActions(true)}\n onMouseLeave={() => setShowActions(false)}\n className={cn(\n \"group flex gap-2 px-4 py-1\",\n isOwnMessage ? \"flex-row-reverse\" : \"flex-row\",\n )}\n >\n {/* Avatar */}\n {showAvatar ? (\n <div\n className={cn(\n \"flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center text-xs font-semibold mt-0.5\",\n isOwnMessage\n ? \"bg-primary/10 text-primary\"\n : \"bg-muted text-muted-foreground\",\n )}\n >\n {sender.avatarUrl ? (\n <img\n src={sender.avatarUrl}\n alt={sender.name}\n className=\"w-full h-full rounded-full object-cover\"\n />\n ) : (\n sender.name\n .split(\" \")\n .map((w) => w[0])\n .join(\"\")\n .toUpperCase()\n .slice(0, 2)\n )}\n </div>\n ) : (\n <div className=\"w-8 flex-shrink-0\" />\n )}\n\n {/* Message content */}\n <div\n className={cn(\n \"flex flex-col max-w-[85%] sm:max-w-[70%]\",\n isOwnMessage ? \"items-end\" : \"items-start\",\n )}\n >\n {/* Sender name + pin indicator */}\n {showSender && !isOwnMessage && (\n <div className=\"flex items-center gap-1.5 mb-0.5\">\n <span className=\"text-xs font-semibold text-foreground\">\n {sender.name}\n </span>\n {isPinned && <Pin size={10} className=\"text-status-warning-text\" />}\n </div>\n )}\n\n {/* Bubble */}\n <div\n className={cn(\n \"relative rounded-2xl px-3.5 py-2 text-sm leading-relaxed\",\n isOwnMessage\n ? \"bg-primary text-primary-foreground rounded-tr-md\"\n : \"bg-muted/70 text-foreground rounded-tl-md\",\n )}\n >\n {/* Text content */}\n <p className=\"whitespace-pre-wrap break-words\">{content}</p>\n\n {/* Attachments */}\n {attachments.length > 0 && (\n <div className=\"mt-2 grid gap-2\">\n {attachments.map((att) => {\n const attachmentClass = cn(\n \"flex min-w-0 items-center gap-2 rounded-lg px-2 py-1.5 text-xs transition-colors\",\n isOwnMessage\n ? \"bg-primary-foreground/10 text-primary-foreground hover:bg-primary-foreground/15\"\n : \"bg-background/50 text-foreground hover:bg-background/70\",\n );\n const attachmentContent = (\n <>\n <div className=\"flex size-10 flex-shrink-0 items-center justify-center overflow-hidden rounded-md bg-background/40\">\n {att.thumbnail && isImageAttachment(att.mimeType) ? (\n <img\n src={att.thumbnail}\n alt={att.fileName}\n className=\"h-full w-full object-contain\"\n />\n ) : isImageAttachment(att.mimeType) ? (\n <ImageIcon size={16} />\n ) : (\n <FileText size={16} />\n )}\n </div>\n <div className=\"min-w-0 flex-1\">\n <div className=\"truncate font-medium\">{att.fileName}</div>\n <div className=\"truncate text-[10px] opacity-70\">\n {formatFileSize(att.fileSize)} - {att.mimeType}\n </div>\n </div>\n </>\n );\n\n return att.downloadUrl ? (\n <a\n key={att.id}\n href={att.downloadUrl}\n target=\"_blank\"\n rel=\"noreferrer\"\n className={attachmentClass}\n title={att.fileName}\n >\n {attachmentContent}\n </a>\n ) : (\n <div\n key={att.id}\n className={attachmentClass}\n title={att.fileName}\n >\n {attachmentContent}\n </div>\n );\n })}\n </div>\n )}\n </div>\n\n {/* Reactions */}\n {reactions.length > 0 && (\n <div className=\"flex flex-wrap gap-1 mt-1\">\n {reactions.map((reaction) => (\n <button\n key={reaction.emoji}\n type=\"button\"\n onClick={() => onReact?.(message.id, reaction.emoji)}\n className={cn(\n \"inline-flex items-center gap-1 px-1.5 py-0.5 rounded-full text-xs\",\n \"border transition-colors\",\n reaction.currentUserReacted\n ? \"bg-primary/10 border-primary/30 text-primary\"\n : \"bg-muted/50 border-border text-muted-foreground hover:bg-muted\",\n )}\n >\n <span>{reaction.emoji}</span>\n <span className=\"font-medium\">{reaction.count}</span>\n </button>\n ))}\n </div>\n )}\n\n {/* Timestamp + status row */}\n <div className=\"flex items-center gap-1.5 mt-0.5 px-1\">\n <span className=\"text-[10px] text-muted-foreground\">\n {formatMessageTime(createdAt)}\n </span>\n {isEdited && (\n <span className=\"text-[10px] text-muted-foreground italic\">\n {tr(\"conversations.edited\", \"edited\")}\n </span>\n )}\n {isOwnMessage && (\n <MessageStatusIcon\n status={status}\n failedLabel={tr(\"conversations.failed\", \"Failed\")}\n />\n )}\n </div>\n </div>\n\n {/* Action buttons (shown on hover) */}\n <div\n data-tour=\"conversation-message-actions\"\n tabIndex={0}\n onFocus={() => setShowActions(true)}\n onBlur={(event) => {\n if (!event.currentTarget.contains(event.relatedTarget)) {\n setShowActions(false);\n }\n }}\n className={cn(\n \"relative mt-1 flex items-center gap-0.5 self-start rounded-md opacity-100 transition-opacity duration-150 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 sm:opacity-0 sm:group-focus-within:opacity-100 sm:group-hover:opacity-100\",\n actionsVisible && \"sm:opacity-100\",\n )}\n >\n {message.permissions.canReact && (\n <ActionButton\n icon={<SmilePlus size={14} />}\n label={tr(\"conversations.react\", \"React\")}\n pressed={showReactionPicker}\n onClick={() => {\n setShowReactionPicker((value) => !value);\n setShowMoreMenu(false);\n }}\n />\n )}\n {message.permissions.canReply && (\n <ActionButton\n icon={<Reply size={14} />}\n label={tr(\"conversations.reply\", \"Reply\")}\n onClick={() => onReply?.(message)}\n />\n )}\n {message.permissions.canEdit && (\n <ActionButton\n icon={<Pencil size={14} />}\n label={tr(\"conversations.edit\", \"Edit\")}\n onClick={() => onEdit?.(message)}\n />\n )}\n {message.permissions.canDelete && (\n <ActionButton\n icon={<Trash2 size={14} />}\n label={tr(\"conversations.delete\", \"Delete\")}\n onClick={() => onDelete?.(message.id)}\n />\n )}\n <ActionButton\n icon={<MoreHorizontal size={14} />}\n label={tr(\"conversations.more\", \"More\")}\n pressed={showMoreMenu}\n onClick={() => {\n setShowMoreMenu((value) => !value);\n setShowReactionPicker(false);\n }}\n />\n {showReactionPicker && (\n <div className=\"absolute top-full z-10 mt-1 flex items-center gap-1 rounded-lg border border-border bg-popover p-1 shadow-lg\">\n {[\"👍\", \"❤️\", \"🎉\", \"👀\", \"✅\"].map((emoji) => (\n <button\n key={emoji}\n type=\"button\"\n onClick={() => {\n onReact?.(message.id, emoji);\n setShowReactionPicker(false);\n }}\n className=\"rounded-md px-2 py-1 text-sm hover:bg-muted/80\"\n aria-label={`${tr(\"conversations.reactWith\", \"React with\")} ${emoji}`}\n >\n {emoji}\n </button>\n ))}\n </div>\n )}\n {showMoreMenu && message.permissions.canPin && onTogglePin && (\n <div className=\"absolute right-0 top-full z-10 mt-1 min-w-40 rounded-lg border border-border bg-popover p-1 shadow-lg\">\n <button\n type=\"button\"\n onClick={() => {\n onTogglePin(message.id, message.isPinned);\n setShowMoreMenu(false);\n }}\n className=\"flex w-full items-center gap-2 rounded-md px-3 py-2 text-left text-sm text-foreground hover:bg-muted/80\"\n >\n <Pin size={14} />\n {message.isPinned\n ? tr(\"conversations.unpinMessage\", \"Unpin message\")\n : tr(\"conversations.pinMessage\", \"Pin message\")}\n </button>\n </div>\n )}\n </div>\n </div>\n );\n});\n\n/**\n * Small icon button for message actions\n */\nfunction ActionButton({\n icon,\n label,\n onClick,\n pressed,\n}: {\n icon: React.ReactNode;\n label: string;\n onClick: () => void;\n pressed?: boolean;\n}) {\n return (\n <button\n type=\"button\"\n onClick={onClick}\n aria-label={label}\n aria-pressed={pressed}\n title={label}\n className=\"flex min-h-9 min-w-9 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted/80 hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n >\n {icon}\n </button>\n );\n}\n\n/**\n * Format file size in human-readable form\n */\nfunction formatFileSize(bytes: number): string {\n if (bytes === 0) return \"0 B\";\n const k = 1024;\n const sizes = [\"B\", \"KB\", \"MB\", \"GB\"];\n const i = Math.floor(Math.log(bytes) / Math.log(k));\n return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`;\n}\n"],"mappings":"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"}
@@ -20,6 +20,8 @@ interface PageLayoutProps {
20
20
  className?: string;
21
21
  /** Additional CSS classes for the content area */
22
22
  contentClassName?: string;
23
+ /** Stable selector for guided tours and smoke tests */
24
+ dataTour?: string;
23
25
  }
24
26
  /**
25
27
  * PageLayout - A consistent page wrapper with responsive design
@@ -30,7 +32,7 @@ interface PageLayoutProps {
30
32
  * - Animated entrance
31
33
  * - Consistent header styling
32
34
  */
33
- export declare function PageLayout({ children, title, subtitle, icon, headerActions, headerContent, maxWidth, showHeader, className, contentClassName, }: PageLayoutProps): import("react/jsx-runtime").JSX.Element;
35
+ export declare function PageLayout({ children, title, subtitle, icon, headerActions, headerContent, maxWidth, showHeader, className, contentClassName, dataTour, }: PageLayoutProps): import("react/jsx-runtime").JSX.Element;
34
36
  /**
35
37
  * PageSection - A section within a page with consistent spacing
36
38
  */
@@ -1 +1 @@
1
- {"version":3,"file":"PageLayout.d.ts","sourceRoot":"","sources":["../../../src/conversations/components/PageLayout.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAGvC,UAAU,eAAe;IACvB,QAAQ,EAAE,SAAS,CAAC;IACpB,iBAAiB;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,gCAAgC;IAChC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,8CAA8C;IAC9C,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,mDAAmD;IACnD,aAAa,CAAC,EAAE,SAAS,CAAC;IAC1B,4CAA4C;IAC5C,aAAa,CAAC,EAAE,SAAS,CAAC;IAC1B,mFAAmF;IACnF,QAAQ,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,KAAK,GAAG,KAAK,GAAG,MAAM,CAAC;IAC9D,yCAAyC;IACzC,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,+CAA+C;IAC/C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,kDAAkD;IAClD,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAYD;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CAAC,EACzB,QAAQ,EACR,KAAK,EACL,QAAQ,EACR,IAAI,EACJ,aAAa,EACb,aAAa,EACb,QAAe,EACf,UAAiB,EACjB,SAAS,EACT,gBAAgB,GACjB,EAAE,eAAe,2CAsEjB;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,EAC1B,QAAQ,EACR,KAAK,EACL,WAAW,EACX,OAAO,EACP,SAAS,GACV,EAAE;IACD,QAAQ,EAAE,SAAS,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,SAAS,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,2CAmBA;AAED;;GAEG;AACH,wBAAgB,UAAU,CAAC,EACzB,IAAI,EACJ,KAAK,EACL,WAAW,EACX,MAAM,EACN,SAAS,GACV,EAAE;IACD,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,2CAyBA"}
1
+ {"version":3,"file":"PageLayout.d.ts","sourceRoot":"","sources":["../../../src/conversations/components/PageLayout.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAGvC,UAAU,eAAe;IACvB,QAAQ,EAAE,SAAS,CAAC;IACpB,iBAAiB;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,gCAAgC;IAChC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,8CAA8C;IAC9C,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,mDAAmD;IACnD,aAAa,CAAC,EAAE,SAAS,CAAC;IAC1B,4CAA4C;IAC5C,aAAa,CAAC,EAAE,SAAS,CAAC;IAC1B,mFAAmF;IACnF,QAAQ,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,KAAK,GAAG,KAAK,GAAG,MAAM,CAAC;IAC9D,yCAAyC;IACzC,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,+CAA+C;IAC/C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,kDAAkD;IAClD,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,uDAAuD;IACvD,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAYD;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CAAC,EACzB,QAAQ,EACR,KAAK,EACL,QAAQ,EACR,IAAI,EACJ,aAAa,EACb,aAAa,EACb,QAAe,EACf,UAAiB,EACjB,SAAS,EACT,gBAAgB,EAChB,QAAQ,GACT,EAAE,eAAe,2CAuEjB;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,EAC1B,QAAQ,EACR,KAAK,EACL,WAAW,EACX,OAAO,EACP,SAAS,GACV,EAAE;IACD,QAAQ,EAAE,SAAS,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,SAAS,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,2CAmBA;AAED;;GAEG;AACH,wBAAgB,UAAU,CAAC,EACzB,IAAI,EACJ,KAAK,EACL,WAAW,EACX,MAAM,EACN,SAAS,GACV,EAAE;IACD,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,2CAyBA"}
@@ -10,8 +10,9 @@ var r = {
10
10
  "3xl": "max-w-[1600px]",
11
11
  full: "max-w-full"
12
12
  };
13
- function i({ children: i, title: a, subtitle: o, icon: s, headerActions: c, headerContent: l, maxWidth: u = "xl", showHeader: d = !0, className: f, contentClassName: p }) {
13
+ function i({ children: i, title: a, subtitle: o, icon: s, headerActions: c, headerContent: l, maxWidth: u = "xl", showHeader: d = !0, className: f, contentClassName: p, dataTour: m }) {
14
14
  return /* @__PURE__ */ t("div", {
15
+ "data-tour": m,
15
16
  className: e("min-h-full w-full", "px-4 sm:px-6 lg:px-8 xl:px-12", "py-6 sm:py-8 lg:py-10", "animate-in fade-in-0 slide-in-from-bottom-4 duration-500", f),
16
17
  children: /* @__PURE__ */ n("div", {
17
18
  className: e("mx-auto w-full", r[u], p),
@@ -1 +1 @@
1
- {"version":3,"file":"PageLayout.js","names":[],"sources":["../../../src/conversations/components/PageLayout.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport { cn } from \"../utils/cn\";\n\ninterface PageLayoutProps {\n children: ReactNode;\n /** Page title */\n title?: string;\n /** Page subtitle/description */\n subtitle?: string;\n /** Icon component to display next to title */\n icon?: ReactNode;\n /** Actions to display in header (buttons, etc.) */\n headerActions?: ReactNode;\n /** Additional header content below title */\n headerContent?: ReactNode;\n /** Maximum width of content: 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | 'full' */\n maxWidth?: \"sm\" | \"md\" | \"lg\" | \"xl\" | \"2xl\" | \"3xl\" | \"full\";\n /** Whether to show the header section */\n showHeader?: boolean;\n /** Additional CSS classes for the container */\n className?: string;\n /** Additional CSS classes for the content area */\n contentClassName?: string;\n}\n\nconst maxWidthClasses = {\n sm: \"max-w-screen-sm\", // 640px\n md: \"max-w-screen-md\", // 768px\n lg: \"max-w-screen-lg\", // 1024px\n xl: \"max-w-screen-xl\", // 1280px\n \"2xl\": \"max-w-screen-2xl\", // 1536px\n \"3xl\": \"max-w-[1600px]\",\n full: \"max-w-full\",\n};\n\n/**\n * PageLayout - A consistent page wrapper with responsive design\n *\n * Features:\n * - Proper max-width constraints\n * - Responsive padding\n * - Animated entrance\n * - Consistent header styling\n */\nexport function PageLayout({\n children,\n title,\n subtitle,\n icon,\n headerActions,\n headerContent,\n maxWidth = \"xl\",\n showHeader = true,\n className,\n contentClassName,\n}: PageLayoutProps) {\n return (\n <div\n className={cn(\n // Base styles - full width container with padding\n \"min-h-full w-full\",\n // Responsive horizontal padding\n \"px-4 sm:px-6 lg:px-8 xl:px-12\",\n // Responsive vertical padding\n \"py-6 sm:py-8 lg:py-10\",\n // Entrance animation\n \"animate-in fade-in-0 slide-in-from-bottom-4 duration-500\",\n className,\n )}\n >\n {/* Max-width constrained content container */}\n <div\n className={cn(\n \"mx-auto w-full\",\n maxWidthClasses[maxWidth],\n contentClassName,\n )}\n >\n {/* Page Header */}\n {showHeader && (title || headerActions) && (\n <header className=\"mb-6 sm:mb-8\">\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between\">\n {/* Title Section */}\n <div className=\"space-y-1\">\n {(title || icon) && (\n <div className=\"flex items-center gap-3\">\n {icon && (\n <div className=\"flex-shrink-0 p-2.5 rounded-xl bg-primary/10 text-primary\">\n {icon}\n </div>\n )}\n {title && (\n <h1 className=\"text-2xl sm:text-3xl font-bold tracking-tight text-foreground\">\n {title}\n </h1>\n )}\n </div>\n )}\n {subtitle && (\n <p className=\"text-sm sm:text-base text-muted-foreground max-w-2xl\">\n {subtitle}\n </p>\n )}\n </div>\n\n {/* Header Actions */}\n {headerActions && (\n <div className=\"flex items-center gap-2 flex-shrink-0\">\n {headerActions}\n </div>\n )}\n </div>\n\n {/* Additional Header Content */}\n {headerContent && <div className=\"mt-4\">{headerContent}</div>}\n </header>\n )}\n\n {/* Page content — must not use a <main> landmark here because the\n host shell (AppShellLayout) already renders one; nesting <main>\n elements is invalid HTML and an a11y violation. */}\n <div className=\"space-y-6\">{children}</div>\n </div>\n </div>\n );\n}\n\n/**\n * PageSection - A section within a page with consistent spacing\n */\nexport function PageSection({\n children,\n title,\n description,\n actions,\n className,\n}: {\n children: ReactNode;\n title?: string;\n description?: string;\n actions?: ReactNode;\n className?: string;\n}) {\n return (\n <section className={cn(\"space-y-4\", className)}>\n {(title || actions) && (\n <div className=\"flex items-center justify-between\">\n <div>\n {title && (\n <h2 className=\"text-lg font-semibold text-foreground\">{title}</h2>\n )}\n {description && (\n <p className=\"text-sm text-muted-foreground\">{description}</p>\n )}\n </div>\n {actions && <div className=\"flex items-center gap-2\">{actions}</div>}\n </div>\n )}\n {children}\n </section>\n );\n}\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":";;;AAyBA,IAAM,IAAkB;CACtB,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,OAAO;CACP,OAAO;CACP,MAAM;CACP;AAWD,SAAgB,EAAW,EACzB,aACA,UACA,aACA,SACA,kBACA,kBACA,cAAW,MACX,gBAAa,IACb,cACA,uBACkB;AAClB,QACE,kBAAC,OAAD;EACE,WAAW,EAET,qBAEA,iCAEA,yBAEA,4DACA,EACD;YAGD,kBAAC,OAAD;GACE,WAAW,EACT,kBACA,EAAgB,IAChB,EACD;aALH,CAQG,MAAe,KAAS,MACvB,kBAAC,UAAD;IAAQ,WAAU;cAAlB,CACE,kBAAC,OAAD;KAAK,WAAU;eAAf,CAEE,kBAAC,OAAD;MAAK,WAAU;gBAAf,EACI,KAAS,MACT,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACG,KACC,kBAAC,OAAD;QAAK,WAAU;kBACZ;QACG,CAAA,EAEP,KACC,kBAAC,MAAD;QAAI,WAAU;kBACX;QACE,CAAA,CAEH;UAEP,KACC,kBAAC,KAAD;OAAG,WAAU;iBACV;OACC,CAAA,CAEF;SAGL,KACC,kBAAC,OAAD;MAAK,WAAU;gBACZ;MACG,CAAA,CAEJ;QAGL,KAAiB,kBAAC,OAAD;KAAK,WAAU;eAAQ;KAAoB,CAAA,CACtD;OAMX,kBAAC,OAAD;IAAK,WAAU;IAAa;IAAe,CAAA,CACvC;;EACF,CAAA;;AAOV,SAAgB,EAAY,EAC1B,aACA,UACA,gBACA,YACA,gBAOC;AACD,QACE,kBAAC,WAAD;EAAS,WAAW,EAAG,aAAa,EAAU;YAA9C,EACI,KAAS,MACT,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,OAAD,EAAA,UAAA,CACG,KACC,kBAAC,MAAD;IAAI,WAAU;cAAyC;IAAW,CAAA,EAEnE,KACC,kBAAC,KAAD;IAAG,WAAU;cAAiC;IAAgB,CAAA,CAE5D,EAAA,CAAA,EACL,KAAW,kBAAC,OAAD;IAAK,WAAU;cAA2B;IAAc,CAAA,CAChE;MAEP,EACO;;;AAOd,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":"PageLayout.js","names":[],"sources":["../../../src/conversations/components/PageLayout.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport { cn } from \"../utils/cn\";\n\ninterface PageLayoutProps {\n children: ReactNode;\n /** Page title */\n title?: string;\n /** Page subtitle/description */\n subtitle?: string;\n /** Icon component to display next to title */\n icon?: ReactNode;\n /** Actions to display in header (buttons, etc.) */\n headerActions?: ReactNode;\n /** Additional header content below title */\n headerContent?: ReactNode;\n /** Maximum width of content: 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | 'full' */\n maxWidth?: \"sm\" | \"md\" | \"lg\" | \"xl\" | \"2xl\" | \"3xl\" | \"full\";\n /** Whether to show the header section */\n showHeader?: boolean;\n /** Additional CSS classes for the container */\n className?: string;\n /** Additional CSS classes for the content area */\n contentClassName?: string;\n /** Stable selector for guided tours and smoke tests */\n dataTour?: string;\n}\n\nconst maxWidthClasses = {\n sm: \"max-w-screen-sm\", // 640px\n md: \"max-w-screen-md\", // 768px\n lg: \"max-w-screen-lg\", // 1024px\n xl: \"max-w-screen-xl\", // 1280px\n \"2xl\": \"max-w-screen-2xl\", // 1536px\n \"3xl\": \"max-w-[1600px]\",\n full: \"max-w-full\",\n};\n\n/**\n * PageLayout - A consistent page wrapper with responsive design\n *\n * Features:\n * - Proper max-width constraints\n * - Responsive padding\n * - Animated entrance\n * - Consistent header styling\n */\nexport function PageLayout({\n children,\n title,\n subtitle,\n icon,\n headerActions,\n headerContent,\n maxWidth = \"xl\",\n showHeader = true,\n className,\n contentClassName,\n dataTour,\n}: PageLayoutProps) {\n return (\n <div\n data-tour={dataTour}\n className={cn(\n // Base styles - full width container with padding\n \"min-h-full w-full\",\n // Responsive horizontal padding\n \"px-4 sm:px-6 lg:px-8 xl:px-12\",\n // Responsive vertical padding\n \"py-6 sm:py-8 lg:py-10\",\n // Entrance animation\n \"animate-in fade-in-0 slide-in-from-bottom-4 duration-500\",\n className,\n )}\n >\n {/* Max-width constrained content container */}\n <div\n className={cn(\n \"mx-auto w-full\",\n maxWidthClasses[maxWidth],\n contentClassName,\n )}\n >\n {/* Page Header */}\n {showHeader && (title || headerActions) && (\n <header className=\"mb-6 sm:mb-8\">\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between\">\n {/* Title Section */}\n <div className=\"space-y-1\">\n {(title || icon) && (\n <div className=\"flex items-center gap-3\">\n {icon && (\n <div className=\"flex-shrink-0 p-2.5 rounded-xl bg-primary/10 text-primary\">\n {icon}\n </div>\n )}\n {title && (\n <h1 className=\"text-2xl sm:text-3xl font-bold tracking-tight text-foreground\">\n {title}\n </h1>\n )}\n </div>\n )}\n {subtitle && (\n <p className=\"text-sm sm:text-base text-muted-foreground max-w-2xl\">\n {subtitle}\n </p>\n )}\n </div>\n\n {/* Header Actions */}\n {headerActions && (\n <div className=\"flex items-center gap-2 flex-shrink-0\">\n {headerActions}\n </div>\n )}\n </div>\n\n {/* Additional Header Content */}\n {headerContent && <div className=\"mt-4\">{headerContent}</div>}\n </header>\n )}\n\n {/* Page content — must not use a <main> landmark here because the\n host shell (AppShellLayout) already renders one; nesting <main>\n elements is invalid HTML and an a11y violation. */}\n <div className=\"space-y-6\">{children}</div>\n </div>\n </div>\n );\n}\n\n/**\n * PageSection - A section within a page with consistent spacing\n */\nexport function PageSection({\n children,\n title,\n description,\n actions,\n className,\n}: {\n children: ReactNode;\n title?: string;\n description?: string;\n actions?: ReactNode;\n className?: string;\n}) {\n return (\n <section className={cn(\"space-y-4\", className)}>\n {(title || actions) && (\n <div className=\"flex items-center justify-between\">\n <div>\n {title && (\n <h2 className=\"text-lg font-semibold text-foreground\">{title}</h2>\n )}\n {description && (\n <p className=\"text-sm text-muted-foreground\">{description}</p>\n )}\n </div>\n {actions && <div className=\"flex items-center gap-2\">{actions}</div>}\n </div>\n )}\n {children}\n </section>\n );\n}\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":";;;AA2BA,IAAM,IAAkB;CACtB,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,OAAO;CACP,OAAO;CACP,MAAM;CACP;AAWD,SAAgB,EAAW,EACzB,aACA,UACA,aACA,SACA,kBACA,kBACA,cAAW,MACX,gBAAa,IACb,cACA,qBACA,eACkB;AAClB,QACE,kBAAC,OAAD;EACE,aAAW;EACX,WAAW,EAET,qBAEA,iCAEA,yBAEA,4DACA,EACD;YAGD,kBAAC,OAAD;GACE,WAAW,EACT,kBACA,EAAgB,IAChB,EACD;aALH,CAQG,MAAe,KAAS,MACvB,kBAAC,UAAD;IAAQ,WAAU;cAAlB,CACE,kBAAC,OAAD;KAAK,WAAU;eAAf,CAEE,kBAAC,OAAD;MAAK,WAAU;gBAAf,EACI,KAAS,MACT,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACG,KACC,kBAAC,OAAD;QAAK,WAAU;kBACZ;QACG,CAAA,EAEP,KACC,kBAAC,MAAD;QAAI,WAAU;kBACX;QACE,CAAA,CAEH;UAEP,KACC,kBAAC,KAAD;OAAG,WAAU;iBACV;OACC,CAAA,CAEF;SAGL,KACC,kBAAC,OAAD;MAAK,WAAU;gBACZ;MACG,CAAA,CAEJ;QAGL,KAAiB,kBAAC,OAAD;KAAK,WAAU;eAAQ;KAAoB,CAAA,CACtD;OAMX,kBAAC,OAAD;IAAK,WAAU;IAAa;IAAe,CAAA,CACvC;;EACF,CAAA;;AAOV,SAAgB,EAAY,EAC1B,aACA,UACA,gBACA,YACA,gBAOC;AACD,QACE,kBAAC,WAAD;EAAS,WAAW,EAAG,aAAa,EAAU;YAA9C,EACI,KAAS,MACT,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,OAAD,EAAA,UAAA,CACG,KACC,kBAAC,MAAD;IAAI,WAAU;cAAyC;IAAW,CAAA,EAEnE,KACC,kBAAC,KAAD;IAAG,WAAU;cAAiC;IAAgB,CAAA,CAE5D,EAAA,CAAA,EACL,KAAW,kBAAC,OAAD;IAAK,WAAU;cAA2B;IAAc,CAAA,CAChE;MAEP,EACO;;;AAOd,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"}
@@ -58,7 +58,7 @@ function g(t) {
58
58
  query: r,
59
59
  first: 25
60
60
  },
61
- skip: !n || !r
61
+ skip: !n || r.length < 2
62
62
  });
63
63
  return {
64
64
  conversations: i.data?.searchConversations.edges.map((e) => d(e.node)) ?? [],