@banbox/chat 1.0.11 → 1.0.13

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/README.md CHANGED
@@ -1,37 +1,72 @@
1
1
  # @banbox/chat
2
2
 
3
- Banbox Chat UI package — plug-and-play chat popup for any React or Next.js project.
3
+ > **Current version:** `1.0.12`
4
+
5
+ Banbox Chat UI package — plug-and-play chat popup for any React or Next.js project.
6
+ Data-agnostic: bring your own adapter (demo, REST API, or WebSocket).
7
+
8
+ ---
4
9
 
5
10
  ## Installation
6
11
 
7
12
  ```bash
8
- # From GitHub private registry
9
13
  npm install @banbox/chat
10
-
11
- # Or local development link
12
- npm install ../banbox-chat
13
14
  ```
14
15
 
16
+ CSS is **auto-injected** — no manual import needed.
17
+
18
+ ---
19
+
15
20
  ## Requirements
16
21
 
17
22
  | Peer dep | Version |
18
- |----------|---------|
19
- | react | ≥ 18 |
20
- | react-dom | ≥ 18 |
21
- | framer-motion | ≥ 10 |
23
+ |---|---|
24
+ | `react` | ≥ 18 |
25
+ | `react-dom` | ≥ 18 |
26
+ | `framer-motion` | ≥ 10 |
27
+ | `lottie-react` | ≥ 2 |
22
28
 
23
29
  ---
24
30
 
25
31
  ## Quick Start (Vite / React)
26
32
 
27
- ### 1. Wrap your app
33
+ ### 1. Create your adapter
34
+
35
+ ```ts
36
+ // src/components/chat/demoData.ts
37
+ import type { ChatAdapter } from "@banbox/chat";
38
+
39
+ export const createDemoChatAdapter = (): ChatAdapter => ({
40
+ threads: {
41
+ list: () => cache.threads,
42
+ subscribe: (cb) => {
43
+ socket.on("threads:update", cb);
44
+ return () => socket.off("threads:update", cb);
45
+ },
46
+ pin: (id, pinned) => api.patch(`/threads/${id}`, { pinned }),
47
+ delete: (id) => api.delete(`/threads/${id}`),
48
+ markRead: (id) => api.post(`/threads/${id}/read`),
49
+ },
50
+ messages: {
51
+ list: (tid) => cache.messages[tid] ?? [],
52
+ subscribe: (tid, cb) => {
53
+ socket.on(`messages:${tid}`, cb);
54
+ return () => socket.off(`messages:${tid}`, cb);
55
+ },
56
+ send: (tid, payload) => api.post(`/threads/${tid}/messages`, payload),
57
+ },
58
+ });
59
+ ```
60
+
61
+ ### 2. Mount `ChatRoot` once at the app root
28
62
 
29
63
  ```tsx
30
- // main.tsx
64
+ // src/main.tsx
31
65
  import { ChatUIProvider, ChatRoot } from "@banbox/chat";
32
66
  import { createDemoChatAdapter } from "./components/chat/demoData";
33
- import "@banbox/chat/dist/index.css"; // if CSS file is emitted
67
+ import { showToast } from "./utils/toast";
34
68
 
69
+ // Create adapter once — outside render
35
70
  const adapter = createDemoChatAdapter();
36
71
 
37
72
  createRoot(document.getElementById("root")!).render(
@@ -39,8 +74,17 @@ createRoot(document.getElementById("root")!).render(
39
74
  <App />
40
75
  <ChatRoot
41
76
  adapter={adapter}
77
+ theme="marketplace"
78
+
79
+ // ── Footer toolbar: allow-list per popup ──────────────────────────
80
+ // Default (when omitted): ["attachment", "emoji", "translate"]
81
+ // Available keys: "attachment" | "emoji" | "businessCard" | "addressCard" | "translate"
82
+ inboxFooterActions={["attachment", "emoji", "translate"]}
83
+ singleFooterActions={["attachment", "emoji", "translate"]}
84
+ // ─────────────────────────────────────────────────────────────────
85
+
42
86
  uiCallbacks={{
43
- showToast: ({ title }) => toast(title),
87
+ showToast,
44
88
  onNavigate: ({ type, id }) => navigate(`/${type}s/${id}`),
45
89
  }}
46
90
  />
@@ -48,17 +92,30 @@ createRoot(document.getElementById("root")!).render(
48
92
  );
49
93
  ```
50
94
 
51
- ### 2. Open the chat from anywhere
95
+ ### 3. Open the chat from anywhere in the app
52
96
 
53
97
  ```tsx
54
98
  import { useChatUI } from "@banbox/chat";
55
99
 
56
- function OrderPage() {
57
- const { openSingle } = useChatUI();
100
+ function OrderPage({ orderId }: { orderId: string }) {
101
+ const { openSingle, openInbox } = useChatUI();
102
+
58
103
  return (
59
- <button onClick={() => openSingle({ reference: { kind: "order", id: "ORD-123" } })}>
60
- Chat with Buyer
61
- </button>
104
+ <>
105
+ {/* Open inbox (all conversations) */}
106
+ <button onClick={() => openInbox()}>Messages</button>
107
+
108
+ {/* Open single chat linked to this order */}
109
+ <button
110
+ onClick={() =>
111
+ openSingle({
112
+ reference: { kind: "order", id: orderId, title: `Order #${orderId}` },
113
+ })
114
+ }
115
+ >
116
+ Chat with Buyer
117
+ </button>
118
+ </>
62
119
  );
63
120
  }
64
121
  ```
@@ -67,27 +124,32 @@ function OrderPage() {
67
124
 
68
125
  ## Quick Start (Next.js App Router)
69
126
 
70
- ### 1. Create a client wrapper component
127
+ ### 1. Create a client wrapper
71
128
 
72
129
  ```tsx
73
130
  // components/ChatWrapper.tsx
74
131
  "use client";
75
132
  import { ChatUIProvider, ChatRoot } from "@banbox/chat";
76
- import { createDemoChatAdapter } from "@/lib/chat/demoAdapter";
133
+ import { createApiChatAdapter } from "@/lib/chat/apiAdapter";
77
134
 
78
- const adapter = createDemoChatAdapter();
135
+ const adapter = createApiChatAdapter();
79
136
 
80
137
  export function ChatWrapper({ children }: { children: React.ReactNode }) {
81
138
  return (
82
139
  <ChatUIProvider>
83
140
  {children}
84
- <ChatRoot adapter={adapter} />
141
+ <ChatRoot
142
+ adapter={adapter}
143
+ theme="marketplace"
144
+ inboxFooterActions={["attachment", "emoji", "translate"]}
145
+ singleFooterActions={["attachment", "emoji", "translate"]}
146
+ />
85
147
  </ChatUIProvider>
86
148
  );
87
149
  }
88
150
  ```
89
151
 
90
- ### 2. Add to your root layout
152
+ ### 2. Add to root layout
91
153
 
92
154
  ```tsx
93
155
  // app/layout.tsx
@@ -104,104 +166,331 @@ export default function RootLayout({ children }) {
104
166
  }
105
167
  ```
106
168
 
107
- ### 3. Open from any Client Component
108
-
109
- ```tsx
110
- "use client";
111
- import { useChatUI } from "@banbox/chat";
112
-
113
- export function ChatButton() {
114
- const { openInbox } = useChatUI();
115
- return <button onClick={() => openInbox()}>Messages</button>;
116
- }
117
- ```
118
-
119
169
  ---
120
170
 
121
171
  ## Tailwind CSS Setup
122
172
 
123
- The package uses Tailwind utility classes. Add the package source to your Tailwind content config so the classes are included in your build:
124
-
125
173
  ```ts
126
- // tailwind.config.ts (or vite.config.ts with @tailwindcss/vite)
174
+ // vite.config.ts (or tailwind.config.ts)
127
175
  export default {
128
176
  content: [
129
177
  "./src/**/*.{ts,tsx}",
130
- // Include @banbox/chat source for Tailwind scanning
178
+ // Include @banbox/chat source for Tailwind class scanning
131
179
  "./node_modules/@banbox/chat/src/**/*.{ts,tsx}",
132
180
  ],
133
- }
181
+ };
134
182
  ```
135
183
 
136
184
  ---
137
185
 
138
- ## Implementing the ChatAdapter
186
+ ## Local Development (Hybrid Mode)
139
187
 
140
- The package is data-agnostic. You implement `ChatAdapter` in your host app:
188
+ 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.
141
189
 
142
190
  ```ts
143
- // lib/chat/apiAdapter.ts
144
- import type { ChatAdapter } from "@banbox/chat";
145
-
146
- export const createApiChatAdapter = ({ baseUrl, token }): ChatAdapter => ({
147
- threads: {
148
- list: () => cache.threads,
149
- subscribe: (cb) => {
150
- socket.on("threads:update", cb);
151
- return () => socket.off("threads:update", cb);
152
- },
153
- pin: (id, pinned) => api.patch(`/threads/${id}`, { pinned }),
154
- delete: (id) => api.delete(`/threads/${id}`),
155
- markRead: (id) => api.post(`/threads/${id}/read`),
156
- },
157
- messages: {
158
- list: (tid) => cache.messages[tid] ?? [],
159
- subscribe: (tid, cb) => {
160
- socket.on(`messages:${tid}`, cb);
161
- return () => socket.off(`messages:${tid}`, cb);
162
- },
163
- send: (tid, payload) => api.post(`/threads/${tid}/messages`, payload),
191
+ // vite.config.ts — hybrid alias (already set up in banbox-seller-react)
192
+ import fs from "fs";
193
+ import path from "path";
194
+
195
+ const localChatPath = path.resolve(__dirname, "../banbox-chat");
196
+ const useLocalChat = fs.existsSync(localChatPath);
197
+
198
+ export default defineConfig({
199
+ resolve: {
200
+ alias: useLocalChat
201
+ ? { "@banbox/chat": path.join(localChatPath, "dist/index.js") }
202
+ : {},
203
+ dedupe: ["react", "react-dom", "framer-motion", "lottie-react"],
164
204
  },
165
205
  });
166
206
  ```
167
207
 
208
+ | Mode | Source |
209
+ |---|---|
210
+ | `npm run dev` (local folder exists) | `../banbox-chat/dist/` |
211
+ | Production build / CI | `node_modules/@banbox/chat` |
212
+
168
213
  ---
169
214
 
170
- ## API Reference
215
+ ## `ChatRoot` Props
171
216
 
172
- ### Components
217
+ ```ts
218
+ <ChatRoot
219
+ adapter={adapter} // Required — your ChatAdapter implementation
220
+ theme="marketplace" // Optional — "marketplace" | "admin" | custom object
221
+ uiCallbacks={...} // Optional — toast, navigate, kebab menu
222
+ inboxFooterActions={[...]} // Optional — allow-list for InboxPopup toolbar
223
+ singleFooterActions={[...]} // Optional — allow-list for SinglePopup toolbar
224
+ />
225
+ ```
226
+
227
+ ### `theme` prop
228
+
229
+ ```tsx
230
+ // Named themes
231
+ <ChatRoot theme="marketplace" /> // orange primary (#ff5300)
232
+ <ChatRoot theme="admin" /> // black primary (#1a1a1a)
233
+
234
+ // Custom theme object
235
+ <ChatRoot theme={{ primary: "#7C3AED", primaryActive: "#6D28D9" }} />
236
+ ```
173
237
 
174
- | Component | Description |
175
- |-----------|-------------|
176
- | `<ChatRoot adapter uiCallbacks? />` | Main entry point — mounts the chat popup |
177
- | `<ChatUIProvider>` | Context provider — wrap your app with this |
238
+ ### `inboxFooterActions` / `singleFooterActions`
178
239
 
179
- ### Hooks
240
+ Controls which toolbar buttons appear in the footer of each popup variant.
180
241
 
181
- | Hook | Description |
182
- |------|-------------|
183
- | `useChatUI()` | Open/close chat, select threads |
242
+ | Key | Button | Default |
243
+ |---|---|---|
244
+ | `"attachment"` | 📎 Attach file / image | Yes |
245
+ | `"emoji"` | 😊 Emoji picker | ✅ Yes |
246
+ | `"translate"` | 🌐 Translation settings | ✅ Yes |
247
+ | `"businessCard"` | 👤 Share business card | ❌ Opt-in |
248
+ | `"addressCard"` | 📍 Share delivery address | ❌ Opt-in |
184
249
 
185
- ### `useChatUI()` methods
250
+ ```tsx
251
+ // Default (omit the prop — shows attachment, emoji, translate only)
252
+ <ChatRoot adapter={adapter} />
253
+
254
+ // Add location sharing to SinglePopup only
255
+ <ChatRoot
256
+ inboxFooterActions={["attachment", "emoji", "translate"]}
257
+ singleFooterActions={["attachment", "emoji", "translate", "addressCard"]}
258
+ />
259
+
260
+ // Full toolbar (all buttons)
261
+ <ChatRoot
262
+ inboxFooterActions={["attachment", "emoji", "businessCard", "addressCard", "translate"]}
263
+ singleFooterActions={["attachment", "emoji", "businessCard", "addressCard", "translate"]}
264
+ />
265
+ ```
266
+
267
+ ### `uiCallbacks` prop
186
268
 
187
269
  ```ts
270
+ uiCallbacks={{
271
+ // Show a toast from your app's existing toast system
272
+ showToast: ({ type, title, message }) => toast[type](title),
273
+
274
+ // Navigate when user clicks "View Order" / "View Inquiry"
275
+ onNavigate: ({ type, id }) => navigate(`/${type}s/${id}`),
276
+
277
+ // (Optional) Replace the default ⋮ kebab menu with your own component
278
+ renderKebabMenu: ({ pinned, onPinToggle, onDelete }) => (
279
+ <MyDropdownMenu pinned={pinned} onPin={onPinToggle} onDelete={onDelete} />
280
+ ),
281
+ }}
282
+ ```
283
+
284
+ ---
285
+
286
+ ## `useChatUI()` Hook
287
+
288
+ ```tsx
188
289
  const {
189
- openInbox, // open the inbox (thread list) view
190
- openSingle, // open a single chat view
191
- close, // close the popup
192
- selectThread, // switch active thread
193
- isOpen, // boolean
194
- variant, // "inbox" | "single"
290
+ openInbox, // () => void — open inbox (all threads)
291
+ openSingle, // (opts?) => void — open single chat
292
+ close, // () => void — close popup
293
+ selectThread, // (id: string | null) => void
294
+ isOpen, // boolean
295
+ variant, // "inbox" | "single"
296
+ reference, // Reference | undefined
297
+ selectedThreadId, // string | null
195
298
  } = useChatUI();
196
299
  ```
197
300
 
198
- ### `openSingle(opts)` Reference kinds
301
+ ### `openInbox(opts?)`
199
302
 
200
303
  ```ts
201
- openSingle({ reference: { kind: "order", id: "ORD-123", title: "Order Details" } });
202
- openSingle({ reference: { kind: "inquiry", id: "INQ-456", title: "Product Inquiry" } });
203
- openSingle({ reference: { kind: "quotation", id: "QUOT-789", title: "Quotation Request" } });
304
+ // Open inbox with all threads
305
+ openInbox();
306
+
307
+ // Open inbox pre-filtered to a reference kind
308
+ openInbox({ reference: { kind: "order" } });
309
+
310
+ // Open inbox with a specific thread pre-selected
311
+ openInbox({ threadId: "t4" });
312
+ ```
313
+
314
+ ### `openSingle(opts?)`
315
+
316
+ ```ts
317
+ // Plain single chat
318
+ openSingle();
319
+
320
+ // Linked to an order — shows "View Order" bar in header
321
+ openSingle({ reference: { kind: "order", id: "ORD-123", title: "Order #123" } });
322
+
323
+ // Linked to an inquiry
324
+ openSingle({ reference: { kind: "inquiry", id: "INQ-456" } });
325
+
326
+ // Linked to a quotation
327
+ openSingle({ reference: { kind: "quotation", id: "QUOT-789" } });
328
+
329
+ // Product inquiry
204
330
  openSingle({ reference: { kind: "productInquiry", id: "PI-101" } });
331
+
332
+ // With metadata for the header (title, subtitle, online status)
333
+ openSingle({
334
+ reference: {
335
+ kind: "order",
336
+ id: "ORD-123",
337
+ meta: { title: "Emon Hasan", subtitle: "Customer", online: true },
338
+ },
339
+ });
340
+ ```
341
+
342
+ ---
343
+
344
+ ## `ChatAdapter` Interface
345
+
346
+ Full interface contract:
347
+
348
+ ```ts
349
+ import type { ChatAdapter } from "@banbox/chat";
350
+
351
+ const adapter: ChatAdapter = {
352
+ threads: {
353
+ // Returns current thread list (sync — keep a local cache)
354
+ list: (reference?) => Thread[],
355
+
356
+ // Subscribe to thread changes — returns unsubscribe function
357
+ subscribe: (cb: () => void) => () => void,
358
+
359
+ // Pin / unpin a thread
360
+ pin: (id: string, pinned: boolean) => Promise<void> | void,
361
+
362
+ // Delete a thread
363
+ delete: (id: string) => Promise<void> | void,
364
+
365
+ // Mark thread as read (optional)
366
+ markRead?: (id: string) => Promise<void> | void,
367
+ },
368
+
369
+ messages: {
370
+ // Returns messages for a thread (sync — keep a local cache)
371
+ list: (threadId: string) => Message[],
372
+
373
+ // Subscribe to new messages for a thread (optional)
374
+ subscribe?: (threadId: string, cb: () => void) => () => void,
375
+
376
+ // Send a message — handles all payload types
377
+ send: (threadId: string, payload: SendPayload) => Promise<void> | void,
378
+ },
379
+ };
380
+ ```
381
+
382
+ ### `SendPayload` — all message types
383
+
384
+ ```ts
385
+ // Text message
386
+ { type: "text"; text: string; replyTo?: MessageRef }
387
+
388
+ // Voice/audio message
389
+ { type: "voice"; src?: string; durationSec: number; durationText: string; replyTo?: MessageRef }
390
+
391
+ // Images and/or files only
392
+ { type: "attachments"; images: string[]; files: MessageFile[]; replyTo?: MessageRef }
393
+
394
+ // Text + images/files combined
395
+ { type: "combined"; text: string; images: string[]; files: MessageFile[]; replyTo?: MessageRef }
396
+
397
+ // Business card
398
+ { type: "businessCard"; card: BusinessCard; replyTo?: MessageRef }
399
+
400
+ // Address / delivery card
401
+ { type: "addressCard"; card: AddressCard; replyTo?: MessageRef }
402
+ ```
403
+
404
+ ### Adding a new message type
405
+
406
+ 1. Add a new variant to `SendPayload` in `banbox-chat/src/types/index.ts`
407
+ 2. Handle it in your adapter's `messages.send()` implementation
408
+ 3. Optionally add a new UI component in `banbox-chat/src/ui/message-items/`
409
+
410
+ ---
411
+
412
+ ## Domain Types
413
+
414
+ ```ts
415
+ import type {
416
+ Thread, // Conversation thread
417
+ Message, // A single chat message
418
+ SendPayload, // Discriminated union of all sendable message types
419
+ MessageFile, // File attachment
420
+ MessageAudio, // Audio clip
421
+ MessageRef, // Reply-to reference
422
+ BusinessCard, // Business card data
423
+ AddressCard, // Delivery address data
424
+ ThreadStatus, // "seen" | "delivered" | { kind: "new", count: number }
425
+ Reference, // Context link: order | inquiry | quotation | productInquiry
426
+ } from "@banbox/chat";
427
+ ```
428
+
429
+ ---
430
+
431
+ ## Individual UI Components (Advanced)
432
+
433
+ Export individual components for custom layouts:
434
+
435
+ ```tsx
436
+ import {
437
+ ChatFooter,
438
+ ChatHeader,
439
+ ChatIdentity,
440
+ ChatMessageItem,
441
+ ChatScroll,
442
+ ChatThreadItem,
443
+ ChatListHeader,
444
+ ChatInquiryBar,
445
+ TypingIndicator,
446
+ ReplyCard,
447
+ ChatSpinner,
448
+ ChatKebabMenu,
449
+ } from "@banbox/chat";
450
+ ```
451
+
452
+ ### `ChatFooter` standalone
453
+
454
+ ```tsx
455
+ import { ChatFooter, DEFAULT_FOOTER_ACTIONS } from "@banbox/chat";
456
+
457
+ <ChatFooter
458
+ onSend={(payload) => adapter.messages.send(threadId, payload)}
459
+ onAfterSend={() => setRev(v => v + 1)}
460
+
461
+ // Allow-list — only these buttons appear
462
+ enabledActions={["attachment", "emoji", "translate"]}
463
+
464
+ replyTo={replyTo}
465
+ clearReply={() => setReplyTo(undefined)}
466
+ />
467
+ ```
468
+
469
+ ---
470
+
471
+ ## Translation Support
472
+
473
+ 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`:
474
+
475
+ ```tsx
476
+ <ChatMessageItem
477
+ // ...
478
+ onTranslate={(originalText) => {
479
+ // Return translated string or undefined (will keep original if undefined)
480
+ return myTranslationService.translate(originalText, "bn");
481
+ }}
482
+ />
483
+ ```
484
+
485
+ > When `onTranslate` is omitted, the translate button is still visible but acts as a no-op toggle (good for demo mode).
486
+
487
+ ---
488
+
489
+ ## Exported Constants
490
+
491
+ ```ts
492
+ import { DEFAULT_FOOTER_ACTIONS } from "@banbox/chat";
493
+ // → ["attachment", "emoji", "translate"]
205
494
  ```
206
495
 
207
496
  ---
@@ -209,7 +498,33 @@ openSingle({ reference: { kind: "productInquiry", id: "PI-101" } });
209
498
  ## Build
210
499
 
211
500
  ```bash
212
- npm run build # compile to dist/
213
- npm run build:watch # watch mode
214
- npm run typecheck # tsc --noEmit
501
+ npm run build # compile to dist/
502
+ npm run build:watch # watch mode
503
+ npm run typecheck # tsc --noEmit
215
504
  ```
505
+
506
+ ---
507
+
508
+ ## Changelog
509
+
510
+ ### v1.0.12
511
+ - ♻️ `hiddenActionKeys` → `enabledActions` allow-list in `ChatFooter` (breaking rename in package internals, not host-facing)
512
+ - ✨ `inboxFooterActions` and `singleFooterActions` props on `ChatRoot` — per-popup toolbar control
513
+ - `businessCard` and `addressCard` are now **opt-in** (not shown by default)
514
+
515
+ ### v1.0.11
516
+ - Added `hiddenActionKeys` prop threading through `ChatRoot` → `InboxPopup/SinglePopup` → `ChatFooter`
517
+
518
+ ### v1.0.10
519
+ - 🐛 **Critical:** `SinglePopup` now subscribes to thread list (was fetched once on mount — real-time updates were missing)
520
+ - 🐛 `coalesceThreadId` no longer has hardcoded `"t4"` demo dependency — uses pure reference matching
521
+ - 🐛 `isOnline = true` hardcoded removed from `ChatMessageItem` avatar
522
+ - ✨ `onTranslate` prop on `ChatMessageItem` — replaces internal `toBanglaDemo` hardcoded translator
523
+ - 🐛 `markRead` now fires on **initial popup open**, not only on thread switch
524
+ - ♻️ Shared `utils/theme.ts` — `GRADIENT_BORDER`, `getThemeAttr`, `getThemeVars` extracted from `InboxPopup`/`SinglePopup`
525
+
526
+ ### v1.0.9
527
+ - Initial public release
528
+ - Full adapter pattern, pub/sub threads/messages
529
+ - InboxPopup + SinglePopup
530
+ - All message types: text, voice, images, files, businessCard, addressCard