@burdenoff/microfe-conversations 2026.523.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 (26) hide show
  1. package/dist/conversations/components/ConversationCard.d.ts.map +1 -1
  2. package/dist/conversations/components/ConversationCard.js +1 -0
  3. package/dist/conversations/components/ConversationCard.js.map +1 -1
  4. package/dist/conversations/components/MessageBubble.d.ts.map +1 -1
  5. package/dist/conversations/components/MessageBubble.js +37 -29
  6. package/dist/conversations/components/MessageBubble.js.map +1 -1
  7. package/dist/conversations/components/PageLayout.d.ts +3 -1
  8. package/dist/conversations/components/PageLayout.d.ts.map +1 -1
  9. package/dist/conversations/components/PageLayout.js +2 -1
  10. package/dist/conversations/components/PageLayout.js.map +1 -1
  11. package/dist/conversations/pages/ConversationDetailPage.d.ts.map +1 -1
  12. package/dist/conversations/pages/ConversationDetailPage.js +256 -251
  13. package/dist/conversations/pages/ConversationDetailPage.js.map +1 -1
  14. package/dist/conversations/pages/ConversationListPage.d.ts.map +1 -1
  15. package/dist/conversations/pages/ConversationListPage.js +6 -0
  16. package/dist/conversations/pages/ConversationListPage.js.map +1 -1
  17. package/dist/conversations/pages/ConversationsLandingPage.d.ts.map +1 -1
  18. package/dist/conversations/pages/ConversationsLandingPage.js +2 -0
  19. package/dist/conversations/pages/ConversationsLandingPage.js.map +1 -1
  20. package/dist/conversations/pages/ConversationsSettingsPage.d.ts.map +1 -1
  21. package/dist/conversations/pages/ConversationsSettingsPage.js +1 -0
  22. package/dist/conversations/pages/ConversationsSettingsPage.js.map +1 -1
  23. package/dist/conversations/pages/NewConversationPage.d.ts.map +1 -1
  24. package/dist/conversations/pages/NewConversationPage.js +4 -0
  25. package/dist/conversations/pages/NewConversationPage.js.map +1 -1
  26. package/package.json +1 -1
@@ -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,6 +35,7 @@ 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"),
@@ -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 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
+ {"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 +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,0DAwSxB,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"}
@@ -36,26 +36,28 @@ function y(e) {
36
36
  return e.startsWith("image/");
37
37
  }
38
38
  var b = t(function({ message: t, isOwnMessage: r = !1, showAvatar: i = !0, showSender: b = !0, onReply: C, onReact: w, onEdit: T, onDelete: E, onTogglePin: D }) {
39
- let [O, k] = n(!1), [A, j] = n(!1), { t: M } = g(), N = (e, t) => {
40
- let n = M(e);
39
+ let [O, k] = n(!1), [A, j] = n(!1), [M, N] = n(!1), { t: P } = g(), F = (e, t) => {
40
+ let n = P(e);
41
41
  return n === e ? t : n;
42
- }, { sender: P, content: F, type: I, status: L, reactions: R, attachments: z, isEdited: B, createdAt: V, isPinned: H } = t;
43
- return I === "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
- children: F
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
53
  children: [
52
54
  i ? /* @__PURE__ */ m("div", {
53
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"),
54
- children: P.avatarUrl ? /* @__PURE__ */ m("img", {
55
- src: P.avatarUrl,
56
- alt: P.name,
56
+ children: I.avatarUrl ? /* @__PURE__ */ m("img", {
57
+ src: I.avatarUrl,
58
+ alt: I.name,
57
59
  className: "w-full h-full rounded-full object-cover"
58
- }) : P.name.split(" ").map((e) => e[0]).join("").toUpperCase().slice(0, 2)
60
+ }) : I.name.split(" ").map((e) => e[0]).join("").toUpperCase().slice(0, 2)
59
61
  }) : /* @__PURE__ */ m("div", { className: "w-8 flex-shrink-0" }),
60
62
  /* @__PURE__ */ h("div", {
61
63
  className: e("flex flex-col max-w-[85%] sm:max-w-[70%]", r ? "items-end" : "items-start"),
@@ -64,8 +66,8 @@ var b = t(function({ message: t, isOwnMessage: r = !1, showAvatar: i = !0, showS
64
66
  className: "flex items-center gap-1.5 mb-0.5",
65
67
  children: [/* @__PURE__ */ m("span", {
66
68
  className: "text-xs font-semibold text-foreground",
67
- children: P.name
68
- }), H && /* @__PURE__ */ m(l, {
69
+ children: I.name
70
+ }), W && /* @__PURE__ */ m(l, {
69
71
  size: 10,
70
72
  className: "text-status-warning-text"
71
73
  })]
@@ -74,10 +76,10 @@ var b = t(function({ message: t, isOwnMessage: r = !1, showAvatar: i = !0, showS
74
76
  className: e("relative rounded-2xl px-3.5 py-2 text-sm leading-relaxed", r ? "bg-primary text-primary-foreground rounded-tr-md" : "bg-muted/70 text-foreground rounded-tl-md"),
75
77
  children: [/* @__PURE__ */ m("p", {
76
78
  className: "whitespace-pre-wrap break-words",
77
- children: F
78
- }), z.length > 0 && /* @__PURE__ */ m("div", {
79
+ children: L
80
+ }), V.length > 0 && /* @__PURE__ */ m("div", {
79
81
  className: "mt-2 grid gap-2",
80
- children: z.map((t) => {
82
+ children: V.map((t) => {
81
83
  let n = e("flex min-w-0 items-center gap-2 rounded-lg px-2 py-1.5 text-xs transition-colors", r ? "bg-primary-foreground/10 text-primary-foreground hover:bg-primary-foreground/15" : "bg-background/50 text-foreground hover:bg-background/70"), i = /* @__PURE__ */ h(p, { children: [/* @__PURE__ */ m("div", {
82
84
  className: "flex size-10 flex-shrink-0 items-center justify-center overflow-hidden rounded-md bg-background/40",
83
85
  children: t.thumbnail && y(t.mimeType) ? /* @__PURE__ */ m("img", {
@@ -114,9 +116,9 @@ var b = t(function({ message: t, isOwnMessage: r = !1, showAvatar: i = !0, showS
114
116
  })
115
117
  })]
116
118
  }),
117
- R.length > 0 && /* @__PURE__ */ m("div", {
119
+ B.length > 0 && /* @__PURE__ */ m("div", {
118
120
  className: "flex flex-wrap gap-1 mt-1",
119
- children: R.map((n) => /* @__PURE__ */ h("button", {
121
+ children: B.map((n) => /* @__PURE__ */ h("button", {
120
122
  type: "button",
121
123
  onClick: () => w?.(t.id, n.emoji),
122
124
  className: e("inline-flex items-center gap-1 px-1.5 py-0.5 rounded-full text-xs", "border transition-colors", n.currentUserReacted ? "bg-primary/10 border-primary/30 text-primary" : "bg-muted/50 border-border text-muted-foreground hover:bg-muted"),
@@ -131,26 +133,32 @@ var b = t(function({ message: t, isOwnMessage: r = !1, showAvatar: i = !0, showS
131
133
  children: [
132
134
  /* @__PURE__ */ m("span", {
133
135
  className: "text-[10px] text-muted-foreground",
134
- children: _(V)
136
+ children: _(U)
135
137
  }),
136
- B && /* @__PURE__ */ m("span", {
138
+ H && /* @__PURE__ */ m("span", {
137
139
  className: "text-[10px] text-muted-foreground italic",
138
- children: N("conversations.edited", "edited")
140
+ children: F("conversations.edited", "edited")
139
141
  }),
140
142
  r && /* @__PURE__ */ m(v, {
141
- status: L,
142
- failedLabel: N("conversations.failed", "Failed")
143
+ status: z,
144
+ failedLabel: F("conversations.failed", "Failed")
143
145
  })
144
146
  ]
145
147
  })
146
148
  ]
147
149
  }),
148
150
  /* @__PURE__ */ h("div", {
149
- className: e("relative mt-1 flex items-center gap-0.5 self-start opacity-100 transition-opacity duration-150 sm:opacity-0 sm:group-focus-within:opacity-100 sm:group-hover:opacity-100", (O || A) && "sm:opacity-100"),
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"),
150
158
  children: [
151
159
  t.permissions.canReact && /* @__PURE__ */ m(x, {
152
160
  icon: /* @__PURE__ */ m(d, { size: 14 }),
153
- label: N("conversations.react", "React"),
161
+ label: F("conversations.react", "React"),
154
162
  pressed: O,
155
163
  onClick: () => {
156
164
  k((e) => !e), j(!1);
@@ -158,22 +166,22 @@ var b = t(function({ message: t, isOwnMessage: r = !1, showAvatar: i = !0, showS
158
166
  }),
159
167
  t.permissions.canReply && /* @__PURE__ */ m(x, {
160
168
  icon: /* @__PURE__ */ m(u, { size: 14 }),
161
- label: N("conversations.reply", "Reply"),
169
+ label: F("conversations.reply", "Reply"),
162
170
  onClick: () => C?.(t)
163
171
  }),
164
172
  t.permissions.canEdit && /* @__PURE__ */ m(x, {
165
173
  icon: /* @__PURE__ */ m(c, { size: 14 }),
166
- label: N("conversations.edit", "Edit"),
174
+ label: F("conversations.edit", "Edit"),
167
175
  onClick: () => T?.(t)
168
176
  }),
169
177
  t.permissions.canDelete && /* @__PURE__ */ m(x, {
170
178
  icon: /* @__PURE__ */ m(f, { size: 14 }),
171
- label: N("conversations.delete", "Delete"),
179
+ label: F("conversations.delete", "Delete"),
172
180
  onClick: () => E?.(t.id)
173
181
  }),
174
182
  /* @__PURE__ */ m(x, {
175
183
  icon: /* @__PURE__ */ m(s, { size: 14 }),
176
- label: N("conversations.more", "More"),
184
+ label: F("conversations.more", "More"),
177
185
  pressed: A,
178
186
  onClick: () => {
179
187
  j((e) => !e), k(!1);
@@ -193,7 +201,7 @@ var b = t(function({ message: t, isOwnMessage: r = !1, showAvatar: i = !0, showS
193
201
  w?.(t.id, e), k(!1);
194
202
  },
195
203
  className: "rounded-md px-2 py-1 text-sm hover:bg-muted/80",
196
- "aria-label": `${N("conversations.reactWith", "React with")} ${e}`,
204
+ "aria-label": `${F("conversations.reactWith", "React with")} ${e}`,
197
205
  children: e
198
206
  }, e))
199
207
  }),
@@ -205,7 +213,7 @@ var b = t(function({ message: t, isOwnMessage: r = !1, showAvatar: i = !0, showS
205
213
  D(t.id, t.isPinned), j(!1);
206
214
  },
207
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",
208
- children: [/* @__PURE__ */ m(l, { size: 14 }), t.isPinned ? N("conversations.unpinMessage", "Unpin message") : N("conversations.pinMessage", "Pin message")]
216
+ children: [/* @__PURE__ */ m(l, { size: 14 }), t.isPinned ? F("conversations.unpinMessage", "Unpin message") : F("conversations.pinMessage", "Pin message")]
209
217
  })
210
218
  })
211
219
  ]
@@ -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-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 { 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 >\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 className={cn(\n \"relative mt-1 flex items-center gap-0.5 self-start opacity-100 transition-opacity duration-150 sm:opacity-0 sm:group-focus-within:opacity-100 sm:group-hover:opacity-100\",\n (showReactionPicker || showMoreMenu) && \"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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"}
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"}
@@ -1 +1 @@
1
- {"version":3,"file":"ConversationDetailPage.d.ts","sourceRoot":"","sources":["../../../src/conversations/pages/ConversationDetailPage.tsx"],"names":[],"mappings":"AA0CA,wBAAgB,sBAAsB,4CA+pBrC"}
1
+ {"version":3,"file":"ConversationDetailPage.d.ts","sourceRoot":"","sources":["../../../src/conversations/pages/ConversationDetailPage.tsx"],"names":[],"mappings":"AA0CA,wBAAgB,sBAAsB,4CAuqBrC"}