@banbox/chat
v1.0.20
Published
Banbox Chat UI components — reusable across all Banbox React/Next.js projects
Maintainers
Readme
@banbox/chat
Current version:
1.0.16
Banbox Chat UI package — plug-and-play chat popup for any React or Next.js project.
Data-agnostic: bring your own adapter (demo, REST API, or WebSocket).
Installation
npm install @banbox/chatCSS is auto-injected — no manual import needed.
Requirements
| Peer dep | Version |
|---|---|
| react | ≥ 18 |
| react-dom | ≥ 18 |
| framer-motion | ≥ 10 |
| lottie-react | ≥ 2 |
Quick Start (Vite / React)
1. Create your adapter
// src/components/chat/demoData.ts
import type { ChatAdapter } from "@banbox/chat";
export const createDemoChatAdapter = (): ChatAdapter => ({
threads: {
list: () => cache.threads,
subscribe: (cb) => {
socket.on("threads:update", cb);
return () => socket.off("threads:update", cb);
},
pin: (id, pinned) => api.patch(`/threads/${id}`, { pinned }),
delete: (id) => api.delete(`/threads/${id}`),
markRead: (id) => api.post(`/threads/${id}/read`),
},
messages: {
list: (tid) => cache.messages[tid] ?? [],
subscribe: (tid, cb) => {
socket.on(`messages:${tid}`, cb);
return () => socket.off(`messages:${tid}`, cb);
},
send: (tid, payload) => api.post(`/threads/${tid}/messages`, payload),
},
});2. Mount ChatRoot once at the app root
// src/main.tsx
import { ChatUIProvider, ChatRoot } from "@banbox/chat";
import { createDemoChatAdapter } from "./components/chat/demoData";
import { showToast } from "./utils/toast";
// Create adapter once — outside render
const adapter = createDemoChatAdapter();
createRoot(document.getElementById("root")!).render(
<ChatUIProvider>
<App />
<ChatRoot
adapter={adapter}
theme="marketplace"
// ── Footer toolbar: allow-list per popup ──────────────────────────
// Default (when omitted): ["attachment", "emoji", "translate"]
// Available keys: "attachment" | "emoji" | "businessCard" | "addressCard" | "translate"
inboxFooterActions={["attachment", "emoji", "translate"]}
singleFooterActions={["attachment", "emoji", "translate"]}
// ─────────────────────────────────────────────────────────────────
uiCallbacks={{
showToast,
onNavigate: ({ type, id }) => navigate(`/${type}s/${id}`),
}}
/>
</ChatUIProvider>
);3. Open the chat from anywhere in the app
import { useChatUI } from "@banbox/chat";
function OrderPage({ orderId }: { orderId: string }) {
const { openSingle, openInbox } = useChatUI();
return (
<>
{/* Open inbox (all conversations) */}
<button onClick={() => openInbox()}>Messages</button>
{/* Open single chat linked to this order */}
<button
onClick={() =>
openSingle({
reference: { kind: "order", id: orderId, title: `Order #${orderId}` },
})
}
>
Chat with Buyer
</button>
</>
);
}Quick Start (Next.js App Router)
1. Create a client wrapper
// components/ChatWrapper.tsx
"use client";
import { ChatUIProvider, ChatRoot } from "@banbox/chat";
import { createApiChatAdapter } from "@/lib/chat/apiAdapter";
const adapter = createApiChatAdapter();
export function ChatWrapper({ children }: { children: React.ReactNode }) {
return (
<ChatUIProvider>
{children}
<ChatRoot
adapter={adapter}
theme="marketplace"
inboxFooterActions={["attachment", "emoji", "translate"]}
singleFooterActions={["attachment", "emoji", "translate"]}
/>
</ChatUIProvider>
);
}2. Add to root layout
// app/layout.tsx
import { ChatWrapper } from "@/components/ChatWrapper";
export default function RootLayout({ children }) {
return (
<html>
<body>
<ChatWrapper>{children}</ChatWrapper>
</body>
</html>
);
}Tailwind CSS Setup
// vite.config.ts (or tailwind.config.ts)
export default {
content: [
"./src/**/*.{ts,tsx}",
// Include @banbox/chat source for Tailwind class scanning
"./node_modules/@banbox/chat/src/**/*.{ts,tsx}",
],
};Local Development (Hybrid Mode)
The seller / host app automatically detects the local banbox-chat folder and uses it during development, while using the published npm package in production builds.
// vite.config.ts — hybrid alias (already set up in banbox-seller-react)
import fs from "fs";
import path from "path";
const localChatPath = path.resolve(__dirname, "../banbox-chat");
const useLocalChat = fs.existsSync(localChatPath);
export default defineConfig({
resolve: {
alias: useLocalChat
? { "@banbox/chat": path.join(localChatPath, "dist/index.js") }
: {},
dedupe: ["react", "react-dom", "framer-motion", "lottie-react"],
},
});| Mode | Source |
|---|---|
| npm run dev (local folder exists) | ../banbox-chat/dist/ |
| Production build / CI | node_modules/@banbox/chat |
ChatRoot Props
<ChatRoot
adapter={adapter} // Required — your ChatAdapter implementation
theme="marketplace" // Optional — "marketplace" | "admin" | custom object
uiCallbacks={...} // Optional — toast, navigate, kebab menu
inboxFooterActions={[...]} // Optional — allow-list for InboxPopup toolbar
singleFooterActions={[...]} // Optional — allow-list for SinglePopup toolbar
/>theme prop
// Named themes
<ChatRoot theme="marketplace" /> // orange primary (#ff5300)
<ChatRoot theme="admin" /> // black primary (#1a1a1a)
// Custom theme object
<ChatRoot theme={{ primary: "#7C3AED", primaryActive: "#6D28D9" }} />inboxFooterActions / singleFooterActions
Controls which toolbar buttons appear in the footer of each popup variant.
| Key | Button | Default |
|---|---|---|
| "attachment" | 📎 Attach file / image | ✅ Yes |
| "emoji" | 😊 Emoji picker | ✅ Yes |
| "translate" | 🌐 Translation settings | ✅ Yes |
| "businessCard" | 👤 Share business card | ❌ Opt-in |
| "addressCard" | 📍 Share delivery address | ❌ Opt-in |
// Default (omit the prop — shows attachment, emoji, translate only)
<ChatRoot adapter={adapter} />
// Add location sharing to SinglePopup only
<ChatRoot
inboxFooterActions={["attachment", "emoji", "translate"]}
singleFooterActions={["attachment", "emoji", "translate", "addressCard"]}
/>
// Full toolbar (all buttons)
<ChatRoot
inboxFooterActions={["attachment", "emoji", "businessCard", "addressCard", "translate"]}
singleFooterActions={["attachment", "emoji", "businessCard", "addressCard", "translate"]}
/>uiCallbacks prop
uiCallbacks={{
// Show a toast from your app's existing toast system
showToast: ({ type, title, message }) => toast[type](title),
// Navigate when user clicks "View Order" / "View Inquiry"
onNavigate: ({ type, id }) => navigate(`/${type}s/${id}`),
// (Optional) Replace the default ⋮ kebab menu with your own component
renderKebabMenu: ({ pinned, onPinToggle, onDelete }) => (
<MyDropdownMenu pinned={pinned} onPin={onPinToggle} onDelete={onDelete} />
),
}}useChatUI() Hook
const {
openInbox, // () => void — open inbox (all threads)
openSingle, // (opts?) => void — open single chat
close, // () => void — close popup
selectThread, // (id: string | null) => void
isOpen, // boolean
variant, // "inbox" | "single"
reference, // Reference | undefined
selectedThreadId, // string | null
} = useChatUI();openInbox(opts?)
// Open inbox with all threads
openInbox();
// Open inbox pre-filtered to a reference kind
openInbox({ reference: { kind: "order" } });
// Open inbox with a specific thread pre-selected
openInbox({ threadId: "t4" });openSingle(opts?)
// Plain single chat
openSingle();
// Linked to an order — shows "View Order" bar in header
openSingle({ reference: { kind: "order", id: "ORD-123", title: "Order #123" } });
// Linked to an inquiry
openSingle({ reference: { kind: "inquiry", id: "INQ-456" } });
// Linked to a quotation
openSingle({ reference: { kind: "quotation", id: "QUOT-789" } });
// Product inquiry
openSingle({ reference: { kind: "productInquiry", id: "PI-101" } });
// With metadata for the header (title, subtitle, online status)
openSingle({
reference: {
kind: "order",
id: "ORD-123",
meta: { title: "Emon Hasan", subtitle: "Customer", online: true },
},
});ChatAdapter Interface
Full interface contract:
import type { ChatAdapter } from "@banbox/chat";
const adapter: ChatAdapter = {
threads: {
// Returns current thread list (sync — keep a local cache)
list: (reference?) => Thread[],
// Subscribe to thread changes — returns unsubscribe function
subscribe: (cb: () => void) => () => void,
// Pin / unpin a thread
pin: (id: string, pinned: boolean) => Promise<void> | void,
// Delete a thread
delete: (id: string) => Promise<void> | void,
// Mark thread as read (optional)
markRead?: (id: string) => Promise<void> | void,
},
messages: {
// Returns messages for a thread (sync — keep a local cache)
list: (threadId: string) => Message[],
// Subscribe to new messages for a thread (optional)
subscribe?: (threadId: string, cb: () => void) => () => void,
// Send a message — handles all payload types
send: (threadId: string, payload: SendPayload) => Promise<void> | void,
},
};SendPayload — all message types
// Text message
{ type: "text"; text: string; replyTo?: MessageRef }
// Voice/audio message
{ type: "voice"; src?: string; durationSec: number; durationText: string; replyTo?: MessageRef }
// Images and/or files only
{ type: "attachments"; images: string[]; files: MessageFile[]; replyTo?: MessageRef }
// Text + images/files combined
{ type: "combined"; text: string; images: string[]; files: MessageFile[]; replyTo?: MessageRef }
// Business card
{ type: "businessCard"; card: BusinessCard; replyTo?: MessageRef }
// Address / delivery card
{ type: "addressCard"; card: AddressCard; replyTo?: MessageRef }Adding a new message type
- Add a new variant to
SendPayloadinbanbox-chat/src/types/index.ts - Handle it in your adapter's
messages.send()implementation - Optionally add a new UI component in
banbox-chat/src/ui/message-items/
Domain Types
import type {
Thread, // Conversation thread
Message, // A single chat message
SendPayload, // Discriminated union of all sendable message types
MessageFile, // File attachment
MessageAudio, // Audio clip
MessageRef, // Reply-to reference
BusinessCard, // Business card data
AddressCard, // Delivery address data
ThreadStatus, // "seen" | "delivered" | { kind: "new", count: number }
Reference, // Context link: order | inquiry | quotation | productInquiry
} from "@banbox/chat";Individual UI Components (Advanced)
Export individual components for custom layouts:
import {
ChatFooter,
ChatHeader,
ChatIdentity,
ChatMessageItem,
ChatScroll,
ChatThreadItem,
ChatListHeader,
ChatInquiryBar,
TypingIndicator,
ReplyCard,
ChatSpinner,
ChatKebabMenu,
} from "@banbox/chat";ChatFooter standalone
import { ChatFooter, DEFAULT_FOOTER_ACTIONS } from "@banbox/chat";
<ChatFooter
onSend={(payload) => adapter.messages.send(threadId, payload)}
onAfterSend={() => setRev(v => v + 1)}
// Allow-list — only these buttons appear
enabledActions={["attachment", "emoji", "translate"]}
replyTo={replyTo}
clearReply={() => setReplyTo(undefined)}
/>Translation Support
The translate button (🌐) in the footer opens a Translation Settings modal where users can configure language preferences. To integrate your own translation API, pass onTranslate to ChatMessageItem:
<ChatMessageItem
// ...
onTranslate={(originalText) => {
// Return translated string or undefined (will keep original if undefined)
return myTranslationService.translate(originalText, "bn");
}}
/>When
onTranslateis omitted, the translate button is still visible but acts as a no-op toggle (good for demo mode).
Exported Constants
import { DEFAULT_FOOTER_ACTIONS } from "@banbox/chat";
// → ["attachment", "emoji", "translate"]Build
npm run build # compile to dist/
npm run build:watch # watch mode
npm run typecheck # tsc --noEmitChangelog
v1.0.16
- 🧹 Fixed all Tailwind CSS class syntax warnings (IDE linter clean)
z-[N]→z-Nfor all numeric z-index valueshover:text-[var(--x)]→hover:text-(--x)(CSS variable shorthand)bg-[var(--x,fallback)]→bg-(--x,fallback)-ml-[5px]→ml-[-5px],-z-[1]→z-[-1],-top-[6px]→top-[-6px]!bg-[#hex]→bg-[#hex]!(Tailwind v4 important suffix)
v1.0.15
- 🚫 Removed
ChatKebabMenu(⋮ three-dot menu) fromSinglePopupheaderSinglePopupheader now shows only the ✕ close buttonChatKebabMenuis still present inInboxPopup(thread list pin/delete)
- 🖼️ Demo data: Emon Hasan (
t2) now has a real generated profile photo
v1.0.14
- 🐛 Critical: Removed hardcoded
/shop/ban-box.pngfallback fromSinglePopupSinglePopupnow conditionally rendersvariant="avatar"(whenavatarSrcexists) orvariant="initial"(letter avatar with deterministic background colour)- Same behavior as
InboxPopup— no broken images when thread has no photo
- ✨
reference.meta.avatarSrcnow supported inSinglePopup(for host-app-supplied per-chat avatars) - 🖼️ Demo data: all threads now have
avatarSrcusing images frompublic/chat/img/
v1.0.13
- 📖 Full README rewrite — documents all props, hooks, adapter interface,
SendPayloadtypes, local dev hybrid mode, individual UI components,DEFAULT_FOOTER_ACTIONSconstant
v1.0.12
- ♻️
hiddenActionKeys→enabledActionsallow-list inChatFooter(internal rename) - ✨
inboxFooterActionsandsingleFooterActionsprops onChatRoot— per-popup toolbar control businessCardandaddressCardare now opt-in (not shown by default)
v1.0.11
- Added
hiddenActionKeysprop threading throughChatRoot→InboxPopup/SinglePopup→ChatFooter
v1.0.10
- 🐛 Critical:
SinglePopupnow subscribes to thread list (real-time updates were missing) - 🐛
coalesceThreadIdno longer has hardcoded"t4"demo dependency - 🐛
isOnline = truehardcoded removed fromChatMessageItemavatar - ✨
onTranslateprop onChatMessageItem— replaces internal hardcoded translator - 🐛
markReadnow fires on initial popup open, not only on thread switch - ♻️ Shared
utils/theme.ts—GRADIENT_BORDER,getThemeAttr,getThemeVarsextracted
v1.0.9
- Initial public release
- Full adapter pattern, pub/sub threads/messages
- InboxPopup + SinglePopup
- All message types: text, voice, images, files, businessCard, addressCard
