@burdenoff/microfe-conversations 2026.624.1 → 2026.625.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.
- package/dist/conversations/components/ConversationRow.js +1 -1
- package/dist/conversations/components/ConversationRow.js.map +1 -1
- package/dist/conversations/components/EmptyState.js +15 -9
- package/dist/conversations/components/EmptyState.js.map +1 -1
- package/dist/conversations/components/InboxLayout.js +65 -81
- package/dist/conversations/components/InboxLayout.js.map +1 -1
- package/dist/conversations/pages/AdminConversationsLandingPage.js +56 -46
- package/dist/conversations/pages/AdminConversationsLandingPage.js.map +1 -1
- package/dist/conversations/pages/ConversationAdminPages.js +182 -154
- package/dist/conversations/pages/ConversationAdminPages.js.map +1 -1
- package/dist/conversations/pages/ConversationDetailPage.js +3 -3
- package/dist/conversations/pages/ConversationDetailPage.js.map +1 -1
- package/dist/conversations/pages/ConversationListPage.js +64 -58
- package/dist/conversations/pages/ConversationListPage.js.map +1 -1
- package/dist/conversations/pages/ConversationsSettingsPage.js +87 -83
- package/dist/conversations/pages/ConversationsSettingsPage.js.map +1 -1
- package/package.json +2 -2
|
@@ -112,7 +112,7 @@ var j = n(function({ conversation: n, isActive: c = !1, onClick: u, onLeave: f }
|
|
|
112
112
|
"aria-hidden": "true"
|
|
113
113
|
}), L]
|
|
114
114
|
}), B && /* @__PURE__ */ b("span", {
|
|
115
|
-
className: "inline-flex h-[18px] min-w-[18px] items-center justify-center rounded-full bg-action-primary-bg px-1 text-[10px] font-bold text-action-primary-text",
|
|
115
|
+
className: "inline-flex h-[18px] min-w-[18px] items-center justify-center rounded-full bg-action-primary-bg px-1 text-[10px] font-bold text-action-primary-text motion-safe:animate-vc-glow-pulse",
|
|
116
116
|
"aria-label": `${P} ${U("conversations.unreadCountLabel", "unread")}`,
|
|
117
117
|
children: P > 99 ? "99+" : P
|
|
118
118
|
})]
|
|
@@ -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 { useI18n } from \"@burdenoff/fe-libs/shared/providers/shell/I18nProvider\";\nimport { cn } from \"../utils/cn\";\nimport { useConversationActions } from \"../hooks/useConversationQueries\";\nimport type { Conversation } from \"../types\";\n\nexport interface ConversationRowProps {\n conversation: Conversation;\n isActive?: boolean;\n onClick?: (conversation: Conversation) => void;\n /** Optional leave handler — only the host has the membership mutation. */\n onLeave?: (conversation: Conversation) => void;\n}\n\nfunction ConversationTypeIcon({\n type,\n size = 16,\n className,\n}: {\n type: Conversation[\"type\"];\n size?: number;\n className?: string;\n}) {\n const props = { size, className };\n switch (type) {\n case \"DIRECT\":\n return <MessageCircle {...props} />;\n case \"GROUP\":\n return <Users {...props} />;\n case \"CHANNEL\":\n return <Hash {...props} />;\n case \"BROADCAST\":\n case \"ANNOUNCEMENT\":\n return <Megaphone {...props} />;\n case \"BOT\":\n return <Bot {...props} />;\n case \"SUPPORT\":\n return <HeadphonesIcon {...props} />;\n default:\n return <MessageCircle {...props} />;\n }\n}\n\nfunction formatRelativeTime(\n dateString: string,\n labels: { now: string; yesterday: string },\n): string {\n const now = new Date();\n const date = new Date(dateString);\n const diffMs = now.getTime() - date.getTime();\n const diffMin = Math.floor(diffMs / 1000 / 60);\n const diffHr = Math.floor(diffMin / 60);\n const diffDay = Math.floor(diffHr / 24);\n\n if (diffMin < 1) return labels.now;\n if (diffMin < 60) return `${diffMin}m`;\n if (diffHr < 24) return `${diffHr}h`;\n if (diffDay === 1) return labels.yesterday;\n if (diffDay < 7) return `${diffDay}d`;\n return date.toLocaleDateString(undefined, { month: \"short\", day: \"numeric\" });\n}\n\nfunction getInitials(title: string): string {\n return title\n .split(\" \")\n .slice(0, 2)\n .map((word) => word[0])\n .join(\"\")\n .toUpperCase();\n}\n\n/**\n * ConversationRow — inbox-style list row.\n *\n * Visual hierarchy:\n * [avatar] Title · 2h\n * last message preview (badge)\n *\n * On hover the kebab (3-dots) menu reveals so the user can pin/mute/archive\n * without opening the conversation first. Wires straight into\n * `useConversationActions` for archive/unarchive/pin/mute.\n */\nexport const ConversationRow = memo(function ConversationRow({\n conversation,\n isActive = false,\n onClick,\n onLeave,\n}: ConversationRowProps) {\n const {\n title,\n type,\n state,\n lastMessage,\n unreadCount,\n isPinned,\n isMuted,\n memberCount,\n updatedAt,\n avatar,\n } = conversation;\n\n const hasUnread = unreadCount > 0;\n const isArchived = state === \"ARCHIVED\";\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const value = t(key);\n return value === key ? fallback : value;\n };\n const timeLabels = {\n now: tr(\"conversations.now\", \"now\"),\n yesterday: tr(\"conversations.yesterday\", \"Yesterday\"),\n };\n\n // Track menu open so the kebab stays visible even when the cursor leaves\n // the row (otherwise the radix portal closes on every hover-out).\n const [menuOpen, setMenuOpen] = useState(false);\n\n const {\n archiveConversation,\n unarchiveConversation,\n pinConversation,\n unpinConversation,\n muteConversation,\n unmuteConversation,\n } = useConversationActions();\n\n // Stop click bubbling from the kebab into the row's click handler — opening\n // the menu must not also open the conversation.\n const stop = (event: React.MouseEvent | React.KeyboardEvent) => {\n event.stopPropagation();\n };\n\n return (\n <div\n data-tour=\"conversation-row\"\n className={cn(\n \"group relative flex w-full min-w-0 items-start gap-3 rounded-md px-3 py-2.5 transition-colors\",\n \"cursor-pointer select-none\",\n isActive\n ? \"bg-bg-sunken border-l-2 border-l-action-primary-bg\"\n : \"border-l-2 border-l-transparent hover:bg-action-ghost-bg-hover\",\n hasUnread && !isActive && \"bg-bg-elevated/40\",\n )}\n role=\"button\"\n tabIndex={0}\n onClick={() => onClick?.(conversation)}\n onKeyDown={(event) => {\n if (event.key === \"Enter\" || event.key === \" \") {\n event.preventDefault();\n onClick?.(conversation);\n }\n }}\n aria-label={\n tr(\"conversations.openConversation\", \"Open conversation\") + \": \" + title\n }\n >\n {/* Avatar / type icon */}\n <div\n className={cn(\n \"flex-shrink-0 flex h-9 w-9 items-center justify-center rounded-full text-xs font-semibold overflow-hidden\",\n type === \"DIRECT\" && \"bg-status-info-bg-subtle text-status-info-text\",\n type === \"CHANNEL\" &&\n \"bg-status-success-bg-subtle text-status-success-text\",\n (type === \"GROUP\" ||\n type === \"BROADCAST\" ||\n type === \"ANNOUNCEMENT\" ||\n type === \"BOT\" ||\n type === \"SUPPORT\") &&\n \"bg-bg-sunken text-text-primary\",\n )}\n aria-hidden=\"true\"\n >\n {avatar ? (\n <img src={avatar} alt=\"\" className=\"h-full w-full object-cover\" />\n ) : type === \"DIRECT\" ? (\n getInitials(title)\n ) : type === \"CHANNEL\" ? (\n <Hash size={16} />\n ) : (\n <ConversationTypeIcon type={type} size={16} />\n )}\n </div>\n\n {/* Body */}\n <div className=\"flex min-w-0 flex-1 flex-col gap-0.5\">\n <div className=\"flex items-center justify-between gap-2\">\n <div className=\"flex min-w-0 items-center gap-1.5\">\n <span\n className={cn(\n \"truncate text-sm\",\n hasUnread\n ? \"font-semibold text-text-primary\"\n : \"font-medium text-text-primary\",\n )}\n >\n {title}\n </span>\n {isPinned && (\n <Pin\n size={11}\n className=\"flex-shrink-0 text-text-secondary\"\n aria-label={tr(\"conversations.pinnedLabel\", \"Pinned\")}\n />\n )}\n {isMuted && (\n <VolumeX\n size={11}\n className=\"flex-shrink-0 text-text-secondary\"\n aria-label={tr(\"conversations.mutedLabel\", \"Muted\")}\n />\n )}\n </div>\n\n <span\n className={cn(\n \"flex-shrink-0 text-[11px]\",\n hasUnread\n ? \"font-medium text-action-primary-bg\"\n : \"text-text-secondary\",\n )}\n >\n {formatRelativeTime(\n lastMessage?.createdAt ?? updatedAt,\n timeLabels,\n )}\n </span>\n </div>\n\n <div className=\"flex items-center justify-between gap-2\">\n <p\n className={cn(\n \"min-w-0 truncate break-all text-xs\",\n hasUnread ? \"text-text-primary/80\" : \"text-text-secondary\",\n )}\n >\n {lastMessage ? (\n <>\n {lastMessage.sender.name && (\n <span className=\"font-medium\">\n {lastMessage.sender.name}\n {\": \"}\n </span>\n )}\n {lastMessage.content ||\n tr(\"conversations.sentAttachment\", \"Sent an attachment\")}\n </>\n ) : (\n <span className=\"italic\">\n {tr(\"conversations.noMessages\", \"No messages yet\")}\n </span>\n )}\n </p>\n\n <div className=\"flex flex-shrink-0 items-center gap-1.5\">\n {type !== \"DIRECT\" && (\n <span\n className=\"inline-flex items-center gap-0.5 text-[10px] text-text-secondary\"\n aria-label={`${memberCount} ${tr(\"conversations.membersLabel\", \"members\")}`}\n >\n <Users size={10} aria-hidden=\"true\" />\n {memberCount}\n </span>\n )}\n {hasUnread && (\n <span\n className=\"inline-flex h-[18px] min-w-[18px] items-center justify-center rounded-full bg-action-primary-bg px-1 text-[10px] font-bold text-action-primary-text\"\n aria-label={`${unreadCount} ${tr(\"conversations.unreadCountLabel\", \"unread\")}`}\n >\n {unreadCount > 99 ? \"99+\" : unreadCount}\n </span>\n )}\n </div>\n </div>\n </div>\n\n {/* Kebab menu — hidden until hover or menu open */}\n <DropdownMenu open={menuOpen} onOpenChange={setMenuOpen}>\n <DropdownMenuTrigger asChild>\n <button\n type=\"button\"\n onClick={stop}\n onKeyDown={stop}\n aria-label={tr(\n \"conversations.rowMenuLabel\",\n \"Conversation actions\",\n )}\n className={cn(\n \"absolute right-2 top-2 flex h-7 w-7 items-center justify-center rounded-md transition-opacity\",\n \"text-text-secondary hover:bg-action-ghost-bg-hover hover:text-text-primary\",\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-action-primary-border\",\n menuOpen\n ? \"opacity-100\"\n : \"opacity-0 group-hover:opacity-100 group-focus-within:opacity-100\",\n )}\n >\n <MoreHorizontal size={14} aria-hidden=\"true\" />\n </button>\n </DropdownMenuTrigger>\n <DropdownMenuContent\n align=\"end\"\n className=\"min-w-[180px]\"\n onClick={stop}\n >\n <DropdownMenuItem\n onSelect={() => {\n void (isPinned\n ? unpinConversation(conversation.id)\n : pinConversation(conversation.id));\n }}\n >\n {isPinned ? (\n <>\n <PinOff size={14} />\n {tr(\"conversations.unpin\", \"Unpin\")}\n </>\n ) : (\n <>\n <Pin size={14} />\n {tr(\"conversations.pin\", \"Pin\")}\n </>\n )}\n </DropdownMenuItem>\n <DropdownMenuItem\n onSelect={() => {\n void (isMuted\n ? unmuteConversation(conversation.id)\n : muteConversation(conversation.id));\n }}\n >\n {isMuted ? (\n <>\n <Bell size={14} />\n {tr(\"conversations.unmute\", \"Unmute\")}\n </>\n ) : (\n <>\n <BellOff size={14} />\n {tr(\"conversations.mute\", \"Mute\")}\n </>\n )}\n </DropdownMenuItem>\n <DropdownMenuSeparator />\n <DropdownMenuItem\n onSelect={() => {\n void (isArchived\n ? unarchiveConversation(conversation.id)\n : archiveConversation(conversation.id));\n }}\n >\n {isArchived ? (\n <>\n <ArchiveRestore size={14} />\n {tr(\"conversations.unarchive\", \"Unarchive\")}\n </>\n ) : (\n <>\n <ArchiveIcon size={14} />\n {tr(\"conversations.archive\", \"Archive\")}\n </>\n )}\n </DropdownMenuItem>\n {(type === \"GROUP\" || type === \"CHANNEL\") && onLeave && (\n <>\n <DropdownMenuSeparator />\n <DropdownMenuItem\n onSelect={() => onLeave(conversation)}\n className=\"text-status-error-text focus:text-status-error-text\"\n >\n <LogOut size={14} />\n {tr(\"conversations.leave\", \"Leave\")}\n </DropdownMenuItem>\n </>\n )}\n </DropdownMenuContent>\n </DropdownMenu>\n </div>\n );\n});\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ConversationRow.js","names":[],"sources":["../../../src/conversations/components/ConversationRow.tsx"],"sourcesContent":["import { memo, useState } from \"react\";\nimport {\n ArchiveIcon,\n ArchiveRestore,\n Bell,\n BellOff,\n Bot,\n Hash,\n HeadphonesIcon,\n LogOut,\n Megaphone,\n MessageCircle,\n MoreHorizontal,\n Pin,\n PinOff,\n Users,\n VolumeX,\n} from \"lucide-react\";\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n} from \"@burdenoff/fe-libs/ui\";\nimport { useI18n } from \"@burdenoff/fe-libs/shared/providers/shell/I18nProvider\";\nimport { cn } from \"../utils/cn\";\nimport { useConversationActions } from \"../hooks/useConversationQueries\";\nimport type { Conversation } from \"../types\";\n\nexport interface ConversationRowProps {\n conversation: Conversation;\n isActive?: boolean;\n onClick?: (conversation: Conversation) => void;\n /** Optional leave handler — only the host has the membership mutation. */\n onLeave?: (conversation: Conversation) => void;\n}\n\nfunction ConversationTypeIcon({\n type,\n size = 16,\n className,\n}: {\n type: Conversation[\"type\"];\n size?: number;\n className?: string;\n}) {\n const props = { size, className };\n switch (type) {\n case \"DIRECT\":\n return <MessageCircle {...props} />;\n case \"GROUP\":\n return <Users {...props} />;\n case \"CHANNEL\":\n return <Hash {...props} />;\n case \"BROADCAST\":\n case \"ANNOUNCEMENT\":\n return <Megaphone {...props} />;\n case \"BOT\":\n return <Bot {...props} />;\n case \"SUPPORT\":\n return <HeadphonesIcon {...props} />;\n default:\n return <MessageCircle {...props} />;\n }\n}\n\nfunction formatRelativeTime(\n dateString: string,\n labels: { now: string; yesterday: string },\n): string {\n const now = new Date();\n const date = new Date(dateString);\n const diffMs = now.getTime() - date.getTime();\n const diffMin = Math.floor(diffMs / 1000 / 60);\n const diffHr = Math.floor(diffMin / 60);\n const diffDay = Math.floor(diffHr / 24);\n\n if (diffMin < 1) return labels.now;\n if (diffMin < 60) return `${diffMin}m`;\n if (diffHr < 24) return `${diffHr}h`;\n if (diffDay === 1) return labels.yesterday;\n if (diffDay < 7) return `${diffDay}d`;\n return date.toLocaleDateString(undefined, { month: \"short\", day: \"numeric\" });\n}\n\nfunction getInitials(title: string): string {\n return title\n .split(\" \")\n .slice(0, 2)\n .map((word) => word[0])\n .join(\"\")\n .toUpperCase();\n}\n\n/**\n * ConversationRow — inbox-style list row.\n *\n * Visual hierarchy:\n * [avatar] Title · 2h\n * last message preview (badge)\n *\n * On hover the kebab (3-dots) menu reveals so the user can pin/mute/archive\n * without opening the conversation first. Wires straight into\n * `useConversationActions` for archive/unarchive/pin/mute.\n */\nexport const ConversationRow = memo(function ConversationRow({\n conversation,\n isActive = false,\n onClick,\n onLeave,\n}: ConversationRowProps) {\n const {\n title,\n type,\n state,\n lastMessage,\n unreadCount,\n isPinned,\n isMuted,\n memberCount,\n updatedAt,\n avatar,\n } = conversation;\n\n const hasUnread = unreadCount > 0;\n const isArchived = state === \"ARCHIVED\";\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const value = t(key);\n return value === key ? fallback : value;\n };\n const timeLabels = {\n now: tr(\"conversations.now\", \"now\"),\n yesterday: tr(\"conversations.yesterday\", \"Yesterday\"),\n };\n\n // Track menu open so the kebab stays visible even when the cursor leaves\n // the row (otherwise the radix portal closes on every hover-out).\n const [menuOpen, setMenuOpen] = useState(false);\n\n const {\n archiveConversation,\n unarchiveConversation,\n pinConversation,\n unpinConversation,\n muteConversation,\n unmuteConversation,\n } = useConversationActions();\n\n // Stop click bubbling from the kebab into the row's click handler — opening\n // the menu must not also open the conversation.\n const stop = (event: React.MouseEvent | React.KeyboardEvent) => {\n event.stopPropagation();\n };\n\n return (\n <div\n data-tour=\"conversation-row\"\n className={cn(\n \"group relative flex w-full min-w-0 items-start gap-3 rounded-md px-3 py-2.5 transition-colors\",\n \"cursor-pointer select-none\",\n isActive\n ? \"bg-bg-sunken border-l-2 border-l-action-primary-bg\"\n : \"border-l-2 border-l-transparent hover:bg-action-ghost-bg-hover\",\n hasUnread && !isActive && \"bg-bg-elevated/40\",\n )}\n role=\"button\"\n tabIndex={0}\n onClick={() => onClick?.(conversation)}\n onKeyDown={(event) => {\n if (event.key === \"Enter\" || event.key === \" \") {\n event.preventDefault();\n onClick?.(conversation);\n }\n }}\n aria-label={\n tr(\"conversations.openConversation\", \"Open conversation\") + \": \" + title\n }\n >\n {/* Avatar / type icon */}\n <div\n className={cn(\n \"flex-shrink-0 flex h-9 w-9 items-center justify-center rounded-full text-xs font-semibold overflow-hidden\",\n type === \"DIRECT\" && \"bg-status-info-bg-subtle text-status-info-text\",\n type === \"CHANNEL\" &&\n \"bg-status-success-bg-subtle text-status-success-text\",\n (type === \"GROUP\" ||\n type === \"BROADCAST\" ||\n type === \"ANNOUNCEMENT\" ||\n type === \"BOT\" ||\n type === \"SUPPORT\") &&\n \"bg-bg-sunken text-text-primary\",\n )}\n aria-hidden=\"true\"\n >\n {avatar ? (\n <img src={avatar} alt=\"\" className=\"h-full w-full object-cover\" />\n ) : type === \"DIRECT\" ? (\n getInitials(title)\n ) : type === \"CHANNEL\" ? (\n <Hash size={16} />\n ) : (\n <ConversationTypeIcon type={type} size={16} />\n )}\n </div>\n\n {/* Body */}\n <div className=\"flex min-w-0 flex-1 flex-col gap-0.5\">\n <div className=\"flex items-center justify-between gap-2\">\n <div className=\"flex min-w-0 items-center gap-1.5\">\n <span\n className={cn(\n \"truncate text-sm\",\n hasUnread\n ? \"font-semibold text-text-primary\"\n : \"font-medium text-text-primary\",\n )}\n >\n {title}\n </span>\n {isPinned && (\n <Pin\n size={11}\n className=\"flex-shrink-0 text-text-secondary\"\n aria-label={tr(\"conversations.pinnedLabel\", \"Pinned\")}\n />\n )}\n {isMuted && (\n <VolumeX\n size={11}\n className=\"flex-shrink-0 text-text-secondary\"\n aria-label={tr(\"conversations.mutedLabel\", \"Muted\")}\n />\n )}\n </div>\n\n <span\n className={cn(\n \"flex-shrink-0 text-[11px]\",\n hasUnread\n ? \"font-medium text-action-primary-bg\"\n : \"text-text-secondary\",\n )}\n >\n {formatRelativeTime(\n lastMessage?.createdAt ?? updatedAt,\n timeLabels,\n )}\n </span>\n </div>\n\n <div className=\"flex items-center justify-between gap-2\">\n <p\n className={cn(\n \"min-w-0 truncate break-all text-xs\",\n hasUnread ? \"text-text-primary/80\" : \"text-text-secondary\",\n )}\n >\n {lastMessage ? (\n <>\n {lastMessage.sender.name && (\n <span className=\"font-medium\">\n {lastMessage.sender.name}\n {\": \"}\n </span>\n )}\n {lastMessage.content ||\n tr(\"conversations.sentAttachment\", \"Sent an attachment\")}\n </>\n ) : (\n <span className=\"italic\">\n {tr(\"conversations.noMessages\", \"No messages yet\")}\n </span>\n )}\n </p>\n\n <div className=\"flex flex-shrink-0 items-center gap-1.5\">\n {type !== \"DIRECT\" && (\n <span\n className=\"inline-flex items-center gap-0.5 text-[10px] text-text-secondary\"\n aria-label={`${memberCount} ${tr(\"conversations.membersLabel\", \"members\")}`}\n >\n <Users size={10} aria-hidden=\"true\" />\n {memberCount}\n </span>\n )}\n {hasUnread && (\n <span\n className=\"inline-flex h-[18px] min-w-[18px] items-center justify-center rounded-full bg-action-primary-bg px-1 text-[10px] font-bold text-action-primary-text 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,16 +1,22 @@
|
|
|
1
1
|
import { jsx as e } from "react/jsx-runtime";
|
|
2
|
-
import { EmptyState as t } from "@burdenoff/fe-libs/ui";
|
|
2
|
+
import { EmptyState as t, IllustratedEmptyState as n } from "@burdenoff/fe-libs/ui";
|
|
3
3
|
//#region src/conversations/components/EmptyState.tsx
|
|
4
|
-
function
|
|
5
|
-
return /* @__PURE__ */ e(
|
|
6
|
-
|
|
7
|
-
title:
|
|
8
|
-
description:
|
|
9
|
-
action:
|
|
10
|
-
className:
|
|
4
|
+
function r({ icon: r, title: i, description: a, action: o, illustration: s, className: c }) {
|
|
5
|
+
return s ? /* @__PURE__ */ e(n, {
|
|
6
|
+
illustration: s,
|
|
7
|
+
title: i,
|
|
8
|
+
description: a,
|
|
9
|
+
action: o,
|
|
10
|
+
className: c
|
|
11
|
+
}) : /* @__PURE__ */ e(t, {
|
|
12
|
+
icon: r,
|
|
13
|
+
title: i,
|
|
14
|
+
description: a,
|
|
15
|
+
action: o,
|
|
16
|
+
className: c
|
|
11
17
|
});
|
|
12
18
|
}
|
|
13
19
|
//#endregion
|
|
14
|
-
export {
|
|
20
|
+
export { r as EmptyState };
|
|
15
21
|
|
|
16
22
|
//# sourceMappingURL=EmptyState.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EmptyState.js","names":[],"sources":["../../../src/conversations/components/EmptyState.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport {
|
|
1
|
+
{"version":3,"file":"EmptyState.js","names":[],"sources":["../../../src/conversations/components/EmptyState.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport {\n EmptyState as CanonicalEmptyState,\n IllustratedEmptyState,\n} from \"@burdenoff/fe-libs/ui\";\n\n/**\n * Local wrapper around the canonical empty-state primitives. Keeps the existing\n * microfe-conversations call sites stable while routing all empty-state\n * rendering through fe-libs. When an `illustration` name is supplied the richer\n * Boff UI 2.0 `IllustratedEmptyState` (on-brand artwork) is rendered; otherwise\n * the compact icon-based `EmptyState` is used.\n */\nexport function EmptyState({\n icon,\n title,\n description,\n action,\n illustration,\n className,\n}: {\n icon?: ReactNode;\n title: string;\n description?: string;\n action?: ReactNode;\n illustration?: string;\n className?: string;\n}) {\n if (illustration) {\n return (\n <IllustratedEmptyState\n illustration={illustration}\n title={title}\n description={description}\n action={action}\n className={className}\n />\n );\n }\n return (\n <CanonicalEmptyState\n icon={icon}\n title={title}\n description={description}\n action={action}\n className={className}\n />\n );\n}\n"],"mappings":";;;AAaA,SAAgB,EAAW,EACzB,SACA,UACA,gBACA,WACA,iBACA,gBAQC;AAYD,QAXI,IAEA,kBAAC,GAAD;EACgB;EACP;EACM;EACL;EACG;EACX,CAAA,GAIJ,kBAAC,GAAD;EACQ;EACC;EACM;EACL;EACG;EACX,CAAA"}
|
|
@@ -6,71 +6,72 @@ import { useCallback as i, useMemo as a } from "react";
|
|
|
6
6
|
import { ArrowLeft as o, MessageSquarePlus as s, Search as c, X as l } from "lucide-react";
|
|
7
7
|
import { useLocation as u, useNavigate as d, useParams as f } from "react-router-dom";
|
|
8
8
|
import { jsx as p, jsxs as m } from "react/jsx-runtime";
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
9
|
+
import { AuroraBackground as h, Button as g, GradientText as _, IllustratedEmptyState as v } from "@burdenoff/fe-libs/ui";
|
|
10
|
+
import { Tabs as y } from "@burdenoff/fe-libs/chrome";
|
|
11
11
|
//#region src/conversations/components/InboxLayout.tsx
|
|
12
|
-
function
|
|
13
|
-
let
|
|
12
|
+
function b({ activeTab: v, onTabChange: b, archivedEnabled: S = !0, searchQuery: C, onSearchChange: w, filterChips: T, listSlot: E, detailSlot: D, emptyDetailSlot: O }) {
|
|
13
|
+
let k = d(), A = t(), { basePath: j } = e(), { setActiveConversationId: M } = r(), N = f(), P = u(), F = !!N.conversationId && !!D, I = a(() => {
|
|
14
14
|
let e = [
|
|
15
15
|
{
|
|
16
16
|
id: "all",
|
|
17
|
-
label:
|
|
17
|
+
label: A("conversations.tabs.all", "All")
|
|
18
18
|
},
|
|
19
19
|
{
|
|
20
20
|
id: "direct",
|
|
21
|
-
label:
|
|
21
|
+
label: A("conversations.tabs.direct", "Direct")
|
|
22
22
|
},
|
|
23
23
|
{
|
|
24
24
|
id: "groups",
|
|
25
|
-
label:
|
|
25
|
+
label: A("conversations.tabs.groups", "Groups")
|
|
26
26
|
},
|
|
27
27
|
{
|
|
28
28
|
id: "channels",
|
|
29
|
-
label:
|
|
29
|
+
label: A("conversations.tabs.channels", "Channels")
|
|
30
30
|
}
|
|
31
31
|
];
|
|
32
|
-
return
|
|
32
|
+
return S && e.push({
|
|
33
33
|
id: "archived",
|
|
34
|
-
label:
|
|
34
|
+
label: A("conversations.tabs.archived", "Archived")
|
|
35
35
|
}), e;
|
|
36
|
-
}, [
|
|
37
|
-
let e =
|
|
38
|
-
t.set("new", "true"),
|
|
36
|
+
}, [S, A]), L = i(() => {
|
|
37
|
+
let e = j || "/conversations", t = new URLSearchParams(P.search);
|
|
38
|
+
t.set("new", "true"), k(`${e}?${t.toString()}`);
|
|
39
39
|
}, [
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
]),
|
|
44
|
-
|
|
40
|
+
j,
|
|
41
|
+
P.search,
|
|
42
|
+
k
|
|
43
|
+
]), R = i(() => {
|
|
44
|
+
M(null), k(`${j || "/conversations"}${P.search}`);
|
|
45
45
|
}, [
|
|
46
|
+
j,
|
|
47
|
+
P.search,
|
|
46
48
|
k,
|
|
47
|
-
M
|
|
48
|
-
D,
|
|
49
|
-
A
|
|
49
|
+
M
|
|
50
50
|
]);
|
|
51
51
|
return /* @__PURE__ */ m("div", {
|
|
52
52
|
"data-tour": "conversations-inbox",
|
|
53
53
|
className: "flex h-full w-full min-h-0 min-w-0 overflow-hidden bg-bg-canvas",
|
|
54
54
|
children: [/* @__PURE__ */ m("aside", {
|
|
55
|
-
"aria-label":
|
|
56
|
-
className: n("flex flex-col min-h-0 border-r border-border-default bg-bg-surface", "w-full md:w-[320px] md:flex-shrink-0",
|
|
55
|
+
"aria-label": A("conversations.listPaneLabel", "Conversations list"),
|
|
56
|
+
className: n("flex flex-col min-h-0 border-r border-border-default bg-bg-surface", "w-full md:w-[320px] md:flex-shrink-0", F ? "hidden md:flex" : "flex"),
|
|
57
57
|
children: [/* @__PURE__ */ m("div", {
|
|
58
|
-
className: "sticky top-0 z-10 flex flex-col gap-3 border-b border-border-subtle bg-bg-surface px-4 pt-4 pb-3",
|
|
58
|
+
className: "relative isolate overflow-hidden sticky top-0 z-10 flex flex-col gap-3 border-b border-border-subtle bg-bg-surface px-4 pt-4 pb-3",
|
|
59
59
|
children: [
|
|
60
|
+
/* @__PURE__ */ p(h, { intensity: "subtle" }),
|
|
60
61
|
/* @__PURE__ */ m("div", {
|
|
61
62
|
className: "flex items-center justify-between gap-2",
|
|
62
63
|
children: [/* @__PURE__ */ p("h1", {
|
|
63
64
|
className: "text-base font-semibold text-text-primary",
|
|
64
|
-
children:
|
|
65
|
+
children: /* @__PURE__ */ p(_, { children: A("conversations.title", "Conversations") })
|
|
65
66
|
}), /* @__PURE__ */ m(g, {
|
|
66
67
|
"data-tour": "conversation-new-button",
|
|
67
68
|
type: "button",
|
|
68
69
|
size: "sm",
|
|
69
|
-
onClick:
|
|
70
|
-
"aria-label":
|
|
70
|
+
onClick: L,
|
|
71
|
+
"aria-label": A("conversations.createNewConversation", "Create a new conversation"),
|
|
71
72
|
children: [/* @__PURE__ */ p(s, { size: 14 }), /* @__PURE__ */ p("span", {
|
|
72
73
|
className: "hidden sm:inline",
|
|
73
|
-
children:
|
|
74
|
+
children: A("conversations.newConversation", "New")
|
|
74
75
|
})]
|
|
75
76
|
})]
|
|
76
77
|
}),
|
|
@@ -85,16 +86,16 @@ function _({ activeTab: _, onTabChange: y, archivedEnabled: b = !0, searchQuery:
|
|
|
85
86
|
/* @__PURE__ */ p("input", {
|
|
86
87
|
"data-tour": "conversation-search",
|
|
87
88
|
type: "search",
|
|
88
|
-
value:
|
|
89
|
-
onChange: (e) =>
|
|
90
|
-
placeholder:
|
|
91
|
-
"aria-label":
|
|
89
|
+
value: C,
|
|
90
|
+
onChange: (e) => w(e.target.value),
|
|
91
|
+
placeholder: A("conversations.search", "Search conversations..."),
|
|
92
|
+
"aria-label": A("conversations.searchAria", "Search conversations"),
|
|
92
93
|
className: n("w-full rounded-md border border-border-default bg-bg-elevated", "py-1.5 pl-8 pr-8 text-sm text-text-primary", "placeholder:text-text-secondary", "focus:border-action-primary-border focus:outline-none focus:ring-2 focus:ring-action-primary-border/40")
|
|
93
94
|
}),
|
|
94
|
-
|
|
95
|
+
C.length > 0 && /* @__PURE__ */ p("button", {
|
|
95
96
|
type: "button",
|
|
96
|
-
onClick: () =>
|
|
97
|
-
"aria-label":
|
|
97
|
+
onClick: () => w(""),
|
|
98
|
+
"aria-label": A("conversations.clearSearch", "Clear conversation search"),
|
|
98
99
|
className: "absolute right-2 top-1/2 -translate-y-1/2 rounded p-0.5 text-text-secondary hover:bg-action-ghost-bg-hover hover:text-text-primary",
|
|
99
100
|
children: /* @__PURE__ */ p(l, { size: 12 })
|
|
100
101
|
})
|
|
@@ -102,18 +103,18 @@ function _({ activeTab: _, onTabChange: y, archivedEnabled: b = !0, searchQuery:
|
|
|
102
103
|
}),
|
|
103
104
|
/* @__PURE__ */ p("div", {
|
|
104
105
|
"data-tour": "conversations-tabs",
|
|
105
|
-
children: /* @__PURE__ */ p(
|
|
106
|
-
items:
|
|
107
|
-
activeTabId:
|
|
108
|
-
onTabChange: (e) =>
|
|
106
|
+
children: /* @__PURE__ */ p(y, {
|
|
107
|
+
items: I,
|
|
108
|
+
activeTabId: v,
|
|
109
|
+
onTabChange: (e) => b(e),
|
|
109
110
|
variant: "underline",
|
|
110
111
|
size: "sm"
|
|
111
112
|
})
|
|
112
113
|
}),
|
|
113
|
-
|
|
114
|
+
T && T.length > 0 && /* @__PURE__ */ p("div", {
|
|
114
115
|
"data-tour": "conversations-filter-chips",
|
|
115
116
|
className: "flex flex-wrap gap-1.5",
|
|
116
|
-
children:
|
|
117
|
+
children: T.map((e) => /* @__PURE__ */ m("button", {
|
|
117
118
|
type: "button",
|
|
118
119
|
onClick: e.onToggle,
|
|
119
120
|
"aria-pressed": e.active,
|
|
@@ -125,65 +126,48 @@ function _({ activeTab: _, onTabChange: y, archivedEnabled: b = !0, searchQuery:
|
|
|
125
126
|
}), /* @__PURE__ */ p("div", {
|
|
126
127
|
"data-tour": "conversations-list-scroll",
|
|
127
128
|
className: "flex-1 min-h-0 overflow-y-auto overflow-x-hidden",
|
|
128
|
-
children:
|
|
129
|
+
children: E
|
|
129
130
|
})]
|
|
130
131
|
}), /* @__PURE__ */ m("section", {
|
|
131
|
-
"aria-label":
|
|
132
|
-
className: n("flex flex-col min-h-0 flex-1 min-w-0 bg-bg-surface",
|
|
133
|
-
children: [
|
|
132
|
+
"aria-label": A("conversations.detailPaneLabel", "Conversation detail"),
|
|
133
|
+
className: n("flex flex-col min-h-0 flex-1 min-w-0 bg-bg-surface", F ? "flex" : "hidden md:flex"),
|
|
134
|
+
children: [F && /* @__PURE__ */ p("div", {
|
|
134
135
|
className: "md:hidden border-b border-border-subtle px-3 py-2",
|
|
135
136
|
children: /* @__PURE__ */ m(g, {
|
|
136
137
|
variant: "ghost",
|
|
137
138
|
size: "sm",
|
|
138
|
-
onClick:
|
|
139
|
-
"aria-label":
|
|
140
|
-
children: [/* @__PURE__ */ p(o, { size: 14 }),
|
|
139
|
+
onClick: R,
|
|
140
|
+
"aria-label": A("conversations.backToInbox", "Back to conversations list"),
|
|
141
|
+
children: [/* @__PURE__ */ p(o, { size: 14 }), A("conversations.backToInbox", "Back to inbox")]
|
|
141
142
|
})
|
|
142
143
|
}), /* @__PURE__ */ p("div", {
|
|
143
144
|
className: "flex-1 min-h-0 min-w-0 overflow-y-auto overflow-x-hidden",
|
|
144
|
-
children:
|
|
145
|
-
onNew:
|
|
146
|
-
title:
|
|
147
|
-
description:
|
|
148
|
-
newLabel:
|
|
145
|
+
children: D ?? O ?? /* @__PURE__ */ p(x, {
|
|
146
|
+
onNew: L,
|
|
147
|
+
title: A("conversations.detailEmpty.title", "Select a conversation"),
|
|
148
|
+
description: A("conversations.detailEmpty.description", "Pick a conversation from the list — or start a new one."),
|
|
149
|
+
newLabel: A("conversations.detailEmpty.newCta", "Start a new conversation")
|
|
149
150
|
})
|
|
150
151
|
})]
|
|
151
152
|
})]
|
|
152
153
|
});
|
|
153
154
|
}
|
|
154
|
-
function
|
|
155
|
+
function x({ title: e, description: t, newLabel: n, onNew: r }) {
|
|
155
156
|
return /* @__PURE__ */ p("div", {
|
|
156
157
|
className: "flex h-full items-center justify-center p-8",
|
|
157
|
-
children: /* @__PURE__ */
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
})
|
|
167
|
-
}),
|
|
168
|
-
/* @__PURE__ */ p("h2", {
|
|
169
|
-
className: "text-lg font-semibold text-text-primary",
|
|
170
|
-
children: e
|
|
171
|
-
}),
|
|
172
|
-
/* @__PURE__ */ p("p", {
|
|
173
|
-
className: "text-sm text-text-secondary",
|
|
174
|
-
children: t
|
|
175
|
-
}),
|
|
176
|
-
/* @__PURE__ */ m(g, {
|
|
177
|
-
type: "button",
|
|
178
|
-
onClick: r,
|
|
179
|
-
className: "mt-2",
|
|
180
|
-
children: [/* @__PURE__ */ p(s, { size: 14 }), n]
|
|
181
|
-
})
|
|
182
|
-
]
|
|
158
|
+
children: /* @__PURE__ */ p(v, {
|
|
159
|
+
illustration: "empty-vibes",
|
|
160
|
+
title: e,
|
|
161
|
+
description: t,
|
|
162
|
+
action: /* @__PURE__ */ m(g, {
|
|
163
|
+
type: "button",
|
|
164
|
+
onClick: r,
|
|
165
|
+
children: [/* @__PURE__ */ p(s, { size: 14 }), n]
|
|
166
|
+
})
|
|
183
167
|
})
|
|
184
168
|
});
|
|
185
169
|
}
|
|
186
170
|
//#endregion
|
|
187
|
-
export {
|
|
171
|
+
export { b as InboxLayout };
|
|
188
172
|
|
|
189
173
|
//# sourceMappingURL=InboxLayout.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"InboxLayout.js","names":[],"sources":["../../../src/conversations/components/InboxLayout.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport { useCallback, useMemo } from \"react\";\nimport { useLocation, useNavigate, useParams } from \"react-router-dom\";\nimport { Tabs, type TabItem } from \"@burdenoff/fe-libs/chrome\";\nimport { Button } from \"@burdenoff/fe-libs/ui\";\nimport { ArrowLeft, MessageSquarePlus, Search, X } from \"lucide-react\";\nimport { cn } from \"../utils/cn\";\nimport { useConversations } from \"../providers/ConversationsProvider\";\nimport { useConversationsStore } from \"../store/conversationsStore\";\nimport { useTr } from \"../utils/i18n\";\n\n/**\n * Identifier for each inbox tab. Drives the conversation list filter and is\n * mirrored to / read from the `?tab=` URL search param.\n */\nexport type InboxTabId = \"all\" | \"direct\" | \"groups\" | \"channels\" | \"archived\";\n\nexport const INBOX_TAB_IDS: InboxTabId[] = [\n \"all\",\n \"direct\",\n \"groups\",\n \"channels\",\n \"archived\",\n];\n\nexport interface FilterChipDescriptor {\n id: \"unread\" | \"pinned\";\n label: string;\n active: boolean;\n onToggle: () => void;\n icon?: ReactNode;\n}\n\nexport interface InboxLayoutProps {\n /**\n * Currently active inbox tab. Drives which list view is rendered in the\n * left pane.\n */\n activeTab: InboxTabId;\n /** Tab change handler. Used by the host page to sync URL state. */\n onTabChange: (id: InboxTabId) => void;\n /**\n * Whether the Archived tab is exposed. Gated behind the `conversations.archived-view`\n * feature flag — when false the tab is hidden from the bar entirely.\n */\n archivedEnabled?: boolean;\n /** Search input value (controlled). */\n searchQuery: string;\n /** Search input change handler. */\n onSearchChange: (value: string) => void;\n /**\n * Quick toggleable filter chips rendered below the tabs. Pass an empty\n * array (or omit) to hide the chip row entirely — useful on the Archived\n * tab where pin/unread filters don't make sense.\n */\n filterChips?: FilterChipDescriptor[];\n /** Scrollable list content rendered in the left pane below the chrome. */\n listSlot: ReactNode;\n /**\n * Right-pane content. When a conversation is selected the parent should\n * pass the conversation detail subtree here. When nothing is selected the\n * empty-state slot below is used instead.\n */\n detailSlot?: ReactNode;\n /**\n * Empty-state rendered in the right pane when `detailSlot` is null /\n * undefined. Defaults to a generic \"Select a conversation\" prompt.\n */\n emptyDetailSlot?: ReactNode;\n}\n\n/**\n * Two-pane inbox shell for the conversations microfrontend.\n *\n * Left pane: fixed 320px (collapses to full-width below `md` when no\n * conversation is selected, the way Slack / Linear / Discord behave on\n * mobile). Holds:\n * - sticky header (title + New CTA)\n * - search input\n * - tabs (All / Direct / Groups / Channels / Archived)\n * - filter chips (Unread / Pinned)\n * - scrollable conversation list\n *\n * Right pane: detail subtree OR empty state.\n *\n * The component is presentation-only — URL <-> tab sync, query\n * construction, and conversation selection all live in the host page.\n */\nexport function InboxLayout({\n activeTab,\n onTabChange,\n archivedEnabled = true,\n searchQuery,\n onSearchChange,\n filterChips,\n listSlot,\n detailSlot,\n emptyDetailSlot,\n}: InboxLayoutProps) {\n const navigate = useNavigate();\n const tr = useTr();\n const { basePath } = useConversations();\n const { setActiveConversationId } = useConversationsStore();\n const params = useParams<{ conversationId?: string }>();\n const location = useLocation();\n // A conversation is \"open\" whenever the URL targets a detail route\n // (/:conversationId). We can't rely solely on a passed detailSlot —\n // routing decides whether to mount the detail page.\n const conversationOpen =\n Boolean(params.conversationId) && Boolean(detailSlot);\n\n const tabs: TabItem[] = useMemo(() => {\n const items: TabItem[] = [\n { id: \"all\", label: tr(\"conversations.tabs.all\", \"All\") },\n { id: \"direct\", label: tr(\"conversations.tabs.direct\", \"Direct\") },\n { id: \"groups\", label: tr(\"conversations.tabs.groups\", \"Groups\") },\n { id: \"channels\", label: tr(\"conversations.tabs.channels\", \"Channels\") },\n ];\n if (archivedEnabled) {\n items.push({\n id: \"archived\",\n label: tr(\"conversations.tabs.archived\", \"Archived\"),\n });\n }\n return items;\n }, [archivedEnabled, tr]);\n\n const handleNewConversation = useCallback(() => {\n // Open the create-conversation dialog in-place via `?new=true` on the\n // inbox landing page (parent route). This replaces the legacy\n // `/conversations/new` full-page navigation — the user stays inside\n // the inbox and the dialog is mounted there.\n const inboxRoot = basePath || \"/conversations\";\n const currentSearch = new URLSearchParams(location.search);\n currentSearch.set(\"new\", \"true\");\n navigate(`${inboxRoot}?${currentSearch.toString()}`);\n }, [basePath, location.search, navigate]);\n\n const handleBackToList = useCallback(() => {\n // Clear the open conversation id so the empty state shows and routing\n // returns to the inbox shell. We keep the current tab via location.search.\n setActiveConversationId(null);\n navigate(`${basePath || \"/conversations\"}${location.search}`);\n }, [basePath, location.search, navigate, setActiveConversationId]);\n\n return (\n <div\n data-tour=\"conversations-inbox\"\n className=\"flex h-full w-full min-h-0 min-w-0 overflow-hidden bg-bg-canvas\"\n >\n {/* Left pane: list. On md+ always visible as a fixed sidebar. Below md,\n hidden when a conversation is open (full-screen detail mode). */}\n <aside\n aria-label={tr(\"conversations.listPaneLabel\", \"Conversations list\")}\n className={cn(\n \"flex flex-col min-h-0 border-r border-border-default bg-bg-surface\",\n \"w-full md:w-[320px] md:flex-shrink-0\",\n conversationOpen ? \"hidden md:flex\" : \"flex\",\n )}\n >\n {/* Sticky header: title + new CTA */}\n <div className=\"sticky top-0 z-10 flex flex-col gap-3 border-b border-border-subtle bg-bg-surface px-4 pt-4 pb-3\">\n <div className=\"flex items-center justify-between gap-2\">\n <h1 className=\"text-base font-semibold text-text-primary\">\n {tr(\"conversations.title\", \"Conversations\")}\n </h1>\n <Button\n data-tour=\"conversation-new-button\"\n type=\"button\"\n size=\"sm\"\n onClick={handleNewConversation}\n aria-label={tr(\n \"conversations.createNewConversation\",\n \"Create a new conversation\",\n )}\n >\n <MessageSquarePlus size={14} />\n <span className=\"hidden sm:inline\">\n {tr(\"conversations.newConversation\", \"New\")}\n </span>\n </Button>\n </div>\n\n {/* Search */}\n <div className=\"relative\">\n <Search\n size={14}\n className=\"pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-text-secondary\"\n aria-hidden=\"true\"\n />\n <input\n data-tour=\"conversation-search\"\n type=\"search\"\n value={searchQuery}\n onChange={(event) => onSearchChange(event.target.value)}\n placeholder={tr(\n \"conversations.search\",\n \"Search conversations...\",\n )}\n aria-label={tr(\n \"conversations.searchAria\",\n \"Search conversations\",\n )}\n className={cn(\n \"w-full rounded-md border border-border-default bg-bg-elevated\",\n \"py-1.5 pl-8 pr-8 text-sm text-text-primary\",\n \"placeholder:text-text-secondary\",\n \"focus:border-action-primary-border focus:outline-none focus:ring-2 focus:ring-action-primary-border/40\",\n )}\n />\n {searchQuery.length > 0 && (\n <button\n type=\"button\"\n onClick={() => onSearchChange(\"\")}\n aria-label={tr(\n \"conversations.clearSearch\",\n \"Clear conversation search\",\n )}\n className=\"absolute right-2 top-1/2 -translate-y-1/2 rounded p-0.5 text-text-secondary hover:bg-action-ghost-bg-hover hover:text-text-primary\"\n >\n <X size={12} />\n </button>\n )}\n </div>\n\n {/* Tabs */}\n <div data-tour=\"conversations-tabs\">\n <Tabs\n items={tabs}\n activeTabId={activeTab}\n onTabChange={(id: string) => onTabChange(id as InboxTabId)}\n variant=\"underline\"\n size=\"sm\"\n />\n </div>\n\n {/* Filter chips */}\n {filterChips && filterChips.length > 0 && (\n <div\n data-tour=\"conversations-filter-chips\"\n className=\"flex flex-wrap gap-1.5\"\n >\n {filterChips.map((chip) => (\n <button\n key={chip.id}\n type=\"button\"\n onClick={chip.onToggle}\n aria-pressed={chip.active}\n className={cn(\n \"inline-flex items-center gap-1 rounded-full border px-2.5 py-0.5 text-xs font-medium transition-colors\",\n chip.active\n ? \"border-action-primary-border bg-action-primary-bg/10 text-action-primary-bg\"\n : \"border-border-default text-text-secondary hover:bg-action-ghost-bg-hover hover:text-text-primary\",\n )}\n >\n {chip.icon}\n {chip.label}\n </button>\n ))}\n </div>\n )}\n </div>\n\n {/* Scrollable list */}\n <div\n data-tour=\"conversations-list-scroll\"\n className=\"flex-1 min-h-0 overflow-y-auto overflow-x-hidden\"\n >\n {listSlot}\n </div>\n </aside>\n\n {/* Right pane: detail OR empty state */}\n <section\n aria-label={tr(\"conversations.detailPaneLabel\", \"Conversation detail\")}\n className={cn(\n \"flex flex-col min-h-0 flex-1 min-w-0 bg-bg-surface\",\n conversationOpen ? \"flex\" : \"hidden md:flex\",\n )}\n >\n {conversationOpen && (\n // Mobile-only back button. On md+ the list and detail render\n // side-by-side so the back arrow would be useless.\n <div className=\"md:hidden border-b border-border-subtle px-3 py-2\">\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={handleBackToList}\n aria-label={tr(\n \"conversations.backToInbox\",\n \"Back to conversations list\",\n )}\n >\n <ArrowLeft size={14} />\n {tr(\"conversations.backToInbox\", \"Back to inbox\")}\n </Button>\n </div>\n )}\n <div className=\"flex-1 min-h-0 min-w-0 overflow-y-auto overflow-x-hidden\">\n {detailSlot ?? emptyDetailSlot ?? (\n <InboxEmptyDetail\n onNew={handleNewConversation}\n title={tr(\n \"conversations.detailEmpty.title\",\n \"Select a conversation\",\n )}\n description={tr(\n \"conversations.detailEmpty.description\",\n \"Pick a conversation from the list — or start a new one.\",\n )}\n newLabel={tr(\n \"conversations.detailEmpty.newCta\",\n \"Start a new conversation\",\n )}\n />\n )}\n </div>\n </section>\n </div>\n );\n}\n\ninterface InboxEmptyDetailProps {\n title: string;\n description: string;\n newLabel: string;\n onNew: () => void;\n}\n\n/**\n * Default empty-state rendered in the detail pane when no conversation is\n * currently selected. Lifted out so the host page (or tests) can override\n * the copy via `emptyDetailSlot`.\n */\nfunction InboxEmptyDetail({\n title,\n description,\n newLabel,\n onNew,\n}: InboxEmptyDetailProps) {\n return (\n <div className=\"flex h-full items-center justify-center p-8\">\n <div className=\"flex max-w-sm flex-col items-center gap-3 text-center\">\n <div className=\"flex size-16 items-center justify-center rounded-full bg-bg-sunken\">\n <MessageSquarePlus\n size={28}\n className=\"text-text-secondary\"\n aria-hidden=\"true\"\n />\n </div>\n <h2 className=\"text-lg font-semibold text-text-primary\">{title}</h2>\n <p className=\"text-sm text-text-secondary\">{description}</p>\n <Button type=\"button\" onClick={onNew} className=\"mt-2\">\n <MessageSquarePlus size={14} />\n {newLabel}\n </Button>\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"InboxLayout.js","names":[],"sources":["../../../src/conversations/components/InboxLayout.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport { useCallback, useMemo } from \"react\";\nimport { useLocation, useNavigate, useParams } from \"react-router-dom\";\nimport { Tabs, type TabItem } from \"@burdenoff/fe-libs/chrome\";\nimport {\n Button,\n AuroraBackground,\n GradientText,\n IllustratedEmptyState,\n} from \"@burdenoff/fe-libs/ui\";\nimport { ArrowLeft, MessageSquarePlus, Search, X } from \"lucide-react\";\nimport { cn } from \"../utils/cn\";\nimport { useConversations } from \"../providers/ConversationsProvider\";\nimport { useConversationsStore } from \"../store/conversationsStore\";\nimport { useTr } from \"../utils/i18n\";\n\n/**\n * Identifier for each inbox tab. Drives the conversation list filter and is\n * mirrored to / read from the `?tab=` URL search param.\n */\nexport type InboxTabId = \"all\" | \"direct\" | \"groups\" | \"channels\" | \"archived\";\n\nexport const INBOX_TAB_IDS: InboxTabId[] = [\n \"all\",\n \"direct\",\n \"groups\",\n \"channels\",\n \"archived\",\n];\n\nexport interface FilterChipDescriptor {\n id: \"unread\" | \"pinned\";\n label: string;\n active: boolean;\n onToggle: () => void;\n icon?: ReactNode;\n}\n\nexport interface InboxLayoutProps {\n /**\n * Currently active inbox tab. Drives which list view is rendered in the\n * left pane.\n */\n activeTab: InboxTabId;\n /** Tab change handler. Used by the host page to sync URL state. */\n onTabChange: (id: InboxTabId) => void;\n /**\n * Whether the Archived tab is exposed. Gated behind the `conversations.archived-view`\n * feature flag — when false the tab is hidden from the bar entirely.\n */\n archivedEnabled?: boolean;\n /** Search input value (controlled). */\n searchQuery: string;\n /** Search input change handler. */\n onSearchChange: (value: string) => void;\n /**\n * Quick toggleable filter chips rendered below the tabs. Pass an empty\n * array (or omit) to hide the chip row entirely — useful on the Archived\n * tab where pin/unread filters don't make sense.\n */\n filterChips?: FilterChipDescriptor[];\n /** Scrollable list content rendered in the left pane below the chrome. */\n listSlot: ReactNode;\n /**\n * Right-pane content. When a conversation is selected the parent should\n * pass the conversation detail subtree here. When nothing is selected the\n * empty-state slot below is used instead.\n */\n detailSlot?: ReactNode;\n /**\n * Empty-state rendered in the right pane when `detailSlot` is null /\n * undefined. Defaults to a generic \"Select a conversation\" prompt.\n */\n emptyDetailSlot?: ReactNode;\n}\n\n/**\n * Two-pane inbox shell for the conversations microfrontend.\n *\n * Left pane: fixed 320px (collapses to full-width below `md` when no\n * conversation is selected, the way Slack / Linear / Discord behave on\n * mobile). Holds:\n * - sticky header (title + New CTA)\n * - search input\n * - tabs (All / Direct / Groups / Channels / Archived)\n * - filter chips (Unread / Pinned)\n * - scrollable conversation list\n *\n * Right pane: detail subtree OR empty state.\n *\n * The component is presentation-only — URL <-> tab sync, query\n * construction, and conversation selection all live in the host page.\n */\nexport function InboxLayout({\n activeTab,\n onTabChange,\n archivedEnabled = true,\n searchQuery,\n onSearchChange,\n filterChips,\n listSlot,\n detailSlot,\n emptyDetailSlot,\n}: InboxLayoutProps) {\n const navigate = useNavigate();\n const tr = useTr();\n const { basePath } = useConversations();\n const { setActiveConversationId } = useConversationsStore();\n const params = useParams<{ conversationId?: string }>();\n const location = useLocation();\n // A conversation is \"open\" whenever the URL targets a detail route\n // (/:conversationId). We can't rely solely on a passed detailSlot —\n // routing decides whether to mount the detail page.\n const conversationOpen =\n Boolean(params.conversationId) && Boolean(detailSlot);\n\n const tabs: TabItem[] = useMemo(() => {\n const items: TabItem[] = [\n { id: \"all\", label: tr(\"conversations.tabs.all\", \"All\") },\n { id: \"direct\", label: tr(\"conversations.tabs.direct\", \"Direct\") },\n { id: \"groups\", label: tr(\"conversations.tabs.groups\", \"Groups\") },\n { id: \"channels\", label: tr(\"conversations.tabs.channels\", \"Channels\") },\n ];\n if (archivedEnabled) {\n items.push({\n id: \"archived\",\n label: tr(\"conversations.tabs.archived\", \"Archived\"),\n });\n }\n return items;\n }, [archivedEnabled, tr]);\n\n const handleNewConversation = useCallback(() => {\n // Open the create-conversation dialog in-place via `?new=true` on the\n // inbox landing page (parent route). This replaces the legacy\n // `/conversations/new` full-page navigation — the user stays inside\n // the inbox and the dialog is mounted there.\n const inboxRoot = basePath || \"/conversations\";\n const currentSearch = new URLSearchParams(location.search);\n currentSearch.set(\"new\", \"true\");\n navigate(`${inboxRoot}?${currentSearch.toString()}`);\n }, [basePath, location.search, navigate]);\n\n const handleBackToList = useCallback(() => {\n // Clear the open conversation id so the empty state shows and routing\n // returns to the inbox shell. We keep the current tab via location.search.\n setActiveConversationId(null);\n navigate(`${basePath || \"/conversations\"}${location.search}`);\n }, [basePath, location.search, navigate, setActiveConversationId]);\n\n return (\n <div\n data-tour=\"conversations-inbox\"\n className=\"flex h-full w-full min-h-0 min-w-0 overflow-hidden bg-bg-canvas\"\n >\n {/* Left pane: list. On md+ always visible as a fixed sidebar. Below md,\n hidden when a conversation is open (full-screen detail mode). */}\n <aside\n aria-label={tr(\"conversations.listPaneLabel\", \"Conversations list\")}\n className={cn(\n \"flex flex-col min-h-0 border-r border-border-default bg-bg-surface\",\n \"w-full md:w-[320px] md:flex-shrink-0\",\n conversationOpen ? \"hidden md:flex\" : \"flex\",\n )}\n >\n {/* Sticky header: title + new CTA. Hosts the MFE's single aurora hero\n backdrop (decorative, -z-10 — never moves content). */}\n <div className=\"relative isolate overflow-hidden sticky top-0 z-10 flex flex-col gap-3 border-b border-border-subtle bg-bg-surface px-4 pt-4 pb-3\">\n <AuroraBackground intensity=\"subtle\" />\n <div className=\"flex items-center justify-between gap-2\">\n <h1 className=\"text-base font-semibold text-text-primary\">\n <GradientText>\n {tr(\"conversations.title\", \"Conversations\")}\n </GradientText>\n </h1>\n <Button\n data-tour=\"conversation-new-button\"\n type=\"button\"\n size=\"sm\"\n onClick={handleNewConversation}\n aria-label={tr(\n \"conversations.createNewConversation\",\n \"Create a new conversation\",\n )}\n >\n <MessageSquarePlus size={14} />\n <span className=\"hidden sm:inline\">\n {tr(\"conversations.newConversation\", \"New\")}\n </span>\n </Button>\n </div>\n\n {/* Search */}\n <div className=\"relative\">\n <Search\n size={14}\n className=\"pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-text-secondary\"\n aria-hidden=\"true\"\n />\n <input\n data-tour=\"conversation-search\"\n type=\"search\"\n value={searchQuery}\n onChange={(event) => onSearchChange(event.target.value)}\n placeholder={tr(\n \"conversations.search\",\n \"Search conversations...\",\n )}\n aria-label={tr(\n \"conversations.searchAria\",\n \"Search conversations\",\n )}\n className={cn(\n \"w-full rounded-md border border-border-default bg-bg-elevated\",\n \"py-1.5 pl-8 pr-8 text-sm text-text-primary\",\n \"placeholder:text-text-secondary\",\n \"focus:border-action-primary-border focus:outline-none focus:ring-2 focus:ring-action-primary-border/40\",\n )}\n />\n {searchQuery.length > 0 && (\n <button\n type=\"button\"\n onClick={() => onSearchChange(\"\")}\n aria-label={tr(\n \"conversations.clearSearch\",\n \"Clear conversation search\",\n )}\n className=\"absolute right-2 top-1/2 -translate-y-1/2 rounded p-0.5 text-text-secondary hover:bg-action-ghost-bg-hover hover:text-text-primary\"\n >\n <X size={12} />\n </button>\n )}\n </div>\n\n {/* Tabs */}\n <div data-tour=\"conversations-tabs\">\n <Tabs\n items={tabs}\n activeTabId={activeTab}\n onTabChange={(id: string) => onTabChange(id as InboxTabId)}\n variant=\"underline\"\n size=\"sm\"\n />\n </div>\n\n {/* Filter chips */}\n {filterChips && filterChips.length > 0 && (\n <div\n data-tour=\"conversations-filter-chips\"\n className=\"flex flex-wrap gap-1.5\"\n >\n {filterChips.map((chip) => (\n <button\n key={chip.id}\n type=\"button\"\n onClick={chip.onToggle}\n aria-pressed={chip.active}\n className={cn(\n \"inline-flex items-center gap-1 rounded-full border px-2.5 py-0.5 text-xs font-medium transition-colors\",\n chip.active\n ? \"border-action-primary-border bg-action-primary-bg/10 text-action-primary-bg\"\n : \"border-border-default text-text-secondary hover:bg-action-ghost-bg-hover hover:text-text-primary\",\n )}\n >\n {chip.icon}\n {chip.label}\n </button>\n ))}\n </div>\n )}\n </div>\n\n {/* Scrollable list */}\n <div\n data-tour=\"conversations-list-scroll\"\n className=\"flex-1 min-h-0 overflow-y-auto overflow-x-hidden\"\n >\n {listSlot}\n </div>\n </aside>\n\n {/* Right pane: detail OR empty state */}\n <section\n aria-label={tr(\"conversations.detailPaneLabel\", \"Conversation detail\")}\n className={cn(\n \"flex flex-col min-h-0 flex-1 min-w-0 bg-bg-surface\",\n conversationOpen ? \"flex\" : \"hidden md:flex\",\n )}\n >\n {conversationOpen && (\n // Mobile-only back button. On md+ the list and detail render\n // side-by-side so the back arrow would be useless.\n <div className=\"md:hidden border-b border-border-subtle px-3 py-2\">\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={handleBackToList}\n aria-label={tr(\n \"conversations.backToInbox\",\n \"Back to conversations list\",\n )}\n >\n <ArrowLeft size={14} />\n {tr(\"conversations.backToInbox\", \"Back to inbox\")}\n </Button>\n </div>\n )}\n <div className=\"flex-1 min-h-0 min-w-0 overflow-y-auto overflow-x-hidden\">\n {detailSlot ?? emptyDetailSlot ?? (\n <InboxEmptyDetail\n onNew={handleNewConversation}\n title={tr(\n \"conversations.detailEmpty.title\",\n \"Select a conversation\",\n )}\n description={tr(\n \"conversations.detailEmpty.description\",\n \"Pick a conversation from the list — or start a new one.\",\n )}\n newLabel={tr(\n \"conversations.detailEmpty.newCta\",\n \"Start a new conversation\",\n )}\n />\n )}\n </div>\n </section>\n </div>\n );\n}\n\ninterface InboxEmptyDetailProps {\n title: string;\n description: string;\n newLabel: string;\n onNew: () => void;\n}\n\n/**\n * Default empty-state rendered in the detail pane when no conversation is\n * currently selected. Lifted out so the host page (or tests) can override\n * the copy via `emptyDetailSlot`.\n */\nfunction InboxEmptyDetail({\n title,\n description,\n newLabel,\n onNew,\n}: InboxEmptyDetailProps) {\n return (\n <div className=\"flex h-full items-center justify-center p-8\">\n <IllustratedEmptyState\n illustration=\"empty-vibes\"\n title={title}\n description={description}\n action={\n <Button type=\"button\" onClick={onNew}>\n <MessageSquarePlus size={14} />\n {newLabel}\n </Button>\n }\n />\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -3,8 +3,9 @@ import { useMemo as t } from "react";
|
|
|
3
3
|
import { Activity as n, ChevronRight as r, ListChecks as i, MessageCircle as a, Package as o, ShieldCheck as s } from "lucide-react";
|
|
4
4
|
import { Link as c } from "react-router-dom";
|
|
5
5
|
import { jsx as l, jsxs as u } from "react/jsx-runtime";
|
|
6
|
+
import { EmphasisPanel as d, GlassCard as f, GradientText as p } from "@burdenoff/fe-libs/ui";
|
|
6
7
|
//#region src/conversations/pages/AdminConversationsLandingPage.tsx
|
|
7
|
-
var
|
|
8
|
+
var m = [
|
|
8
9
|
{
|
|
9
10
|
id: "moderation",
|
|
10
11
|
labelKey: "conversations.admin.moderation",
|
|
@@ -15,7 +16,8 @@ var d = [
|
|
|
15
16
|
icon: /* @__PURE__ */ l(s, {
|
|
16
17
|
className: "size-5",
|
|
17
18
|
"aria-hidden": "true"
|
|
18
|
-
})
|
|
19
|
+
}),
|
|
20
|
+
featured: !0
|
|
19
21
|
},
|
|
20
22
|
{
|
|
21
23
|
id: "bulk",
|
|
@@ -54,64 +56,72 @@ var d = [
|
|
|
54
56
|
})
|
|
55
57
|
}
|
|
56
58
|
];
|
|
57
|
-
function
|
|
58
|
-
let n = e(), i = t(() =>
|
|
59
|
+
function h() {
|
|
60
|
+
let n = e(), i = t(() => m.map((e) => ({
|
|
59
61
|
...e,
|
|
60
62
|
label: n(e.labelKey, e.labelFallback),
|
|
61
63
|
description: n(e.descriptionKey, e.descriptionFallback)
|
|
62
64
|
})), [n]);
|
|
63
|
-
return /* @__PURE__ */
|
|
65
|
+
return /* @__PURE__ */ l("div", {
|
|
64
66
|
className: "flex flex-col h-full",
|
|
65
|
-
children:
|
|
66
|
-
className: "
|
|
67
|
+
children: /* @__PURE__ */ l("div", {
|
|
68
|
+
className: "flex-1 min-h-0 overflow-auto p-4 sm:p-6",
|
|
67
69
|
children: /* @__PURE__ */ u("div", {
|
|
68
|
-
className: "
|
|
69
|
-
children: [/* @__PURE__ */ l("div", {
|
|
70
|
-
className: "
|
|
71
|
-
children: /* @__PURE__ */ l(a, {
|
|
72
|
-
className: "size-5 text-text-secondary",
|
|
73
|
-
"aria-hidden": "true"
|
|
74
|
-
})
|
|
75
|
-
}), /* @__PURE__ */ u("div", { children: [/* @__PURE__ */ l("h1", {
|
|
76
|
-
className: "text-lg font-semibold text-text-primary",
|
|
77
|
-
children: n("conversations.admin.landingTitle", "Conversations admin")
|
|
78
|
-
}), /* @__PURE__ */ l("p", {
|
|
79
|
-
className: "text-sm text-text-secondary",
|
|
80
|
-
children: n("conversations.admin.landingDescription", "Governance tools for workspace conversations. Select a section below to continue.")
|
|
81
|
-
})] })]
|
|
82
|
-
})
|
|
83
|
-
}), /* @__PURE__ */ l("div", {
|
|
84
|
-
className: "flex-1 min-h-0 overflow-auto p-6",
|
|
85
|
-
children: /* @__PURE__ */ l("div", {
|
|
86
|
-
className: "grid gap-3 sm:grid-cols-2 max-w-3xl",
|
|
87
|
-
children: i.map((e) => /* @__PURE__ */ u(c, {
|
|
88
|
-
to: e.path,
|
|
89
|
-
className: "group flex items-start gap-3 rounded-lg border border-border-subtle bg-bg-surface p-4 transition-colors hover:border-border-subtle hover:bg-accent",
|
|
70
|
+
className: "mx-auto w-full max-w-3xl space-y-6",
|
|
71
|
+
children: [/* @__PURE__ */ l(d, { children: /* @__PURE__ */ u("div", {
|
|
72
|
+
className: "flex items-start gap-3",
|
|
90
73
|
children: [/* @__PURE__ */ l("div", {
|
|
91
|
-
className: "p-2 rounded-
|
|
92
|
-
children:
|
|
74
|
+
className: "p-2 rounded-lg bg-gloss-card text-text-secondary border border-border-seam",
|
|
75
|
+
children: /* @__PURE__ */ l(a, {
|
|
76
|
+
className: "size-5",
|
|
77
|
+
"aria-hidden": "true"
|
|
78
|
+
})
|
|
93
79
|
}), /* @__PURE__ */ u("div", {
|
|
94
|
-
className: "
|
|
95
|
-
children: [/* @__PURE__ */
|
|
96
|
-
className: "
|
|
97
|
-
children:
|
|
98
|
-
className: "text-sm font-medium text-text-primary",
|
|
99
|
-
children: e.label
|
|
100
|
-
}), /* @__PURE__ */ l(r, {
|
|
101
|
-
className: "size-4 text-text-secondary",
|
|
102
|
-
"aria-hidden": "true"
|
|
103
|
-
})]
|
|
80
|
+
className: "min-w-0",
|
|
81
|
+
children: [/* @__PURE__ */ l("h1", {
|
|
82
|
+
className: "text-lg font-semibold text-text-primary",
|
|
83
|
+
children: /* @__PURE__ */ l(p, { children: n("conversations.admin.landingTitle", "Conversations admin") })
|
|
104
84
|
}), /* @__PURE__ */ l("p", {
|
|
105
|
-
className: "mt-1 text-
|
|
106
|
-
children:
|
|
85
|
+
className: "mt-1 text-sm text-text-secondary",
|
|
86
|
+
children: n("conversations.admin.landingDescription", "Governance tools for workspace conversations. Select a section below to continue.")
|
|
107
87
|
})]
|
|
108
88
|
})]
|
|
109
|
-
},
|
|
89
|
+
}) }), /* @__PURE__ */ l("div", {
|
|
90
|
+
className: "grid gap-3 sm:grid-cols-2",
|
|
91
|
+
children: i.map((e) => /* @__PURE__ */ l(c, {
|
|
92
|
+
to: e.path,
|
|
93
|
+
className: "group rounded-xl focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]",
|
|
94
|
+
children: /* @__PURE__ */ u(f, {
|
|
95
|
+
treatment: "glass",
|
|
96
|
+
glow: e.featured,
|
|
97
|
+
className: "flex items-start gap-3 border-border-seam p-4 shadow-[var(--shadow-pop)]",
|
|
98
|
+
children: [/* @__PURE__ */ l("div", {
|
|
99
|
+
className: "p-2 rounded-md bg-bg-sunken text-text-secondary group-hover:text-text-primary",
|
|
100
|
+
children: e.icon
|
|
101
|
+
}), /* @__PURE__ */ u("div", {
|
|
102
|
+
className: "flex-1 min-w-0",
|
|
103
|
+
children: [/* @__PURE__ */ u("div", {
|
|
104
|
+
className: "flex items-center justify-between gap-2",
|
|
105
|
+
children: [/* @__PURE__ */ l("span", {
|
|
106
|
+
className: "text-sm font-medium text-text-primary",
|
|
107
|
+
children: e.label
|
|
108
|
+
}), /* @__PURE__ */ l(r, {
|
|
109
|
+
className: "size-4 text-text-secondary",
|
|
110
|
+
"aria-hidden": "true"
|
|
111
|
+
})]
|
|
112
|
+
}), /* @__PURE__ */ l("p", {
|
|
113
|
+
className: "mt-1 text-xs text-text-secondary",
|
|
114
|
+
children: e.description
|
|
115
|
+
})]
|
|
116
|
+
})]
|
|
117
|
+
})
|
|
118
|
+
}, e.id))
|
|
119
|
+
})]
|
|
110
120
|
})
|
|
111
|
-
})
|
|
121
|
+
})
|
|
112
122
|
});
|
|
113
123
|
}
|
|
114
124
|
//#endregion
|
|
115
|
-
export {
|
|
125
|
+
export { h as AdminConversationsLandingPage };
|
|
116
126
|
|
|
117
127
|
//# sourceMappingURL=AdminConversationsLandingPage.js.map
|