@burdenoff/microfe-conversations 2026.928.1 → 2026.1006.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -66,7 +66,7 @@ var j = r(function({ conversation: r, isActive: l = !1, onClick: d, onLeave: p }
66
66
  })
67
67
  }),
68
68
  /* @__PURE__ */ S("div", {
69
- className: "flex min-w-0 flex-1 flex-col gap-0.5",
69
+ className: "flex min-w-0 flex-1 flex-col gap-0.5 pr-9",
70
70
  children: [/* @__PURE__ */ S("div", {
71
71
  className: "flex items-center justify-between gap-2",
72
72
  children: [/* @__PURE__ */ S("div", {
@@ -1 +1 @@
1
- {"version":3,"file":"ConversationRow.js","names":[],"sources":["../../../src/conversations/components/ConversationRow.tsx"],"sourcesContent":["import { memo, useState } from \"react\";\nimport {\n ArchiveIcon,\n ArchiveRestore,\n Bell,\n BellOff,\n Bot,\n Hash,\n HeadphonesIcon,\n LogOut,\n Megaphone,\n MessageCircle,\n MoreHorizontal,\n Pin,\n PinOff,\n Users,\n VolumeX,\n} from \"lucide-react\";\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n} from \"@burdenoff/fe-libs/ui\";\nimport { cn } from \"../utils/cn\";\nimport { useTr } from \"../utils/i18n\";\nimport { useConversationActions } from \"../hooks/useConversationQueries\";\nimport type { Conversation } from \"../types\";\n\nexport interface ConversationRowProps {\n conversation: Conversation;\n isActive?: boolean;\n onClick?: (conversation: Conversation) => void;\n /** Optional leave handler — only the host has the membership mutation. */\n onLeave?: (conversation: Conversation) => void;\n}\n\nfunction ConversationTypeIcon({\n type,\n size = 16,\n className,\n}: {\n type: Conversation[\"type\"];\n size?: number;\n className?: string;\n}) {\n const props = { size, className };\n switch (type) {\n case \"DIRECT\":\n return <MessageCircle {...props} />;\n case \"GROUP\":\n return <Users {...props} />;\n case \"CHANNEL\":\n return <Hash {...props} />;\n case \"BROADCAST\":\n case \"ANNOUNCEMENT\":\n return <Megaphone {...props} />;\n case \"BOT\":\n return <Bot {...props} />;\n case \"SUPPORT\":\n return <HeadphonesIcon {...props} />;\n default:\n return <MessageCircle {...props} />;\n }\n}\n\nfunction formatRelativeTime(\n dateString: string,\n labels: {\n now: string;\n yesterday: string;\n minutesAgo: string;\n hoursAgo: string;\n daysAgo: string;\n },\n): string {\n const now = new Date();\n const date = new Date(dateString);\n const diffMs = now.getTime() - date.getTime();\n const diffMin = Math.floor(diffMs / 1000 / 60);\n const diffHr = Math.floor(diffMin / 60);\n const diffDay = Math.floor(diffHr / 24);\n\n if (diffMin < 1) return labels.now;\n if (diffMin < 60)\n return labels.minutesAgo.replace(\"{{count}}\", String(diffMin));\n if (diffHr < 24) return labels.hoursAgo.replace(\"{{count}}\", String(diffHr));\n if (diffDay === 1) return labels.yesterday;\n if (diffDay < 7) return labels.daysAgo.replace(\"{{count}}\", String(diffDay));\n return date.toLocaleDateString(undefined, { month: \"short\", day: \"numeric\" });\n}\n\nfunction getInitials(title: string): string {\n return title\n .split(\" \")\n .slice(0, 2)\n .map((word) => word[0])\n .join(\"\")\n .toUpperCase();\n}\n\n/**\n * ConversationRow — inbox-style list row.\n *\n * Visual hierarchy:\n * [avatar] Title · 2h\n * last message preview (badge)\n *\n * On hover the kebab (3-dots) menu reveals so the user can pin/mute/archive\n * without opening the conversation first. Wires straight into\n * `useConversationActions` for archive/unarchive/pin/mute.\n */\nexport const ConversationRow = memo(function ConversationRow({\n conversation,\n isActive = false,\n onClick,\n onLeave,\n}: ConversationRowProps) {\n const {\n title,\n type,\n state,\n lastMessage,\n unreadCount,\n isPinned,\n isMuted,\n memberCount,\n updatedAt,\n avatar,\n } = conversation;\n\n const hasUnread = unreadCount > 0;\n const isArchived = state === \"ARCHIVED\";\n const tr = useTr();\n const timeLabels = {\n now: tr(\"conversations.now\", \"now\"),\n yesterday: tr(\"conversations.yesterday\", \"Yesterday\"),\n minutesAgo: tr(\"conversations.time.minutesAgo\", \"{{count}}m\"),\n hoursAgo: tr(\"conversations.time.hoursAgo\", \"{{count}}h\"),\n daysAgo: tr(\"conversations.time.daysAgo\", \"{{count}}d\"),\n };\n\n // Track menu open so the kebab stays visible even when the cursor leaves\n // the row (otherwise the radix portal closes on every hover-out).\n const [menuOpen, setMenuOpen] = useState(false);\n\n const {\n archiveConversation,\n unarchiveConversation,\n pinConversation,\n unpinConversation,\n muteConversation,\n unmuteConversation,\n } = useConversationActions();\n\n // Stop click bubbling from the kebab into the row's click handler — opening\n // the menu must not also open the conversation.\n const stop = (event: React.MouseEvent | React.KeyboardEvent) => {\n event.stopPropagation();\n };\n\n return (\n <div\n data-tour=\"conversation-row\"\n className={cn(\n \"group relative flex w-full min-w-0 items-start gap-3 rounded-md px-3 py-2.5 transition-colors\",\n \"cursor-pointer select-none\",\n isActive\n ? \"bg-bg-sunken border-l-2 border-l-action-primary-bg\"\n : \"border-l-2 border-l-transparent hover:bg-action-ghost-bg-hover\",\n hasUnread && !isActive && \"bg-bg-elevated/40\",\n )}\n role=\"button\"\n tabIndex={0}\n onClick={() => onClick?.(conversation)}\n onKeyDown={(event) => {\n if (event.key === \"Enter\" || event.key === \" \") {\n event.preventDefault();\n onClick?.(conversation);\n }\n }}\n aria-label={\n tr(\"conversations.openConversation\", \"Open conversation\") + \": \" + title\n }\n >\n {/* Avatar / type icon */}\n <div\n className={cn(\n \"flex-shrink-0 flex h-9 w-9 items-center justify-center rounded-full text-xs font-semibold overflow-hidden\",\n type === \"DIRECT\" && \"bg-status-info-bg-subtle text-status-info-text\",\n type === \"CHANNEL\" &&\n \"bg-status-success-bg-subtle text-status-success-text\",\n (type === \"GROUP\" ||\n type === \"BROADCAST\" ||\n type === \"ANNOUNCEMENT\" ||\n type === \"BOT\" ||\n type === \"SUPPORT\") &&\n \"bg-bg-sunken text-text-primary\",\n )}\n aria-hidden=\"true\"\n >\n {avatar ? (\n <img src={avatar} alt=\"\" className=\"h-full w-full object-cover\" />\n ) : type === \"DIRECT\" ? (\n getInitials(title)\n ) : type === \"CHANNEL\" ? (\n <Hash size={16} />\n ) : (\n <ConversationTypeIcon type={type} size={16} />\n )}\n </div>\n\n {/* Body */}\n <div className=\"flex min-w-0 flex-1 flex-col gap-0.5\">\n <div className=\"flex items-center justify-between gap-2\">\n <div className=\"flex min-w-0 items-center gap-1.5\">\n <span\n className={cn(\n \"truncate text-sm\",\n hasUnread\n ? \"font-semibold text-text-primary\"\n : \"font-medium text-text-primary\",\n )}\n >\n {title}\n </span>\n {isPinned && (\n <Pin\n size={11}\n className=\"flex-shrink-0 text-text-secondary\"\n aria-label={tr(\"conversations.pinnedLabel\", \"Pinned\")}\n />\n )}\n {isMuted && (\n <VolumeX\n size={11}\n className=\"flex-shrink-0 text-text-secondary\"\n aria-label={tr(\"conversations.mutedLabel\", \"Muted\")}\n />\n )}\n </div>\n\n <span\n className={cn(\n \"flex-shrink-0 text-[11px]\",\n hasUnread\n ? \"font-medium text-action-primary-bg\"\n : \"text-text-secondary\",\n )}\n >\n {formatRelativeTime(\n lastMessage?.createdAt ?? updatedAt,\n timeLabels,\n )}\n </span>\n </div>\n\n <div className=\"flex items-center justify-between gap-2\">\n <p\n className={cn(\n \"min-w-0 truncate break-all text-xs\",\n hasUnread ? \"text-text-primary/80\" : \"text-text-secondary\",\n )}\n >\n {lastMessage ? (\n <>\n {lastMessage.sender.name && (\n <span className=\"font-medium\">\n {lastMessage.sender.name}\n {\": \"}\n </span>\n )}\n {lastMessage.content ||\n tr(\"conversations.sentAttachment\", \"Sent an attachment\")}\n </>\n ) : (\n <span className=\"italic\">\n {tr(\"conversations.noMessages\", \"No messages yet\")}\n </span>\n )}\n </p>\n\n <div className=\"flex flex-shrink-0 items-center gap-1.5\">\n {type !== \"DIRECT\" && (\n <span\n className=\"inline-flex items-center gap-0.5 text-[10px] text-text-secondary\"\n aria-label={`${memberCount} ${tr(\"conversations.membersLabel\", \"members\")}`}\n >\n <Users size={10} aria-hidden=\"true\" />\n {memberCount}\n </span>\n )}\n {hasUnread && (\n <span\n className=\"inline-flex h-[18px] min-w-[18px] items-center justify-center rounded-full bg-action-primary-bg px-1 text-[10px] font-bold text-action-primary-text motion-safe:animate-vc-glow-pulse\"\n aria-label={`${unreadCount} ${tr(\"conversations.unreadCountLabel\", \"unread\")}`}\n >\n {unreadCount > 99 ? \"99+\" : unreadCount}\n </span>\n )}\n </div>\n </div>\n </div>\n\n {/* Kebab menu — hidden until hover or menu open */}\n <DropdownMenu open={menuOpen} onOpenChange={setMenuOpen}>\n <DropdownMenuTrigger asChild>\n <button\n type=\"button\"\n onClick={stop}\n onKeyDown={stop}\n aria-label={tr(\n \"conversations.rowMenuLabel\",\n \"Conversation actions\",\n )}\n className={cn(\n \"absolute right-2 top-2 flex h-7 w-7 items-center justify-center rounded-md transition-opacity\",\n \"text-text-secondary hover:bg-action-ghost-bg-hover hover:text-text-primary\",\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-action-primary-border\",\n menuOpen\n ? \"opacity-100\"\n : \"opacity-0 group-hover:opacity-100 group-focus-within:opacity-100\",\n )}\n >\n <MoreHorizontal size={14} aria-hidden=\"true\" />\n </button>\n </DropdownMenuTrigger>\n <DropdownMenuContent\n align=\"end\"\n className=\"min-w-[180px]\"\n onClick={stop}\n >\n <DropdownMenuItem\n onSelect={() => {\n void (isPinned\n ? unpinConversation(conversation.id)\n : pinConversation(conversation.id));\n }}\n >\n {isPinned ? (\n <>\n <PinOff size={14} />\n {tr(\"conversations.unpin\", \"Unpin\")}\n </>\n ) : (\n <>\n <Pin size={14} />\n {tr(\"conversations.pin\", \"Pin\")}\n </>\n )}\n </DropdownMenuItem>\n <DropdownMenuItem\n onSelect={() => {\n void (isMuted\n ? unmuteConversation(conversation.id)\n : muteConversation(conversation.id));\n }}\n >\n {isMuted ? (\n <>\n <Bell size={14} />\n {tr(\"conversations.unmute\", \"Unmute\")}\n </>\n ) : (\n <>\n <BellOff size={14} />\n {tr(\"conversations.mute\", \"Mute\")}\n </>\n )}\n </DropdownMenuItem>\n <DropdownMenuSeparator />\n <DropdownMenuItem\n onSelect={() => {\n void (isArchived\n ? unarchiveConversation(conversation.id)\n : archiveConversation(conversation.id));\n }}\n >\n {isArchived ? (\n <>\n <ArchiveRestore size={14} />\n {tr(\"conversations.unarchive\", \"Unarchive\")}\n </>\n ) : (\n <>\n <ArchiveIcon size={14} />\n {tr(\"conversations.archive\", \"Archive\")}\n </>\n )}\n </DropdownMenuItem>\n {(type === \"GROUP\" || type === \"CHANNEL\") && onLeave && (\n <>\n <DropdownMenuSeparator />\n <DropdownMenuItem\n onSelect={() => onLeave(conversation)}\n className=\"text-status-error-text focus:text-status-error-text\"\n >\n <LogOut size={14} />\n {tr(\"conversations.leave\", \"Leave\")}\n </DropdownMenuItem>\n </>\n )}\n </DropdownMenuContent>\n </DropdownMenu>\n </div>\n );\n});\n"],"mappings":"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"}
1
+ {"version":3,"file":"ConversationRow.js","names":[],"sources":["../../../src/conversations/components/ConversationRow.tsx"],"sourcesContent":["import { memo, useState } from \"react\";\nimport {\n ArchiveIcon,\n ArchiveRestore,\n Bell,\n BellOff,\n Bot,\n Hash,\n HeadphonesIcon,\n LogOut,\n Megaphone,\n MessageCircle,\n MoreHorizontal,\n Pin,\n PinOff,\n Users,\n VolumeX,\n} from \"lucide-react\";\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n} from \"@burdenoff/fe-libs/ui\";\nimport { cn } from \"../utils/cn\";\nimport { useTr } from \"../utils/i18n\";\nimport { useConversationActions } from \"../hooks/useConversationQueries\";\nimport type { Conversation } from \"../types\";\n\nexport interface ConversationRowProps {\n conversation: Conversation;\n isActive?: boolean;\n onClick?: (conversation: Conversation) => void;\n /** Optional leave handler — only the host has the membership mutation. */\n onLeave?: (conversation: Conversation) => void;\n}\n\nfunction ConversationTypeIcon({\n type,\n size = 16,\n className,\n}: {\n type: Conversation[\"type\"];\n size?: number;\n className?: string;\n}) {\n const props = { size, className };\n switch (type) {\n case \"DIRECT\":\n return <MessageCircle {...props} />;\n case \"GROUP\":\n return <Users {...props} />;\n case \"CHANNEL\":\n return <Hash {...props} />;\n case \"BROADCAST\":\n case \"ANNOUNCEMENT\":\n return <Megaphone {...props} />;\n case \"BOT\":\n return <Bot {...props} />;\n case \"SUPPORT\":\n return <HeadphonesIcon {...props} />;\n default:\n return <MessageCircle {...props} />;\n }\n}\n\nfunction formatRelativeTime(\n dateString: string,\n labels: {\n now: string;\n yesterday: string;\n minutesAgo: string;\n hoursAgo: string;\n daysAgo: string;\n },\n): string {\n const now = new Date();\n const date = new Date(dateString);\n const diffMs = now.getTime() - date.getTime();\n const diffMin = Math.floor(diffMs / 1000 / 60);\n const diffHr = Math.floor(diffMin / 60);\n const diffDay = Math.floor(diffHr / 24);\n\n if (diffMin < 1) return labels.now;\n if (diffMin < 60)\n return labels.minutesAgo.replace(\"{{count}}\", String(diffMin));\n if (diffHr < 24) return labels.hoursAgo.replace(\"{{count}}\", String(diffHr));\n if (diffDay === 1) return labels.yesterday;\n if (diffDay < 7) return labels.daysAgo.replace(\"{{count}}\", String(diffDay));\n return date.toLocaleDateString(undefined, { month: \"short\", day: \"numeric\" });\n}\n\nfunction getInitials(title: string): string {\n return title\n .split(\" \")\n .slice(0, 2)\n .map((word) => word[0])\n .join(\"\")\n .toUpperCase();\n}\n\n/**\n * ConversationRow — inbox-style list row.\n *\n * Visual hierarchy:\n * [avatar] Title · 2h\n * last message preview (badge)\n *\n * On hover the kebab (3-dots) menu reveals so the user can pin/mute/archive\n * without opening the conversation first. Wires straight into\n * `useConversationActions` for archive/unarchive/pin/mute.\n */\nexport const ConversationRow = memo(function ConversationRow({\n conversation,\n isActive = false,\n onClick,\n onLeave,\n}: ConversationRowProps) {\n const {\n title,\n type,\n state,\n lastMessage,\n unreadCount,\n isPinned,\n isMuted,\n memberCount,\n updatedAt,\n avatar,\n } = conversation;\n\n const hasUnread = unreadCount > 0;\n const isArchived = state === \"ARCHIVED\";\n const tr = useTr();\n const timeLabels = {\n now: tr(\"conversations.now\", \"now\"),\n yesterday: tr(\"conversations.yesterday\", \"Yesterday\"),\n minutesAgo: tr(\"conversations.time.minutesAgo\", \"{{count}}m\"),\n hoursAgo: tr(\"conversations.time.hoursAgo\", \"{{count}}h\"),\n daysAgo: tr(\"conversations.time.daysAgo\", \"{{count}}d\"),\n };\n\n // Track menu open so the kebab stays visible even when the cursor leaves\n // the row (otherwise the radix portal closes on every hover-out).\n const [menuOpen, setMenuOpen] = useState(false);\n\n const {\n archiveConversation,\n unarchiveConversation,\n pinConversation,\n unpinConversation,\n muteConversation,\n unmuteConversation,\n } = useConversationActions();\n\n // Stop click bubbling from the kebab into the row's click handler — opening\n // the menu must not also open the conversation.\n const stop = (event: React.MouseEvent | React.KeyboardEvent) => {\n event.stopPropagation();\n };\n\n return (\n <div\n data-tour=\"conversation-row\"\n className={cn(\n \"group relative flex w-full min-w-0 items-start gap-3 rounded-md px-3 py-2.5 transition-colors\",\n \"cursor-pointer select-none\",\n isActive\n ? \"bg-bg-sunken border-l-2 border-l-action-primary-bg\"\n : \"border-l-2 border-l-transparent hover:bg-action-ghost-bg-hover\",\n hasUnread && !isActive && \"bg-bg-elevated/40\",\n )}\n role=\"button\"\n tabIndex={0}\n onClick={() => onClick?.(conversation)}\n onKeyDown={(event) => {\n if (event.key === \"Enter\" || event.key === \" \") {\n event.preventDefault();\n onClick?.(conversation);\n }\n }}\n aria-label={\n tr(\"conversations.openConversation\", \"Open conversation\") + \": \" + title\n }\n >\n {/* Avatar / type icon */}\n <div\n className={cn(\n \"flex-shrink-0 flex h-9 w-9 items-center justify-center rounded-full text-xs font-semibold overflow-hidden\",\n type === \"DIRECT\" && \"bg-status-info-bg-subtle text-status-info-text\",\n type === \"CHANNEL\" &&\n \"bg-status-success-bg-subtle text-status-success-text\",\n (type === \"GROUP\" ||\n type === \"BROADCAST\" ||\n type === \"ANNOUNCEMENT\" ||\n type === \"BOT\" ||\n type === \"SUPPORT\") &&\n \"bg-bg-sunken text-text-primary\",\n )}\n aria-hidden=\"true\"\n >\n {avatar ? (\n <img src={avatar} alt=\"\" className=\"h-full w-full object-cover\" />\n ) : type === \"DIRECT\" ? (\n getInitials(title)\n ) : type === \"CHANNEL\" ? (\n <Hash size={16} />\n ) : (\n <ConversationTypeIcon type={type} size={16} />\n )}\n </div>\n\n {/* Body */}\n {/*\n * ★ `pr-9` (36 px) reserves the kebab's footprint up front so the\n * timestamp and unread/member badges never sit under the absolutely\n * positioned 28 px kebab (right-2 top-2 h-7 w-7). The row's two\n * rows are `flex justify-between`; without this padding the kebab\n * collides with the timestamp on hover / focus / menu-open, and a\n * long title plus a wide timestamp would let the timestamp and the\n * kebab overlap the preview row's unread count.\n *\n * `pr-8` would align with the kebab's right edge (28 + 8 = 36 px\n * matches the kebab's outer footprint). We use `pr-9` so the\n * timestamp sits a full kebab-width to the left of the row's right\n * edge — even if the kebab grows or the row gains a right-side\n * adornment later — and so the badge in the preview row never\n * pushes into the kebab area at narrow widths.\n */}\n <div className=\"flex min-w-0 flex-1 flex-col gap-0.5 pr-9\">\n <div className=\"flex items-center justify-between gap-2\">\n <div className=\"flex min-w-0 items-center gap-1.5\">\n <span\n className={cn(\n \"truncate text-sm\",\n hasUnread\n ? \"font-semibold text-text-primary\"\n : \"font-medium text-text-primary\",\n )}\n >\n {title}\n </span>\n {isPinned && (\n <Pin\n size={11}\n className=\"flex-shrink-0 text-text-secondary\"\n aria-label={tr(\"conversations.pinnedLabel\", \"Pinned\")}\n />\n )}\n {isMuted && (\n <VolumeX\n size={11}\n className=\"flex-shrink-0 text-text-secondary\"\n aria-label={tr(\"conversations.mutedLabel\", \"Muted\")}\n />\n )}\n </div>\n\n <span\n className={cn(\n \"flex-shrink-0 text-[11px]\",\n hasUnread\n ? \"font-medium text-action-primary-bg\"\n : \"text-text-secondary\",\n )}\n >\n {formatRelativeTime(\n lastMessage?.createdAt ?? updatedAt,\n timeLabels,\n )}\n </span>\n </div>\n\n <div className=\"flex items-center justify-between gap-2\">\n <p\n className={cn(\n \"min-w-0 truncate break-all text-xs\",\n hasUnread ? \"text-text-primary/80\" : \"text-text-secondary\",\n )}\n >\n {lastMessage ? (\n <>\n {lastMessage.sender.name && (\n <span className=\"font-medium\">\n {lastMessage.sender.name}\n {\": \"}\n </span>\n )}\n {lastMessage.content ||\n tr(\"conversations.sentAttachment\", \"Sent an attachment\")}\n </>\n ) : (\n <span className=\"italic\">\n {tr(\"conversations.noMessages\", \"No messages yet\")}\n </span>\n )}\n </p>\n\n <div className=\"flex flex-shrink-0 items-center gap-1.5\">\n {type !== \"DIRECT\" && (\n <span\n className=\"inline-flex items-center gap-0.5 text-[10px] text-text-secondary\"\n aria-label={`${memberCount} ${tr(\"conversations.membersLabel\", \"members\")}`}\n >\n <Users size={10} aria-hidden=\"true\" />\n {memberCount}\n </span>\n )}\n {hasUnread && (\n <span\n className=\"inline-flex h-[18px] min-w-[18px] items-center justify-center rounded-full bg-action-primary-bg px-1 text-[10px] font-bold text-action-primary-text motion-safe:animate-vc-glow-pulse\"\n aria-label={`${unreadCount} ${tr(\"conversations.unreadCountLabel\", \"unread\")}`}\n >\n {unreadCount > 99 ? \"99+\" : unreadCount}\n </span>\n )}\n </div>\n </div>\n </div>\n\n {/* Kebab menu — hidden until hover or menu open */}\n <DropdownMenu open={menuOpen} onOpenChange={setMenuOpen}>\n <DropdownMenuTrigger asChild>\n <button\n type=\"button\"\n onClick={stop}\n onKeyDown={stop}\n aria-label={tr(\n \"conversations.rowMenuLabel\",\n \"Conversation actions\",\n )}\n className={cn(\n \"absolute right-2 top-2 flex h-7 w-7 items-center justify-center rounded-md transition-opacity\",\n \"text-text-secondary hover:bg-action-ghost-bg-hover hover:text-text-primary\",\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-action-primary-border\",\n menuOpen\n ? \"opacity-100\"\n : \"opacity-0 group-hover:opacity-100 group-focus-within:opacity-100\",\n )}\n >\n <MoreHorizontal size={14} aria-hidden=\"true\" />\n </button>\n </DropdownMenuTrigger>\n <DropdownMenuContent\n align=\"end\"\n className=\"min-w-[180px]\"\n onClick={stop}\n >\n <DropdownMenuItem\n onSelect={() => {\n void (isPinned\n ? unpinConversation(conversation.id)\n : pinConversation(conversation.id));\n }}\n >\n {isPinned ? (\n <>\n <PinOff size={14} />\n {tr(\"conversations.unpin\", \"Unpin\")}\n </>\n ) : (\n <>\n <Pin size={14} />\n {tr(\"conversations.pin\", \"Pin\")}\n </>\n )}\n </DropdownMenuItem>\n <DropdownMenuItem\n onSelect={() => {\n void (isMuted\n ? unmuteConversation(conversation.id)\n : muteConversation(conversation.id));\n }}\n >\n {isMuted ? (\n <>\n <Bell size={14} />\n {tr(\"conversations.unmute\", \"Unmute\")}\n </>\n ) : (\n <>\n <BellOff size={14} />\n {tr(\"conversations.mute\", \"Mute\")}\n </>\n )}\n </DropdownMenuItem>\n <DropdownMenuSeparator />\n <DropdownMenuItem\n onSelect={() => {\n void (isArchived\n ? unarchiveConversation(conversation.id)\n : archiveConversation(conversation.id));\n }}\n >\n {isArchived ? (\n <>\n <ArchiveRestore size={14} />\n {tr(\"conversations.unarchive\", \"Unarchive\")}\n </>\n ) : (\n <>\n <ArchiveIcon size={14} />\n {tr(\"conversations.archive\", \"Archive\")}\n </>\n )}\n </DropdownMenuItem>\n {(type === \"GROUP\" || type === \"CHANNEL\") && onLeave && (\n <>\n <DropdownMenuSeparator />\n <DropdownMenuItem\n onSelect={() => onLeave(conversation)}\n className=\"text-status-error-text focus:text-status-error-text\"\n >\n <LogOut size={14} />\n {tr(\"conversations.leave\", \"Leave\")}\n </DropdownMenuItem>\n </>\n )}\n </DropdownMenuContent>\n </DropdownMenu>\n </div>\n );\n});\n"],"mappings":";;;;;;;;AAsCA,SAAS,EAAqB,EAC5B,SACA,UAAO,IACP,gBAKC;CACD,IAAM,IAAQ;EAAE;EAAM;EAAW;AACjC,SAAQ,GAAR;EACE,KAAK,SACH,QAAO,kBAAC,GAAD,EAAe,GAAI,GAAS,CAAA;EACrC,KAAK,QACH,QAAO,kBAAC,GAAD,EAAO,GAAI,GAAS,CAAA;EAC7B,KAAK,UACH,QAAO,kBAAC,GAAD,EAAM,GAAI,GAAS,CAAA;EAC5B,KAAK;EACL,KAAK,eACH,QAAO,kBAAC,GAAD,EAAW,GAAI,GAAS,CAAA;EACjC,KAAK,MACH,QAAO,kBAAC,GAAD,EAAK,GAAI,GAAS,CAAA;EAC3B,KAAK,UACH,QAAO,kBAAC,GAAD,EAAgB,GAAI,GAAS,CAAA;EACtC,QACE,QAAO,kBAAC,GAAD,EAAe,GAAI,GAAS,CAAA;;;AAIzC,SAAS,EACP,GACA,GAOQ;CACR,IAAM,oBAAM,IAAI,MAAM,EAChB,IAAO,IAAI,KAAK,EAAW,EAC3B,IAAS,EAAI,SAAS,GAAG,EAAK,SAAS,EACvC,IAAU,KAAK,MAAM,IAAS,MAAO,GAAG,EACxC,IAAS,KAAK,MAAM,IAAU,GAAG,EACjC,IAAU,KAAK,MAAM,IAAS,GAAG;AAQvC,QANI,IAAU,IAAU,EAAO,MAC3B,IAAU,KACL,EAAO,WAAW,QAAQ,aAAa,OAAO,EAAQ,CAAC,GAC5D,IAAS,KAAW,EAAO,SAAS,QAAQ,aAAa,OAAO,EAAO,CAAC,GACxE,MAAY,IAAU,EAAO,YAC7B,IAAU,IAAU,EAAO,QAAQ,QAAQ,aAAa,OAAO,EAAQ,CAAC,GACrE,EAAK,mBAAmB,KAAA,GAAW;EAAE,OAAO;EAAS,KAAK;EAAW,CAAC;;AAG/E,SAAS,EAAY,GAAuB;AAC1C,QAAO,EACJ,MAAM,IAAI,CACV,MAAM,GAAG,EAAE,CACX,KAAK,MAAS,EAAK,GAAG,CACtB,KAAK,GAAG,CACR,aAAa;;AAclB,IAAa,IAAkB,EAAK,SAAyB,EAC3D,iBACA,cAAW,IACX,YACA,cACuB;CACvB,IAAM,EACJ,UACA,SACA,UACA,gBACA,gBACA,aACA,YACA,gBACA,cACA,cACE,GAEE,IAAY,IAAc,GAC1B,IAAa,MAAU,YACvB,IAAK,GAAO,EACZ,IAAa;EACjB,KAAK,EAAG,qBAAqB,MAAM;EACnC,WAAW,EAAG,2BAA2B,YAAY;EACrD,YAAY,EAAG,iCAAiC,aAAa;EAC7D,UAAU,EAAG,+BAA+B,aAAa;EACzD,SAAS,EAAG,8BAA8B,aAAa;EACxD,EAIK,CAAC,GAAU,KAAe,EAAS,GAAM,EAEzC,EACJ,wBACA,0BACA,oBACA,sBACA,qBACA,0BACE,GAAwB,EAItB,KAAQ,MAAkD;AAC9D,IAAM,iBAAiB;;AAGzB,QACE,kBAAC,OAAD;EACE,aAAU;EACV,WAAW,EACT,iGACA,8BACA,IACI,uDACA,kEACJ,KAAa,CAAC,KAAY,oBAC3B;EACD,MAAK;EACL,UAAU;EACV,eAAe,IAAU,EAAa;EACtC,YAAY,MAAU;AACpB,IAAI,EAAM,QAAQ,WAAW,EAAM,QAAQ,SACzC,EAAM,gBAAgB,EACtB,IAAU,EAAa;;EAG3B,cACE,EAAG,kCAAkC,oBAAoB,GAAG,OAAO;YApBvE;GAwBE,kBAAC,OAAD;IACE,WAAW,EACT,6GACA,MAAS,YAAY,kDACrB,MAAS,aACP,yDACD,MAAS,WACR,MAAS,eACT,MAAS,kBACT,MAAS,SACT,MAAS,cACT,iCACH;IACD,eAAY;cAEX,IACC,kBAAC,OAAD;KAAK,KAAK;KAAQ,KAAI;KAAG,WAAU;KAA+B,CAAA,GAChE,MAAS,WACX,EAAY,EAAM,GAChB,MAAS,YACX,kBAAC,GAAD,EAAM,MAAM,IAAM,CAAA,GAElB,kBAAC,GAAD;KAA4B;KAAM,MAAM;KAAM,CAAA;IAE5C,CAAA;GAmBN,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,OAAD;MAAK,WAAU;gBAAf;OACE,kBAAC,QAAD;QACE,WAAW,EACT,oBACA,IACI,oCACA,gCACL;kBAEA;QACI,CAAA;OACN,KACC,kBAAC,GAAD;QACE,MAAM;QACN,WAAU;QACV,cAAY,EAAG,6BAA6B,SAAS;QACrD,CAAA;OAEH,KACC,kBAAC,GAAD;QACE,MAAM;QACN,WAAU;QACV,cAAY,EAAG,4BAA4B,QAAQ;QACnD,CAAA;OAEA;SAEN,kBAAC,QAAD;MACE,WAAW,EACT,6BACA,IACI,uCACA,sBACL;gBAEA,EACC,GAAa,aAAa,GAC1B,EACD;MACI,CAAA,CACH;QAEN,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,KAAD;MACE,WAAW,EACT,sCACA,IAAY,yBAAyB,sBACtC;gBAEA,IACC,kBAAA,GAAA,EAAA,UAAA,CACG,EAAY,OAAO,QAClB,kBAAC,QAAD;OAAM,WAAU;iBAAhB,CACG,EAAY,OAAO,MACnB,KACI;UAER,EAAY,WACX,EAAG,gCAAgC,qBAAqB,CACzD,EAAA,CAAA,GAEH,kBAAC,QAAD;OAAM,WAAU;iBACb,EAAG,4BAA4B,kBAAkB;OAC7C,CAAA;MAEP,CAAA,EAEJ,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACG,MAAS,YACR,kBAAC,QAAD;OACE,WAAU;OACV,cAAY,GAAG,EAAY,GAAG,EAAG,8BAA8B,UAAU;iBAF3E,CAIE,kBAAC,GAAD;QAAO,MAAM;QAAI,eAAY;QAAS,CAAA,EACrC,EACI;UAER,KACC,kBAAC,QAAD;OACE,WAAU;OACV,cAAY,GAAG,EAAY,GAAG,EAAG,kCAAkC,SAAS;iBAE3E,IAAc,KAAK,QAAQ;OACvB,CAAA,CAEL;QACF;OACF;;GAGN,kBAAC,GAAD;IAAc,MAAM;IAAU,cAAc;cAA5C,CACE,kBAAC,GAAD;KAAqB,SAAA;eACnB,kBAAC,UAAD;MACE,MAAK;MACL,SAAS;MACT,WAAW;MACX,cAAY,EACV,8BACA,uBACD;MACD,WAAW,EACT,iGACA,8EACA,4FACA,IACI,gBACA,mEACL;gBAED,kBAAC,GAAD;OAAgB,MAAM;OAAI,eAAY;OAAS,CAAA;MACxC,CAAA;KACW,CAAA,EACtB,kBAAC,GAAD;KACE,OAAM;KACN,WAAU;KACV,SAAS;eAHX;MAKE,kBAAC,GAAD;OACE,gBAAgB;AACR,YACF,EAAkB,EAAa,GAAG,GAClC,EAAgB,EAAa,GAAG;;iBAGrC,IACC,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,GAAD,EAAQ,MAAM,IAAM,CAAA,EACnB,EAAG,uBAAuB,QAAQ,CAClC,EAAA,CAAA,GAEH,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,GAAD,EAAK,MAAM,IAAM,CAAA,EAChB,EAAG,qBAAqB,MAAM,CAC9B,EAAA,CAAA;OAEY,CAAA;MACnB,kBAAC,GAAD;OACE,gBAAgB;AACR,YACF,EAAmB,EAAa,GAAG,GACnC,EAAiB,EAAa,GAAG;;iBAGtC,IACC,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,GAAD,EAAM,MAAM,IAAM,CAAA,EACjB,EAAG,wBAAwB,SAAS,CACpC,EAAA,CAAA,GAEH,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,GAAD,EAAS,MAAM,IAAM,CAAA,EACpB,EAAG,sBAAsB,OAAO,CAChC,EAAA,CAAA;OAEY,CAAA;MACnB,kBAAC,GAAD,EAAyB,CAAA;MACzB,kBAAC,GAAD;OACE,gBAAgB;AACR,YACF,EAAsB,EAAa,GAAG,GACtC,EAAoB,EAAa,GAAG;;iBAGzC,IACC,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,GAAD,EAAgB,MAAM,IAAM,CAAA,EAC3B,EAAG,2BAA2B,YAAY,CAC1C,EAAA,CAAA,GAEH,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,GAAD,EAAa,MAAM,IAAM,CAAA,EACxB,EAAG,yBAAyB,UAAU,CACtC,EAAA,CAAA;OAEY,CAAA;OACjB,MAAS,WAAW,MAAS,cAAc,KAC3C,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,GAAD,EAAyB,CAAA,EACzB,kBAAC,GAAD;OACE,gBAAgB,EAAQ,EAAa;OACrC,WAAU;iBAFZ,CAIE,kBAAC,GAAD,EAAQ,MAAM,IAAM,CAAA,EACnB,EAAG,uBAAuB,QAAQ,CAClB;SAClB,EAAA,CAAA;MAEe;OACT;;GACX;;EAER"}
@@ -1,19 +1,19 @@
1
1
  import { useConversations as e } from "../providers/ConversationsProvider.js";
2
2
  import { useTr as t } from "../utils/i18n.js";
3
3
  import { cn as n } from "../utils/cn.js";
4
- import { useCreateConversation as r } from "../hooks/useConversationQueries.js";
5
- import { QuotaExhaustedDialog as ee } from "./QuotaExhaustedDialog.js";
6
- import { nativeImpact as te, nativeNotify as i } from "../../utils/nativeBridge.js";
7
- import { MemberPicker as a } from "./MemberPicker.js";
4
+ import { useCreateConversation as ee, useStartDirectConversation as r } from "../hooks/useConversationQueries.js";
5
+ import { QuotaExhaustedDialog as i } from "./QuotaExhaustedDialog.js";
6
+ import { nativeImpact as te, nativeNotify as a } from "../../utils/nativeBridge.js";
7
+ import { MemberPicker as ne } from "./MemberPicker.js";
8
8
  import { useCallback as o, useMemo as s, useState as c } from "react";
9
- import { Globe as l, Hash as u, Lock as d, Megaphone as ne, MessageCircle as re, Shield as ie, Users as ae } from "lucide-react";
10
- import { useNavigate as oe } from "react-router";
11
- import { isQuotaExhaustedError as f } from "@burdenoff/fe-libs/shared/utils";
12
- import { useEventBus as se } from "@burdenoff/fe-libs/shared/events";
13
- import { Fragment as p, jsx as m, jsxs as h } from "react/jsx-runtime";
14
- import { Dialog as g, DialogContent as _, DialogDescription as v, DialogFooter as y, DialogHeader as ce, DialogTitle as le } from "@burdenoff/fe-libs/ui";
9
+ import { Globe as l, Hash as u, Lock as d, Megaphone as f, MessageCircle as p, Shield as re, Users as ie } from "lucide-react";
10
+ import { useNavigate as ae } from "react-router";
11
+ import { isQuotaExhaustedError as m } from "@burdenoff/fe-libs/shared/utils";
12
+ import { useEventBus as oe } from "@burdenoff/fe-libs/shared/events";
13
+ import { Fragment as se, jsx as h, jsxs as g } from "react/jsx-runtime";
14
+ import { Dialog as ce, DialogContent as le, DialogDescription as ue, DialogFooter as _, DialogHeader as de, DialogTitle as v } from "@burdenoff/fe-libs/ui";
15
15
  //#region src/conversations/components/NewConversationDialog.tsx
16
- function ue(e) {
16
+ function y(e) {
17
17
  if ("graphQLErrors" in e) {
18
18
  let t = e.graphQLErrors;
19
19
  if (t && t.length > 0 && t[0]?.message) return t[0].message;
@@ -21,251 +21,253 @@ function ue(e) {
21
21
  return e.message;
22
22
  }
23
23
  function b({ open: b, onOpenChange: x, onQuotaExhausted: S, onQuotaDialogClose: C, onCreated: w }) {
24
- let { createConversation: T, loading: E, error: D } = r(), { currentUser: O } = e(), k = se(), A = t(), j = oe(), [M, N] = c(!1), P = s(() => [
24
+ let { createConversation: T, loading: fe, error: pe } = ee(), { startDirectConversation: E, loading: me, error: he } = r(), { currentUser: D } = e(), O = oe(), k = t(), A = ae(), [j, M] = c(!1), N = s(() => [
25
25
  {
26
26
  type: "DIRECT",
27
- label: A("conversations.directMessage", "Direct Message"),
28
- description: A("conversations.directMessageDescription", "One-on-one private conversation"),
29
- icon: /* @__PURE__ */ m(re, { size: 20 })
27
+ label: k("conversations.directMessage", "Direct Message"),
28
+ description: k("conversations.directMessageDescription", "One-on-one private conversation"),
29
+ icon: /* @__PURE__ */ h(p, { size: 20 })
30
30
  },
31
31
  {
32
32
  type: "GROUP",
33
- label: A("conversations.groupChat", "Group Chat"),
34
- description: A("conversations.groupChatDescription", "Invite multiple people to a group conversation"),
35
- icon: /* @__PURE__ */ m(ae, { size: 20 })
33
+ label: k("conversations.groupChat", "Group Chat"),
34
+ description: k("conversations.groupChatDescription", "Invite multiple people to a group conversation"),
35
+ icon: /* @__PURE__ */ h(ie, { size: 20 })
36
36
  },
37
37
  {
38
38
  type: "CHANNEL",
39
- label: A("conversations.channel", "Channel"),
40
- description: A("conversations.channelDescription", "A topic-based channel that anyone can join"),
41
- icon: /* @__PURE__ */ m(u, { size: 20 })
39
+ label: k("conversations.channel", "Channel"),
40
+ description: k("conversations.channelDescription", "A topic-based channel that anyone can join"),
41
+ icon: /* @__PURE__ */ h(u, { size: 20 })
42
42
  },
43
43
  {
44
44
  type: "ANNOUNCEMENT",
45
- label: A("conversations.announcement", "Announcement"),
46
- description: A("conversations.announcementDescription", "Broadcast messages to a large audience"),
47
- icon: /* @__PURE__ */ m(ne, { size: 20 })
45
+ label: k("conversations.announcement", "Announcement"),
46
+ description: k("conversations.announcementDescription", "Broadcast messages to a large audience"),
47
+ icon: /* @__PURE__ */ h(f, { size: 20 })
48
48
  }
49
- ], [A]), F = s(() => [
49
+ ], [k]), P = s(() => [
50
50
  {
51
51
  value: "PUBLIC",
52
- label: A("conversations.public", "Public"),
53
- description: A("conversations.publicDescription", "Anyone in the workspace can discover and join"),
54
- icon: /* @__PURE__ */ m(l, { size: 16 })
52
+ label: k("conversations.public", "Public"),
53
+ description: k("conversations.publicDescription", "Anyone in the workspace can discover and join"),
54
+ icon: /* @__PURE__ */ h(l, { size: 16 })
55
55
  },
56
56
  {
57
57
  value: "PRIVATE",
58
- label: A("conversations.private", "Private"),
59
- description: A("conversations.privateDescription", "Only invited members can access"),
60
- icon: /* @__PURE__ */ m(d, { size: 16 })
58
+ label: k("conversations.private", "Private"),
59
+ description: k("conversations.privateDescription", "Only invited members can access"),
60
+ icon: /* @__PURE__ */ h(d, { size: 16 })
61
61
  },
62
62
  {
63
63
  value: "PROTECTED",
64
- label: A("conversations.protected", "Protected"),
65
- description: A("conversations.protectedDescription", "Discoverable but requires approval to join"),
66
- icon: /* @__PURE__ */ m(ie, { size: 16 })
64
+ label: k("conversations.protected", "Protected"),
65
+ description: k("conversations.protectedDescription", "Discoverable but requires approval to join"),
66
+ icon: /* @__PURE__ */ h(re, { size: 16 })
67
67
  }
68
- ], [A]), [I, L] = c("GROUP"), [R, z] = c(""), [B, V] = c(""), [H, U] = c("PRIVATE"), [W, G] = c([]), K = I === "DIRECT", q = !K, J = s(() => W.map((e) => e.id), [W]), Y = o(() => {
69
- L("GROUP"), z(""), V(""), U("PRIVATE"), G([]);
70
- }, []), de = o((e) => {
71
- G((t) => t.some((t) => t.id === e.id) ? t : K ? [e] : [...t, e]);
72
- }, [K]), X = o((e) => {
73
- G((t) => t.filter((t) => t.id !== e));
74
- }, []), Z = I === "DIRECT" || I === "GROUP", fe = s(() => O?.id ? [O.id] : [], [O?.id]), Q = o((e) => {
75
- e || Y(), x(e);
76
- }, [x, Y]), pe = o(async () => {
77
- if (!(Z && J.length === 0) && !(q && !R.trim())) {
68
+ ], [k]), [F, I] = c("GROUP"), [L, R] = c(""), [z, B] = c(""), [V, H] = c("PRIVATE"), [U, W] = c([]), G = F === "DIRECT", K = !G, q = G ? he : pe, J = G ? me : fe, Y = s(() => U.map((e) => e.id), [U]), X = o(() => {
69
+ I("GROUP"), R(""), B(""), H("PRIVATE"), W([]);
70
+ }, []), ge = o((e) => {
71
+ W((t) => t.some((t) => t.id === e.id) ? t : G ? [e] : [...t, e]);
72
+ }, [G]), _e = o((e) => {
73
+ W((t) => t.filter((t) => t.id !== e));
74
+ }, []), Z = F === "DIRECT" || F === "GROUP", ve = s(() => D?.id ? [D.id] : [], [D?.id]), Q = o((e) => {
75
+ e || X(), x(e);
76
+ }, [x, X]), ye = o(async () => {
77
+ if (!(Z && Y.length === 0) && !(K && !L.trim())) {
78
78
  te("light");
79
79
  try {
80
- let e = await T({
81
- type: I,
82
- title: q ? R.trim() : void 0,
83
- description: B.trim() || void 0,
84
- participantIds: J,
85
- visibility: H
80
+ let e = G ? await E(Y[0]) : await T({
81
+ type: F,
82
+ title: K ? L.trim() : void 0,
83
+ description: z.trim() || void 0,
84
+ participantIds: Y,
85
+ visibility: V
86
86
  });
87
87
  if (!e) {
88
- i("error");
88
+ a("error");
89
89
  return;
90
90
  }
91
- i("success"), k.emit("conversations.conversation.created", {
91
+ a("success"), O.emit("conversations.conversation.created", {
92
92
  conversationId: e.id,
93
- conversationType: I,
93
+ conversationType: F,
94
94
  source: "new-conversation-dialog"
95
- }), Y(), x(!1), w?.(e.id);
95
+ }), X(), x(!1), w?.(e.id);
96
96
  } catch (e) {
97
- if (f(e)) {
98
- Y(), N(!0), S ? S() : x(!1);
97
+ if (m(e)) {
98
+ X(), M(!0), S ? S() : x(!1);
99
99
  return;
100
100
  }
101
- throw i("error"), e;
101
+ throw a("error"), e;
102
102
  }
103
103
  }
104
104
  }, [
105
- k,
105
+ O,
106
106
  Z,
107
- J,
108
- q,
109
- R,
110
- T,
111
- I,
112
- B,
113
- H,
114
107
  Y,
108
+ K,
109
+ L,
110
+ T,
111
+ E,
112
+ F,
113
+ z,
114
+ V,
115
+ X,
115
116
  x,
116
117
  S,
117
- w
118
- ]), me = o(() => {
119
- N(!1), C?.();
120
- }, [C]), $ = (!Z || J.length > 0) && (!q || R.trim().length > 0);
121
- return /* @__PURE__ */ h(p, { children: [/* @__PURE__ */ m(g, {
118
+ w,
119
+ G
120
+ ]), be = o(() => {
121
+ M(!1), C?.();
122
+ }, [C]), $ = (!Z || Y.length > 0) && (!K || L.trim().length > 0);
123
+ return /* @__PURE__ */ g(se, { children: [/* @__PURE__ */ h(ce, {
122
124
  open: b,
123
125
  onOpenChange: Q,
124
- children: /* @__PURE__ */ h(_, {
126
+ children: /* @__PURE__ */ g(le, {
125
127
  className: "sm:max-w-[640px] max-h-[90vh] overflow-y-auto",
126
128
  "data-tour": "new-conversation-dialog",
127
129
  children: [
128
- /* @__PURE__ */ h(ce, { children: [/* @__PURE__ */ m(le, { children: A("conversations.newConversation", "New Conversation") }), /* @__PURE__ */ m(v, { children: A("conversations.newConversationSubtitle", "Start a new conversation with your team") })] }),
129
- /* @__PURE__ */ h("div", {
130
+ /* @__PURE__ */ g(de, { children: [/* @__PURE__ */ h(v, { children: k("conversations.newConversation", "New Conversation") }), /* @__PURE__ */ h(ue, { children: k("conversations.newConversationSubtitle", "Start a new conversation with your team") })] }),
131
+ /* @__PURE__ */ g("div", {
130
132
  className: "space-y-6",
131
133
  children: [
132
- /* @__PURE__ */ h("section", {
134
+ /* @__PURE__ */ g("section", {
133
135
  "data-tour": "conversation-type-selector",
134
136
  className: "space-y-3",
135
- children: [/* @__PURE__ */ m("h3", {
137
+ children: [/* @__PURE__ */ h("h3", {
136
138
  className: "text-sm font-semibold text-text-primary",
137
- children: A("conversations.conversationType", "Conversation Type")
138
- }), /* @__PURE__ */ m("div", {
139
+ children: k("conversations.conversationType", "Conversation Type")
140
+ }), /* @__PURE__ */ h("div", {
139
141
  className: "grid grid-cols-1 sm:grid-cols-2 gap-3",
140
- children: P.map((e) => /* @__PURE__ */ h("button", {
142
+ children: N.map((e) => /* @__PURE__ */ g("button", {
141
143
  type: "button",
142
144
  onClick: () => {
143
- L(e.type), e.type === "DIRECT" && G((e) => e.slice(0, 1));
145
+ I(e.type), e.type === "DIRECT" && W((e) => e.slice(0, 1));
144
146
  },
145
- className: n("flex items-start gap-3 p-4 rounded-xl border text-left transition-all duration-150", I === e.type ? "border-primary bg-action-primary-bg/5 ring-1 ring-primary/20" : "border-border-subtle hover:border-muted-foreground/30 hover:bg-bg-sunken/30"),
146
- children: [/* @__PURE__ */ m("div", {
147
- className: n("p-2 rounded-lg", I === e.type ? "bg-action-primary-bg/10 text-primary" : "bg-bg-sunken text-text-secondary"),
147
+ className: n("flex items-start gap-3 p-4 rounded-xl border text-left transition-all duration-150", F === e.type ? "border-primary bg-action-primary-bg/5 ring-1 ring-primary/20" : "border-border-subtle hover:border-muted-foreground/30 hover:bg-bg-sunken/30"),
148
+ children: [/* @__PURE__ */ h("div", {
149
+ className: n("p-2 rounded-lg", F === e.type ? "bg-action-primary-bg/10 text-primary" : "bg-bg-sunken text-text-secondary"),
148
150
  children: e.icon
149
- }), /* @__PURE__ */ h("div", { children: [/* @__PURE__ */ m("p", {
151
+ }), /* @__PURE__ */ g("div", { children: [/* @__PURE__ */ h("p", {
150
152
  className: "text-sm font-medium text-text-primary",
151
153
  children: e.label
152
- }), /* @__PURE__ */ m("p", {
154
+ }), /* @__PURE__ */ h("p", {
153
155
  className: "text-xs text-text-secondary mt-0.5",
154
156
  children: e.description
155
157
  })] })]
156
158
  }, e.type))
157
159
  })]
158
160
  }),
159
- !K && /* @__PURE__ */ h("section", {
161
+ !G && /* @__PURE__ */ g("section", {
160
162
  "data-tour": "conversation-visibility-selector",
161
163
  className: "space-y-3",
162
164
  children: [
163
- /* @__PURE__ */ m("h3", {
165
+ /* @__PURE__ */ h("h3", {
164
166
  className: "text-sm font-semibold text-text-primary",
165
- children: A("conversations.visibility", "Visibility")
167
+ children: k("conversations.visibility", "Visibility")
166
168
  }),
167
- /* @__PURE__ */ m("div", {
169
+ /* @__PURE__ */ h("div", {
168
170
  className: "flex gap-2 flex-wrap",
169
- children: F.map((e) => /* @__PURE__ */ h("button", {
171
+ children: P.map((e) => /* @__PURE__ */ g("button", {
170
172
  type: "button",
171
- onClick: () => U(e.value),
172
- className: n("flex items-center gap-2 px-4 py-2.5 rounded-lg border text-sm transition-all", H === e.value ? "border-primary bg-action-primary-bg/5 text-primary font-medium" : "border-border-subtle text-text-secondary hover:text-text-primary hover:bg-bg-sunken/30"),
173
+ onClick: () => H(e.value),
174
+ className: n("flex items-center gap-2 px-4 py-2.5 rounded-lg border text-sm transition-all", V === e.value ? "border-primary bg-action-primary-bg/5 text-primary font-medium" : "border-border-subtle text-text-secondary hover:text-text-primary hover:bg-bg-sunken/30"),
173
175
  children: [e.icon, e.label]
174
176
  }, e.value))
175
177
  }),
176
- /* @__PURE__ */ m("p", {
178
+ /* @__PURE__ */ h("p", {
177
179
  className: "text-xs text-text-secondary",
178
- children: F.find((e) => e.value === H)?.description
180
+ children: P.find((e) => e.value === V)?.description
179
181
  })
180
182
  ]
181
183
  }),
182
- q && /* @__PURE__ */ h("section", {
184
+ K && /* @__PURE__ */ g("section", {
183
185
  "data-tour": "conversation-create-details",
184
186
  className: "space-y-3",
185
- children: [/* @__PURE__ */ h("div", { children: [/* @__PURE__ */ h("label", {
187
+ children: [/* @__PURE__ */ g("div", { children: [/* @__PURE__ */ g("label", {
186
188
  htmlFor: "conv-title",
187
189
  className: "text-sm font-semibold text-text-primary",
188
190
  children: [
189
- A("conversations.name", "Name"),
191
+ k("conversations.name", "Name"),
190
192
  " ",
191
- /* @__PURE__ */ m("span", {
193
+ /* @__PURE__ */ h("span", {
192
194
  className: "text-status-error-text",
193
195
  children: "*"
194
196
  })
195
197
  ]
196
- }), /* @__PURE__ */ m("input", {
198
+ }), /* @__PURE__ */ h("input", {
197
199
  id: "conv-title",
198
200
  type: "text",
199
- value: R,
200
- onChange: (e) => z(e.target.value),
201
- placeholder: I === "CHANNEL" ? A("conversations.channelNamePlaceholder", "e.g., engineering-general") : A("conversations.conversationNamePlaceholder", "e.g., Product Launch Q1"),
201
+ value: L,
202
+ onChange: (e) => R(e.target.value),
203
+ placeholder: F === "CHANNEL" ? k("conversations.channelNamePlaceholder", "e.g., engineering-general") : k("conversations.conversationNamePlaceholder", "e.g., Product Launch Q1"),
202
204
  maxLength: 200,
203
205
  className: "mt-1.5 w-full px-3 py-2 text-sm rounded-lg border border-border-subtle bg-bg-surface placeholder:text-text-secondary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]"
204
- })] }), /* @__PURE__ */ h("div", { children: [/* @__PURE__ */ m("label", {
206
+ })] }), /* @__PURE__ */ g("div", { children: [/* @__PURE__ */ h("label", {
205
207
  htmlFor: "conv-desc",
206
208
  className: "text-sm font-semibold text-text-primary",
207
- children: A("conversations.description", "Description")
208
- }), /* @__PURE__ */ m("textarea", {
209
+ children: k("conversations.description", "Description")
210
+ }), /* @__PURE__ */ h("textarea", {
209
211
  id: "conv-desc",
210
- value: B,
211
- onChange: (e) => V(e.target.value),
212
- placeholder: A("conversations.descriptionPlaceholder", "What is this conversation about?"),
212
+ value: z,
213
+ onChange: (e) => B(e.target.value),
214
+ placeholder: k("conversations.descriptionPlaceholder", "What is this conversation about?"),
213
215
  rows: 2,
214
216
  maxLength: 1e3,
215
217
  className: "mt-1.5 w-full px-3 py-2 text-sm rounded-lg border border-border-subtle bg-bg-surface placeholder:text-text-secondary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)] resize-none"
216
218
  })] })]
217
219
  }),
218
- /* @__PURE__ */ h("section", {
220
+ /* @__PURE__ */ g("section", {
219
221
  "data-tour": "conversation-participants",
220
222
  className: "space-y-3",
221
- children: [/* @__PURE__ */ h("h3", {
223
+ children: [/* @__PURE__ */ g("h3", {
222
224
  className: "text-sm font-semibold text-text-primary",
223
- children: [K ? A("conversations.recipient", "Recipient") : A("conversations.participants", "Participants"), Z ? /* @__PURE__ */ m("span", {
225
+ children: [G ? k("conversations.recipient", "Recipient") : k("conversations.participants", "Participants"), Z ? /* @__PURE__ */ h("span", {
224
226
  className: "ml-1 text-status-error-text",
225
227
  children: "*"
226
- }) : /* @__PURE__ */ h("span", {
228
+ }) : /* @__PURE__ */ g("span", {
227
229
  className: "ml-1.5 text-xs font-normal text-text-secondary",
228
230
  children: [
229
231
  "(",
230
- A("conversations.optional", "optional"),
232
+ k("conversations.optional", "optional"),
231
233
  ")"
232
234
  ]
233
235
  })]
234
- }), /* @__PURE__ */ m(a, {
235
- selected: W,
236
- onAdd: de,
237
- onRemove: X,
238
- single: K,
239
- excludeIds: fe
236
+ }), /* @__PURE__ */ h(ne, {
237
+ selected: U,
238
+ onAdd: ge,
239
+ onRemove: _e,
240
+ single: G,
241
+ excludeIds: ve
240
242
  })]
241
243
  }),
242
- D && !f(D) && /* @__PURE__ */ m("div", {
244
+ q && !m(q) && /* @__PURE__ */ h("div", {
243
245
  className: "text-xs text-status-error-text rounded-lg border border-status-error-border/30 bg-status-error-bg/10 px-3 py-2",
244
- children: ue(D)
246
+ children: y(q)
245
247
  })
246
248
  ]
247
249
  }),
248
- /* @__PURE__ */ h(y, { children: [/* @__PURE__ */ m("button", {
250
+ /* @__PURE__ */ g(_, { children: [/* @__PURE__ */ h("button", {
249
251
  type: "button",
250
252
  onClick: () => Q(!1),
251
253
  className: "px-4 py-2 text-sm font-medium rounded-lg text-text-secondary hover:text-text-primary hover:bg-bg-sunken/50 transition-colors",
252
- children: A("conversations.cancel", "Cancel")
253
- }), /* @__PURE__ */ m("button", {
254
+ children: k("conversations.cancel", "Cancel")
255
+ }), /* @__PURE__ */ h("button", {
254
256
  type: "button",
255
- onClick: () => void pe(),
256
- disabled: !$ || E,
257
- className: n("inline-flex items-center gap-2 px-5 py-2 text-sm font-medium rounded-lg transition-all", $ && !E ? "bg-action-primary-bg text-action-primary-text hover:bg-action-primary-bg/90 shadow-sm" : "bg-bg-sunken text-text-secondary cursor-not-allowed"),
258
- children: E ? A("conversations.creating", "Creating...") : A("conversations.createConversation", "Create Conversation")
257
+ onClick: () => void ye(),
258
+ disabled: !$ || J,
259
+ className: n("inline-flex items-center gap-2 px-5 py-2 text-sm font-medium rounded-lg transition-all", $ && !J ? "bg-action-primary-bg text-action-primary-text hover:bg-action-primary-bg/90 shadow-sm" : "bg-bg-sunken text-text-secondary cursor-not-allowed"),
260
+ children: J ? k("conversations.creating", "Creating...") : k("conversations.createConversation", "Create Conversation")
259
261
  })] })
260
262
  ]
261
263
  })
262
- }), /* @__PURE__ */ m(ee, {
263
- open: M,
264
- title: A("conversations.quota.conversations.title", "Conversation Quota Reached"),
265
- description: A("conversations.quota.conversations.description", "You've reached the conversation limit for your current plan. View billing to upgrade or add more capacity."),
266
- onClose: me,
264
+ }), /* @__PURE__ */ h(i, {
265
+ open: j,
266
+ title: k("conversations.quota.conversations.title", "Conversation Quota Reached"),
267
+ description: k("conversations.quota.conversations.description", "You've reached the conversation limit for your current plan. View billing to upgrade or add more capacity."),
268
+ onClose: be,
267
269
  onConfirm: () => {
268
- N(!1), j("/billing");
270
+ M(!1), A("/billing");
269
271
  }
270
272
  })] });
271
273
  }