@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.
- package/dist/conversations/components/ConversationRow.js +1 -1
- package/dist/conversations/components/ConversationRow.js.map +1 -1
- package/dist/conversations/components/NewConversationDialog.js +135 -133
- package/dist/conversations/components/NewConversationDialog.js.map +1 -1
- package/dist/conversations/hooks/useConversationQueries.js +66 -49
- package/dist/conversations/hooks/useConversationQueries.js.map +1 -1
- package/dist/generated/wspace-operations.js +87 -32
- package/dist/generated/wspace-operations.js.map +1 -1
- package/package.json +1 -1
|
@@ -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":"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"}
|
|
@@ -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
|
|
6
|
-
import { nativeImpact as te, nativeNotify as
|
|
7
|
-
import { MemberPicker as
|
|
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
|
|
10
|
-
import { useNavigate as
|
|
11
|
-
import { isQuotaExhaustedError as
|
|
12
|
-
import { useEventBus as
|
|
13
|
-
import { Fragment as
|
|
14
|
-
import { Dialog as
|
|
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
|
|
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:
|
|
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:
|
|
28
|
-
description:
|
|
29
|
-
icon: /* @__PURE__ */
|
|
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:
|
|
34
|
-
description:
|
|
35
|
-
icon: /* @__PURE__ */
|
|
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:
|
|
40
|
-
description:
|
|
41
|
-
icon: /* @__PURE__ */
|
|
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:
|
|
46
|
-
description:
|
|
47
|
-
icon: /* @__PURE__ */
|
|
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
|
-
], [
|
|
49
|
+
], [k]), P = s(() => [
|
|
50
50
|
{
|
|
51
51
|
value: "PUBLIC",
|
|
52
|
-
label:
|
|
53
|
-
description:
|
|
54
|
-
icon: /* @__PURE__ */
|
|
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:
|
|
59
|
-
description:
|
|
60
|
-
icon: /* @__PURE__ */
|
|
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:
|
|
65
|
-
description:
|
|
66
|
-
icon: /* @__PURE__ */
|
|
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
|
-
], [
|
|
69
|
-
|
|
70
|
-
}, []),
|
|
71
|
-
|
|
72
|
-
}, [
|
|
73
|
-
|
|
74
|
-
}, []), Z =
|
|
75
|
-
e ||
|
|
76
|
-
}, [x,
|
|
77
|
-
if (!(Z &&
|
|
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:
|
|
82
|
-
title:
|
|
83
|
-
description:
|
|
84
|
-
participantIds:
|
|
85
|
-
visibility:
|
|
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
|
-
|
|
88
|
+
a("error");
|
|
89
89
|
return;
|
|
90
90
|
}
|
|
91
|
-
|
|
91
|
+
a("success"), O.emit("conversations.conversation.created", {
|
|
92
92
|
conversationId: e.id,
|
|
93
|
-
conversationType:
|
|
93
|
+
conversationType: F,
|
|
94
94
|
source: "new-conversation-dialog"
|
|
95
|
-
}),
|
|
95
|
+
}), X(), x(!1), w?.(e.id);
|
|
96
96
|
} catch (e) {
|
|
97
|
-
if (
|
|
98
|
-
|
|
97
|
+
if (m(e)) {
|
|
98
|
+
X(), M(!0), S ? S() : x(!1);
|
|
99
99
|
return;
|
|
100
100
|
}
|
|
101
|
-
throw
|
|
101
|
+
throw a("error"), e;
|
|
102
102
|
}
|
|
103
103
|
}
|
|
104
104
|
}, [
|
|
105
|
-
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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__ */
|
|
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__ */
|
|
129
|
-
/* @__PURE__ */
|
|
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__ */
|
|
134
|
+
/* @__PURE__ */ g("section", {
|
|
133
135
|
"data-tour": "conversation-type-selector",
|
|
134
136
|
className: "space-y-3",
|
|
135
|
-
children: [/* @__PURE__ */
|
|
137
|
+
children: [/* @__PURE__ */ h("h3", {
|
|
136
138
|
className: "text-sm font-semibold text-text-primary",
|
|
137
|
-
children:
|
|
138
|
-
}), /* @__PURE__ */
|
|
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:
|
|
142
|
+
children: N.map((e) => /* @__PURE__ */ g("button", {
|
|
141
143
|
type: "button",
|
|
142
144
|
onClick: () => {
|
|
143
|
-
|
|
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",
|
|
146
|
-
children: [/* @__PURE__ */
|
|
147
|
-
className: n("p-2 rounded-lg",
|
|
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__ */
|
|
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__ */
|
|
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
|
-
!
|
|
161
|
+
!G && /* @__PURE__ */ g("section", {
|
|
160
162
|
"data-tour": "conversation-visibility-selector",
|
|
161
163
|
className: "space-y-3",
|
|
162
164
|
children: [
|
|
163
|
-
/* @__PURE__ */
|
|
165
|
+
/* @__PURE__ */ h("h3", {
|
|
164
166
|
className: "text-sm font-semibold text-text-primary",
|
|
165
|
-
children:
|
|
167
|
+
children: k("conversations.visibility", "Visibility")
|
|
166
168
|
}),
|
|
167
|
-
/* @__PURE__ */
|
|
169
|
+
/* @__PURE__ */ h("div", {
|
|
168
170
|
className: "flex gap-2 flex-wrap",
|
|
169
|
-
children:
|
|
171
|
+
children: P.map((e) => /* @__PURE__ */ g("button", {
|
|
170
172
|
type: "button",
|
|
171
|
-
onClick: () =>
|
|
172
|
-
className: n("flex items-center gap-2 px-4 py-2.5 rounded-lg border text-sm transition-all",
|
|
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__ */
|
|
178
|
+
/* @__PURE__ */ h("p", {
|
|
177
179
|
className: "text-xs text-text-secondary",
|
|
178
|
-
children:
|
|
180
|
+
children: P.find((e) => e.value === V)?.description
|
|
179
181
|
})
|
|
180
182
|
]
|
|
181
183
|
}),
|
|
182
|
-
|
|
184
|
+
K && /* @__PURE__ */ g("section", {
|
|
183
185
|
"data-tour": "conversation-create-details",
|
|
184
186
|
className: "space-y-3",
|
|
185
|
-
children: [/* @__PURE__ */
|
|
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
|
-
|
|
191
|
+
k("conversations.name", "Name"),
|
|
190
192
|
" ",
|
|
191
|
-
/* @__PURE__ */
|
|
193
|
+
/* @__PURE__ */ h("span", {
|
|
192
194
|
className: "text-status-error-text",
|
|
193
195
|
children: "*"
|
|
194
196
|
})
|
|
195
197
|
]
|
|
196
|
-
}), /* @__PURE__ */
|
|
198
|
+
}), /* @__PURE__ */ h("input", {
|
|
197
199
|
id: "conv-title",
|
|
198
200
|
type: "text",
|
|
199
|
-
value:
|
|
200
|
-
onChange: (e) =>
|
|
201
|
-
placeholder:
|
|
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__ */
|
|
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:
|
|
208
|
-
}), /* @__PURE__ */
|
|
209
|
+
children: k("conversations.description", "Description")
|
|
210
|
+
}), /* @__PURE__ */ h("textarea", {
|
|
209
211
|
id: "conv-desc",
|
|
210
|
-
value:
|
|
211
|
-
onChange: (e) =>
|
|
212
|
-
placeholder:
|
|
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__ */
|
|
220
|
+
/* @__PURE__ */ g("section", {
|
|
219
221
|
"data-tour": "conversation-participants",
|
|
220
222
|
className: "space-y-3",
|
|
221
|
-
children: [/* @__PURE__ */
|
|
223
|
+
children: [/* @__PURE__ */ g("h3", {
|
|
222
224
|
className: "text-sm font-semibold text-text-primary",
|
|
223
|
-
children: [
|
|
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__ */
|
|
228
|
+
}) : /* @__PURE__ */ g("span", {
|
|
227
229
|
className: "ml-1.5 text-xs font-normal text-text-secondary",
|
|
228
230
|
children: [
|
|
229
231
|
"(",
|
|
230
|
-
|
|
232
|
+
k("conversations.optional", "optional"),
|
|
231
233
|
")"
|
|
232
234
|
]
|
|
233
235
|
})]
|
|
234
|
-
}), /* @__PURE__ */
|
|
235
|
-
selected:
|
|
236
|
-
onAdd:
|
|
237
|
-
onRemove:
|
|
238
|
-
single:
|
|
239
|
-
excludeIds:
|
|
236
|
+
}), /* @__PURE__ */ h(ne, {
|
|
237
|
+
selected: U,
|
|
238
|
+
onAdd: ge,
|
|
239
|
+
onRemove: _e,
|
|
240
|
+
single: G,
|
|
241
|
+
excludeIds: ve
|
|
240
242
|
})]
|
|
241
243
|
}),
|
|
242
|
-
|
|
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:
|
|
246
|
+
children: y(q)
|
|
245
247
|
})
|
|
246
248
|
]
|
|
247
249
|
}),
|
|
248
|
-
/* @__PURE__ */
|
|
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:
|
|
253
|
-
}), /* @__PURE__ */
|
|
254
|
+
children: k("conversations.cancel", "Cancel")
|
|
255
|
+
}), /* @__PURE__ */ h("button", {
|
|
254
256
|
type: "button",
|
|
255
|
-
onClick: () => void
|
|
256
|
-
disabled: !$ ||
|
|
257
|
-
className: n("inline-flex items-center gap-2 px-5 py-2 text-sm font-medium rounded-lg transition-all", $ && !
|
|
258
|
-
children:
|
|
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__ */
|
|
263
|
-
open:
|
|
264
|
-
title:
|
|
265
|
-
description:
|
|
266
|
-
onClose:
|
|
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
|
-
|
|
270
|
+
M(!1), A("/billing");
|
|
269
271
|
}
|
|
270
272
|
})] });
|
|
271
273
|
}
|