diff --git a/packages/chat-ui/src/components/layouts.tsx b/packages/chat-ui/src/components/layouts.tsx new file mode 100644 index 0000000..a341af5 --- /dev/null +++ b/packages/chat-ui/src/components/layouts.tsx @@ -0,0 +1,805 @@ +import * as React from "react" +import { cn } from "@xtablo/shared" +import { + MessageSquare, + Search, + Phone, + X, + ChevronLeft, + Plus, + Minimize2, + Pin, + Ticket, + Clock, + AlertCircle, + CheckCircle2, + Circle, + User, + Tag, +} from "lucide-react" +import type { ChatMessageData, ChatUser, TypingUser } from "../types" +import { ChatProvider, ChatMessages, ChatComposer } from "./chat" + +// ─── Shared: ChatHeader ─────────────────────────────────────────────────────── + +interface ChatHeaderProps { + title: string + subtitle?: string + avatar?: React.ReactNode + actions?: React.ReactNode + onBack?: () => void + className?: string +} + +function ChatHeader({ title, subtitle, avatar, actions, onBack, className }: ChatHeaderProps) { + return ( +
+ {onBack && ( + + )} + {avatar} +
+ {title} + {subtitle && {subtitle}} +
+ {actions} +
+ ) +} + +// ─── Shared: Sidebar conversation item ──────────────────────────────────────── + +interface SidebarConversation { + id: string + title: string + avatar?: string + lastMessage?: string + lastMessageTime?: string + unreadCount?: number + presence?: "online" | "away" | "offline" + isGroup?: boolean +} + +function ConversationItem({ + convo, + isActive, + onClick, +}: { + convo: SidebarConversation + isActive: boolean + onClick: () => void +}) { + return ( + + ) +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// LAYOUT 1: FullMessenger (Slack/Discord) +// ═══════════════════════════════════════════════════════════════════════════════ + +interface FullMessengerProps { + currentUser: ChatUser + conversations: SidebarConversation[] + activeConversationId?: string + onSelectConversation: (id: string) => void + messages: ChatMessageData[] + typingUsers?: TypingUser[] + onSend: (text: string) => void + title?: string + subtitle?: string + className?: string +} + +function FullMessenger({ + currentUser, + conversations, + activeConversationId, + onSelectConversation, + messages, + typingUsers, + onSend, + title = "Messages", + subtitle, + className, +}: FullMessengerProps) { + const activeConvo = conversations.find((c) => c.id === activeConversationId) + const showingConvo = !!activeConvo + + return ( + +
+ {/* Sidebar — hidden on mobile when viewing a conversation */} + + + {/* Main panel — hidden on mobile when no conversation selected */} +
+ {activeConvo ? ( + <> + + {/* Mobile-only back button */} + +
+
+ {activeConvo.title.charAt(0).toUpperCase()} +
+ {activeConvo.presence === "online" && ( +
+ )} +
+ + } + actions={ +
+ + + +
+ } + /> + + + + ) : ( +
+
+ +

Select a conversation

+
+
+ )} +
+
+
+ ) +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// LAYOUT 2: ChatWidget (Intercom-style floating) +// ═══════════════════════════════════════════════════════════════════════════════ + +interface ChatWidgetProps { + currentUser: ChatUser + messages: ChatMessageData[] + onSend: (text: string) => void + title?: string + subtitle?: string + greeting?: string + position?: "bottom-right" | "bottom-left" + className?: string +} + +function ChatWidget({ + currentUser, + messages, + onSend, + title = "Support", + subtitle = "We typically reply in minutes", + position = "bottom-right", + className, +}: ChatWidgetProps) { + const [isOpen, setIsOpen] = React.useState(false) + + return ( + +
+ {/* Chat window */} + {isOpen && ( +
+ + +
+ } + actions={ + + } + /> + + +
+ )} + + {/* FAB */} + + +
+ ) +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// LAYOUT 3: InlineChat (Comments section) +// ═══════════════════════════════════════════════════════════════════════════════ + +interface InlineChatProps { + currentUser: ChatUser + messages: ChatMessageData[] + onSend: (text: string) => void + placeholder?: string + maxHeight?: number + className?: string +} + +function InlineChat({ + currentUser, + messages, + onSend, + placeholder = "Add a comment...", + maxHeight = 600, + className, +}: InlineChatProps) { + return ( + +
+ + +
+
+ ) +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// LAYOUT 5: ChatBoard (Forum/Discussion) +// ═══════════════════════════════════════════════════════════════════════════════ + +interface Topic { + id: string + title: string + author: string + replyCount: number + lastActivity: string + isPinned?: boolean + tags?: string[] +} + +interface ChatBoardProps { + currentUser: ChatUser + topics: Topic[] + activeTopic?: Topic + onSelectTopic: (id: string) => void + onBack?: () => void + children?: React.ReactNode + className?: string +} + +function ChatBoard({ + currentUser, + topics, + activeTopic, + onSelectTopic, + onBack, + children, + className, +}: ChatBoardProps) { + return ( + +
+ {activeTopic ? ( + <> + +
+
{children}
+
+ + ) : ( + <> + + } /> +
+ {topics.map((t) => ( + + ))} +
+ + )} +
+
+ ) +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// LAYOUT 6: LiveChat (Twitch/YouTube live stream) +// ═══════════════════════════════════════════════════════════════════════════════ + +interface LiveChatProps { + currentUser: ChatUser + messages: ChatMessageData[] + onSend: (text: string) => void + title?: string + viewerCount?: number + className?: string +} + +function LiveChat({ + currentUser, + messages, + onSend, + title = "Live Chat", + viewerCount, + className, +}: LiveChatProps) { + return ( + +
+
+ {title} + {viewerCount !== undefined && ( + + + {viewerCount.toLocaleString()} watching + + )} +
+ + +
+
+ ) +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// LAYOUT 7: SupportTickets (Help desk / ticket queue) +// ═══════════════════════════════════════════════════════════════════════════════ + +type TicketStatus = "open" | "in-progress" | "resolved" +type TicketPriority = "low" | "medium" | "high" | "urgent" + +interface SupportTicket { + id: string + subject: string + customerName: string + customerAvatar?: string + status: TicketStatus + priority: TicketPriority + category?: string + tags?: string[] + createdAt: string + updatedAt?: string + lastMessage?: string + unreadCount?: number + assignee?: string +} + +interface SupportTicketsProps { + currentUser: ChatUser + tickets: SupportTicket[] + activeTicketId?: string + onSelectTicket: (id: string) => void + messages: ChatMessageData[] + typingUsers?: TypingUser[] + onSend: (text: string) => void + statusFilter?: TicketStatus | "all" + onStatusFilterChange?: (status: TicketStatus | "all") => void + title?: string + className?: string +} + +// ─── Internal helpers ──────────────────────────────────────────────────────── + +const ticketStatusConfig: Record = { + open: { icon: Circle, label: "Open", color: "var(--chat-orange)" }, + "in-progress": { icon: Clock, label: "In Progress", color: "var(--chat-accent)" }, + resolved: { icon: CheckCircle2, label: "Resolved", color: "var(--chat-green)" }, +} + +const ticketPriorityColors: Record = { + urgent: "var(--chat-red)", + high: "var(--chat-orange)", + medium: "var(--chat-accent)", + low: "var(--chat-text-tertiary)", +} + +function TicketStatusBadge({ status }: { status: TicketStatus }) { + const cfg = ticketStatusConfig[status] + const Icon = cfg.icon + return ( + + + {cfg.label} + + ) +} + +function TicketPriorityBadge({ priority }: { priority: TicketPriority }) { + const color = ticketPriorityColors[priority] + return ( + + + {priority} + + ) +} + +function TicketFilterTabs({ + value, + onChange, +}: { + value: TicketStatus | "all" + onChange: (v: TicketStatus | "all") => void +}) { + const tabs: { key: TicketStatus | "all"; label: string }[] = [ + { key: "all", label: "All" }, + { key: "open", label: "Open" }, + { key: "in-progress", label: "Active" }, + { key: "resolved", label: "Resolved" }, + ] + return ( +
+ {tabs.map((t) => ( + + ))} +
+ ) +} + +function TicketItem({ + ticket, + isActive, + onClick, +}: { + ticket: SupportTicket + isActive: boolean + onClick: () => void +}) { + const StatusIcon = ticketStatusConfig[ticket.status].icon + return ( + + ) +} + +// ─── Main component ────────────────────────────────────────────────────────── + +function SupportTickets({ + currentUser, + tickets, + activeTicketId, + onSelectTicket, + messages, + typingUsers, + onSend, + statusFilter: controlledFilter, + onStatusFilterChange, + title = "Support Tickets", + className, +}: SupportTicketsProps) { + const [internalFilter, setInternalFilter] = React.useState("all") + const filter = controlledFilter ?? internalFilter + const setFilter = onStatusFilterChange ?? setInternalFilter + + const filteredTickets = filter === "all" ? tickets : tickets.filter((t) => t.status === filter) + const activeTicket = tickets.find((t) => t.id === activeTicketId) + + const openCount = tickets.filter((t) => t.status === "open").length + const activeCount = tickets.filter((t) => t.status === "in-progress").length + + const showingTicket = !!activeTicket + + return ( + +
+ {/* ── Ticket sidebar ── */} + {/* Mobile: full-width list when no ticket selected, hidden when viewing one */} + {/* Desktop: always visible at w-[280px] */} + + + {/* ── Main panel ── */} + {/* Mobile: hidden when no ticket selected, full-width when viewing one */} + {/* Desktop: always visible as flex-1 */} +
+ {activeTicket ? ( + <> + {/* Header with mobile back button */} +
+ {/* Mobile back */} + +
+ +
+
+ + {activeTicket.subject} + + + {activeTicket.id} · {activeTicket.customerName} + +
+
+ + +
+
+ + + + ) : ( +
+
+ +

Select a ticket

+
+
+ )} +
+
+
+ ) +} + +// ─── Exports ────────────────────────────────────────────────────────────────── + +const ChatConversationItem = ConversationItem + +export { + ChatHeader, + ChatConversationItem, + FullMessenger, + ChatWidget, + InlineChat, + ChatBoard, + LiveChat, + SupportTickets, + TicketStatusBadge, + TicketPriorityBadge, + TicketFilterTabs, +} +export type { + ChatHeaderProps, + SidebarConversation, + FullMessengerProps, + ChatWidgetProps, + InlineChatProps, + Topic, + ChatBoardProps, + LiveChatProps, + TicketStatus, + TicketPriority, + SupportTicket, + SupportTicketsProps, +}