@cometchat/skills 3.0.0

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.
@@ -0,0 +1,1293 @@
1
+ ---
2
+ name: cometchat-placement
3
+ description: "Production integration patterns -- how to add CometChat as a route, modal, drawer, embedded panel, or widget in an existing project. Teaches Claude WHERE to put chat."
4
+ license: "MIT"
5
+ compatibility: "@cometchat/chat-uikit-react ^6; @cometchat/chat-sdk-javascript ^4"
6
+ allowed-tools: "executeBash, readFile, fileSearch, listDirectory"
7
+ metadata:
8
+ author: "CometChat"
9
+ version: "3.0.0"
10
+ tags: "chat cometchat react placement route modal drawer widget embedded integration"
11
+ ---
12
+
13
+ ## Purpose
14
+
15
+ This skill teaches you WHERE to put CometChat in an existing project. It covers five placement patterns: route, modal, drawer, embedded panel, and floating widget. Each pattern includes step-by-step instructions and complete code examples.
16
+
17
+ **This skill is framework-AGNOSTIC.** It uses generic instructions like "create a page at the framework's route location" and "add a route entry to the project's router." The framework-specific details (file paths, SSR handling, env var prefixes) come from the framework skill and the `cometchat-core` skill.
18
+
19
+ **Before using this skill:**
20
+ - Read `cometchat-core` for initialization, login, CSS, and provider patterns
21
+ - Read `cometchat-components` for component names, props, and composition patterns
22
+
23
+ ---
24
+
25
+ ## "What are you building?" -- placement recommendation
26
+
27
+ Use this table to recommend a placement based on what the user is building. If the user says "add chat to my app" without specifying where, ask them what they are building and use this table.
28
+
29
+ | User intent | Recommended placement | Experience composition |
30
+ |---|---|---|
31
+ | Messaging app | Route (full page) | Multi-conversation (CometChatConversations + MessageHeader + MessageList + MessageComposer) |
32
+ | Marketplace / platform | Drawer on product page + `/messages` route | Single thread (drawer) + Multi-conversation (route) |
33
+ | SaaS / dashboard | Modal from navbar + `/messages` route | Single thread (modal) + Multi-conversation (route) |
34
+ | Social / community | Route (tabs) | Full messenger (CometChatConversations + CallLogs + Users + Groups with tabs) |
35
+ | Support / helpdesk | Floating widget | Widget (use CLI) |
36
+ | Just exploring | Demo (replace home page) | Multi-conversation |
37
+
38
+ ---
39
+
40
+ ## Visual reference — experience layouts
41
+
42
+ When presenting experience options to the user, describe these layouts
43
+ or share the ASCII art so they can visualize what each looks like.
44
+
45
+ ### Multi-conversation (Experience 1)
46
+
47
+ Two-pane layout: conversation list on the left, active chat thread on the right.
48
+
49
+ ```
50
+ ┌─────────────────────────┬───────────────────────────────────────┐
51
+ │ Chats Q │ Richard Ray v c i │
52
+ ├─────────────────────────┼───────────────────────────────────────┤
53
+ │ │ │
54
+ │ (RR) Richard Ray 3:45 │ ╭─────────────────────╮ │
55
+ │ Is it still up.. │ │ Hi, is the watch │ │
56
+ │ │ │ still up for sale? │ │
57
+ │ (SB) Sarah Beth 3:40 │ ╰────────── 4:56 PM ─╯ │
58
+ │ Sure! Sending .. │ │
59
+ │ │ ╭─────────────────╮ │
60
+ │ (RA) Robert Allen 3:38 │ │ Yes, it is │ │
61
+ │ Thanks! Looks .. │ │ available. │ │
62
+ │ │ ╰─ 4:56 PM ──────╯ │
63
+ │ (SG) Sam Game 3:30 │ │
64
+ │ Sending them .. │ ╭─────────────────────╮ │
65
+ │ │ │ Can I see a couple │ │
66
+ │ (SF) Scott F. 3:22 │ │ of pictures? │ │
67
+ │ I will look .. │ ╰────────── 4:56 PM ─╯ │
68
+ │ │ │
69
+ │ (EP) Evan Parker 3:15 │ ╭─────────────────╮ │
70
+ │ Hey, did you .. │ │ Sure! Sending │ │
71
+ │ │ │ them over now. │ │
72
+ │ (JP) John Paul 3:10 │ ╰─ 4:56 PM ──────╯ │
73
+ │ Sounds good │ │
74
+ │ │ ╭─────────────────────╮ │
75
+ │ (LK) Linda Kay 3:05 │ │ Thanks! Looks good. │ │
76
+ │ See you there │ ╰────────── 4:56 PM ─╯ │
77
+ │ ├───────────────────────────────────────┤
78
+ │ │ Type a message... > │
79
+ └─────────────────────────┴───────────────────────────────────────┘
80
+ ```
81
+
82
+ Best for: messaging apps, team chat, inboxes, dedicated chat sections.
83
+
84
+ ### Single thread (Experience 2)
85
+
86
+ One chat window — no conversation list. Shows a direct chat with one user or group.
87
+
88
+ ```
89
+ ┌─────────────────────────────────────────────────────────────────┐
90
+ │ │
91
+ │ (RR) Richard Ray v c i │
92
+ │ . Online │
93
+ │ │
94
+ ├─────────────────────────────────────────────────────────────────┤
95
+ │ │
96
+ │ ╭───────────────────────────────╮ │
97
+ │ │ Hi, is the watch still up │ │
98
+ │ │ for sale? 4:56 PM vv │ │
99
+ │ ╰───────────────────────────────╯ │
100
+ │ │
101
+ │ ╭───────────────────────╮ │
102
+ │ │ Yes, it is available. │ │
103
+ │ ╰── 4:56 PM ───────────╯ │
104
+ │ │
105
+ │ ╭───────────────────────────────╮ │
106
+ │ │ Awesome! Can I see a couple │ │
107
+ │ │ of pictures? 4:56 PM vv │ │
108
+ │ ╰───────────────────────────────╯ │
109
+ │ │
110
+ │ ╭────────────────────────────────╮ │
111
+ │ │ Sure! Sending them over now. │ │
112
+ │ ╰── 4:56 PM ────────────────────╯ │
113
+ │ │
114
+ │ ╭───────────────────────────────╮ │
115
+ │ │ Thanks! Looks good. 4:56 PM vv│ │
116
+ │ ╰───────────────────────────────╯ │
117
+ │ │
118
+ │ ╭─────────────╮ │
119
+ │ │ Thank you! │ │
120
+ │ ╰── 4:56 PM ─╯ │
121
+ │ │
122
+ ├─────────────────────────────────────────────────────────────────┤
123
+ │ + m e a Type a message... > │
124
+ └─────────────────────────────────────────────────────────────────┘
125
+ ```
126
+
127
+ Best for: marketplace chat, embedded consult, support, one-on-one conversations.
128
+
129
+ ### Full messenger (Experience 3)
130
+
131
+ Two-pane layout like Experience 1, plus a bottom tab bar for switching
132
+ between Chats, Calls, Users, and Groups.
133
+
134
+ ```
135
+ ┌─────────────────────────┬───────────────────────────────────────┐
136
+ │ Chats Q │ Richard Ray v c i │
137
+ ├─────────────────────────┼───────────────────────────────────────┤
138
+ │ │ │
139
+ │ (RR) Richard Ray 3:45 │ ╭─────────────────────╮ │
140
+ │ How much extra.. │ │ How much extra are │ │
141
+ │ │ │ we talking for the │ │
142
+ │ (SB) Sarah Beth 3:40 │ │ direct flight? │ │
143
+ │ That sounds w.. │ ╰────────── 4:56 PM ─╯ │
144
+ │ │ │
145
+ │ (RA) Robert Allen 3:38 │ ╭──────────────────────╮ │
146
+ │ 4:56 PM │ │ It is $50 more. Save │ │
147
+ │ │ │ a couple of hours. │ │
148
+ │ (SG) Sam Game 3:30 │ ╰─ 4:56 PM ───────────╯ │
149
+ │ Sending them .. │ │
150
+ │ │ ╭─────────────────────╮ │
151
+ │ (SF) Scott F. 3:22 │ │ That sounds worth │ │
152
+ │ I will look .. │ │ it. Let us do it. │ │
153
+ │ │ ╰────────── 4:56 PM ─╯ │
154
+ │ (EP) Evan Parker 3:15 │ │
155
+ │ Hey, did you .. │ ╭──────────────────────╮ │
156
+ │ │ │ Great, I will send │ │
157
+ │ │ │ you the details. │ │
158
+ │ │ ╰─ 4:56 PM ───────────╯ │
159
+ │ ├───────────────────────────────────────┤
160
+ │ │ Type a message... > │
161
+ ├─────────────────────────┼───────────────────────────────────────┤
162
+ │ Ch Ca Us Gr │ │
163
+ └─────────────────────────┴───────────────────────────────────────┘
164
+ ```
165
+
166
+ Best for: social apps, community platforms, dating apps, full-featured
167
+ chat products.
168
+
169
+ ---
170
+
171
+ ## Thread replies — hidden by default in every example below
172
+
173
+ Every `<CometChatMessageList ...>` in the placement patterns below
174
+ includes `hideReplyInThreadOption`. The kit's default (`false`) puts a
175
+ **"Reply in Thread"** entry in every message's action menu — but that
176
+ entry only works if the integrator has wired up a thread panel
177
+ (`CometChatThreadHeader` + a scoped `CometChatMessageList` +
178
+ `CometChatMessageComposer` with `parentMessageId`). If the thread
179
+ panel isn't wired (the case for a simple drawer, widget, modal, or
180
+ single-thread experience), the option is still visible and clicking it
181
+ silently does nothing — confusing UX.
182
+
183
+ Default: **threads hidden**. To enable threads for an experience that
184
+ actually has the side-panel plumbing:
185
+
186
+ 1. Remove `hideReplyInThreadOption` from the main `<CometChatMessageList>`
187
+ 2. Add `onThreadRepliesClick` to capture the thread message
188
+ 3. Render the thread panel (see `cometchat-components` § Threading for
189
+ the full pattern — `CometChatThreadHeader` + scoped `MessageList` +
190
+ scoped `MessageComposer` with `parentMessageId`)
191
+
192
+ ---
193
+
194
+ ## Route placement
195
+
196
+ The most common pattern. Chat gets its own page in the app, accessible via navigation.
197
+
198
+ ### Steps
199
+
200
+ #### 1. Set up CometChatProvider at the app root
201
+
202
+ The provider (from `cometchat-core`) should wrap the entire app or the chat route's layout. This ensures init and login happen once, not per-navigation.
203
+
204
+ - **Read the project's existing layout/root component first.** Look for the outermost wrapper (e.g., `App.tsx`, `layout.tsx`, `root.tsx`).
205
+ - Add the `CometChatProvider` inside the existing layout, wrapping the router outlet or children.
206
+ - Import `@cometchat/chat-uikit-react/css-variables.css` at the app root CSS file if not already imported.
207
+
208
+ #### 2. Create a chat page component
209
+
210
+ Create a new file (e.g., `ChatPage.tsx` or `MessagesPage.tsx`) at the framework's conventional page location:
211
+
212
+ - React (Vite): `src/pages/ChatPage.tsx` or `src/ChatPage.tsx`
213
+ - Next.js (App Router): `app/chat/page.tsx`
214
+ - Next.js (Pages Router): `pages/chat.tsx`
215
+ - Astro: `src/pages/chat.astro` (with a React island)
216
+ - React Router: `app/routes/chat.tsx`
217
+
218
+ #### 3. Implement the page
219
+
220
+ Choose the experience composition from `cometchat-components`:
221
+
222
+ **Two-pane (most common for routes):**
223
+
224
+ ```tsx
225
+ // ChatPage.tsx
226
+ import { useState } from "react";
227
+ import {
228
+ CometChatConversations,
229
+ CometChatMessageHeader,
230
+ CometChatMessageList,
231
+ CometChatMessageComposer,
232
+ } from "@cometchat/chat-uikit-react";
233
+ import { CometChat } from "@cometchat/chat-sdk-javascript";
234
+
235
+ export default function ChatPage() {
236
+ const [selectedUser, setSelectedUser] = useState<CometChat.User>();
237
+ const [selectedGroup, setSelectedGroup] = useState<CometChat.Group>();
238
+
239
+ function handleConversationClick(conversation: CometChat.Conversation) {
240
+ const entity = conversation.getConversationWith();
241
+ if (entity instanceof CometChat.User) {
242
+ setSelectedUser(entity);
243
+ setSelectedGroup(undefined);
244
+ } else if (entity instanceof CometChat.Group) {
245
+ setSelectedUser(undefined);
246
+ setSelectedGroup(entity);
247
+ }
248
+ }
249
+
250
+ return (
251
+ <div style={{ display: "flex", height: "100vh" }}>
252
+ <div style={{ width: "360px", borderRight: "1px solid #eee" }}>
253
+ <CometChatConversations onItemClick={handleConversationClick} />
254
+ </div>
255
+ <div style={{ flex: 1, display: "flex", flexDirection: "column" }}>
256
+ {(selectedUser || selectedGroup) ? (
257
+ <>
258
+ {selectedUser && <CometChatMessageHeader user={selectedUser} />}
259
+ {selectedGroup && <CometChatMessageHeader group={selectedGroup} />}
260
+ {selectedUser && <CometChatMessageList user={selectedUser} hideReplyInThreadOption />}
261
+ {selectedGroup && <CometChatMessageList group={selectedGroup} hideReplyInThreadOption />}
262
+ {selectedUser && <CometChatMessageComposer user={selectedUser} />}
263
+ {selectedGroup && <CometChatMessageComposer group={selectedGroup} />}
264
+ </>
265
+ ) : (
266
+ <div style={{
267
+ flex: 1,
268
+ display: "flex",
269
+ alignItems: "center",
270
+ justifyContent: "center",
271
+ color: "#999",
272
+ }}>
273
+ Select a conversation to start chatting
274
+ </div>
275
+ )}
276
+ </div>
277
+ </div>
278
+ );
279
+ }
280
+ ```
281
+
282
+ **Full messenger (tabs -- for standalone messaging sections):**
283
+
284
+ ```tsx
285
+ // MessagesPage.tsx
286
+ import { useState } from "react";
287
+ import {
288
+ CometChatConversations,
289
+ CometChatCallLogs,
290
+ CometChatUsers,
291
+ CometChatGroups,
292
+ CometChatMessageHeader,
293
+ CometChatMessageList,
294
+ CometChatMessageComposer,
295
+ } from "@cometchat/chat-uikit-react";
296
+ import { CometChat } from "@cometchat/chat-sdk-javascript";
297
+
298
+ type Tab = "chats" | "calls" | "users" | "groups";
299
+
300
+ export default function MessagesPage() {
301
+ const [activeTab, setActiveTab] = useState<Tab>("chats");
302
+ const [selectedUser, setSelectedUser] = useState<CometChat.User>();
303
+ const [selectedGroup, setSelectedGroup] = useState<CometChat.Group>();
304
+
305
+ function selectUser(user: CometChat.User) {
306
+ setSelectedUser(user);
307
+ setSelectedGroup(undefined);
308
+ }
309
+ function selectGroup(group: CometChat.Group) {
310
+ setSelectedUser(undefined);
311
+ setSelectedGroup(group);
312
+ }
313
+
314
+ return (
315
+ <div style={{ display: "flex", height: "100vh" }}>
316
+ <div style={{ width: "360px", display: "flex", flexDirection: "column", borderRight: "1px solid #eee" }}>
317
+ <nav style={{ display: "flex", borderBottom: "1px solid #eee" }}>
318
+ {(["chats", "calls", "users", "groups"] as Tab[]).map((tab) => (
319
+ <button
320
+ key={tab}
321
+ onClick={() => setActiveTab(tab)}
322
+ style={{
323
+ flex: 1,
324
+ padding: "12px 0",
325
+ border: "none",
326
+ background: "none",
327
+ cursor: "pointer",
328
+ fontWeight: activeTab === tab ? 700 : 400,
329
+ borderBottom: activeTab === tab ? "2px solid var(--cometchat-primary-color, #3399ff)" : "2px solid transparent",
330
+ }}
331
+ >
332
+ {tab.charAt(0).toUpperCase() + tab.slice(1)}
333
+ </button>
334
+ ))}
335
+ </nav>
336
+ <div style={{ flex: 1, overflow: "hidden" }}>
337
+ {activeTab === "chats" && (
338
+ <CometChatConversations
339
+ onItemClick={(conv) => {
340
+ const entity = conv.getConversationWith();
341
+ if (entity instanceof CometChat.User) selectUser(entity);
342
+ else if (entity instanceof CometChat.Group) selectGroup(entity);
343
+ }}
344
+ />
345
+ )}
346
+ {activeTab === "calls" && <CometChatCallLogs />}
347
+ {activeTab === "users" && <CometChatUsers onItemClick={selectUser} />}
348
+ {activeTab === "groups" && <CometChatGroups onItemClick={selectGroup} />}
349
+ </div>
350
+ </div>
351
+ <div style={{ flex: 1, display: "flex", flexDirection: "column" }}>
352
+ {selectedUser && (
353
+ <>
354
+ <CometChatMessageHeader user={selectedUser} />
355
+ <CometChatMessageList user={selectedUser} hideReplyInThreadOption />
356
+ <CometChatMessageComposer user={selectedUser} />
357
+ </>
358
+ )}
359
+ {selectedGroup && (
360
+ <>
361
+ <CometChatMessageHeader group={selectedGroup} />
362
+ <CometChatMessageList group={selectedGroup} hideReplyInThreadOption />
363
+ <CometChatMessageComposer group={selectedGroup} />
364
+ </>
365
+ )}
366
+ {!selectedUser && !selectedGroup && (
367
+ <div style={{
368
+ flex: 1,
369
+ display: "flex",
370
+ alignItems: "center",
371
+ justifyContent: "center",
372
+ color: "#999",
373
+ }}>
374
+ Select a conversation to start chatting
375
+ </div>
376
+ )}
377
+ </div>
378
+ </div>
379
+ );
380
+ }
381
+ ```
382
+
383
+ #### 4. Wire the route into the project's router
384
+
385
+ **Read the project's existing routing setup first.** Do not assume a pattern. Look for:
386
+
387
+ - **React Router:** `createBrowserRouter()`, `<Routes>`, `<Route>` in `App.tsx` or a routes file
388
+ - **Next.js App Router:** `app/` directory -- just creating the page file at `app/chat/page.tsx` IS the route
389
+ - **Next.js Pages Router:** `pages/` directory -- creating `pages/chat.tsx` IS the route
390
+ - **Astro:** `src/pages/` directory -- creating `src/pages/chat.astro` IS the route
391
+ - **React Router v7:** File-based routing in `app/routes/` or manual routes in `app/routes.ts`
392
+
393
+ For manual routers (React Router), add a route entry:
394
+
395
+ ```tsx
396
+ // Example: adding to an existing createBrowserRouter
397
+ {
398
+ path: "/chat",
399
+ element: <ChatPage />,
400
+ }
401
+ ```
402
+
403
+ For file-based routers (Next.js, Astro, React Router v7), creating the file at the right path is sufficient.
404
+
405
+ #### 5. Add a navigation link
406
+
407
+ **Read the project's existing navbar/sidebar first.** Find the component that renders navigation links (could be `Navbar.tsx`, `Sidebar.tsx`, `Header.tsx`, `Nav.tsx`, or inline in a layout).
408
+
409
+ Add a "Messages" or "Chat" link alongside the existing links:
410
+
411
+ ```tsx
412
+ // Example: adding to an existing nav component
413
+ <Link to="/chat">Messages</Link>
414
+ // or
415
+ <a href="/chat">Messages</a>
416
+ ```
417
+
418
+ Match the existing link style. If the nav uses icons, add a chat/message icon. If it uses a specific `NavLink` or `Link` component, use the same one.
419
+
420
+ #### 6. Import CSS
421
+
422
+ Check if `@cometchat/chat-uikit-react/css-variables.css` is already imported at the app root. If not, add it to the root CSS file or root layout:
423
+
424
+ ```css
425
+ /* In globals.css or index.css at the app root */
426
+ @import "@cometchat/chat-uikit-react/css-variables.css";
427
+ ```
428
+
429
+ ---
430
+
431
+ ## Modal placement
432
+
433
+ A centered overlay for quick one-off messages. Use when chat is a secondary action (e.g., "message this user" from a profile page).
434
+
435
+ ### When to use modal vs. drawer
436
+
437
+ - **Modal:** Quick, one-off messages. User sends a message and closes. No ongoing conversation visible.
438
+ - **Drawer:** Ongoing conversation. User keeps the drawer open while browsing the main app. Better for marketplace/support contexts.
439
+
440
+ ### Steps
441
+
442
+ #### 1. Create a ChatModal component
443
+
444
+ ```tsx
445
+ // ChatModal.tsx
446
+ import { useEffect, useState } from "react";
447
+ import {
448
+ CometChatMessageHeader,
449
+ CometChatMessageList,
450
+ CometChatMessageComposer,
451
+ } from "@cometchat/chat-uikit-react";
452
+ import { CometChat } from "@cometchat/chat-sdk-javascript";
453
+
454
+ interface ChatModalProps {
455
+ isOpen: boolean;
456
+ onClose: () => void;
457
+ targetUserId?: string;
458
+ targetGroupId?: string;
459
+ }
460
+
461
+ export function ChatModal({ isOpen, onClose, targetUserId, targetGroupId }: ChatModalProps) {
462
+ const [user, setUser] = useState<CometChat.User>();
463
+ const [group, setGroup] = useState<CometChat.Group>();
464
+ const [loading, setLoading] = useState(true);
465
+
466
+ useEffect(() => {
467
+ if (!isOpen) return;
468
+ setLoading(true);
469
+
470
+ if (targetUserId) {
471
+ CometChat.getUser(targetUserId)
472
+ .then((u) => {
473
+ setUser(u);
474
+ setGroup(undefined);
475
+ setLoading(false);
476
+ })
477
+ .catch(() => setLoading(false));
478
+ } else if (targetGroupId) {
479
+ CometChat.getGroup(targetGroupId)
480
+ .then((g) => {
481
+ setUser(undefined);
482
+ setGroup(g);
483
+ setLoading(false);
484
+ })
485
+ .catch(() => setLoading(false));
486
+ }
487
+ }, [isOpen, targetUserId, targetGroupId]);
488
+
489
+ if (!isOpen) return null;
490
+
491
+ return (
492
+ <div
493
+ style={{
494
+ position: "fixed",
495
+ inset: 0,
496
+ zIndex: 1000,
497
+ display: "flex",
498
+ alignItems: "center",
499
+ justifyContent: "center",
500
+ }}
501
+ >
502
+ {/* Backdrop */}
503
+ <div
504
+ onClick={onClose}
505
+ style={{
506
+ position: "absolute",
507
+ inset: 0,
508
+ backgroundColor: "rgba(0, 0, 0, 0.5)",
509
+ }}
510
+ />
511
+
512
+ {/* Modal content */}
513
+ <div
514
+ style={{
515
+ position: "relative",
516
+ width: "min(600px, 90vw)",
517
+ height: "min(700px, 80vh)",
518
+ backgroundColor: "var(--cometchat-background-color-01, #fff)",
519
+ borderRadius: "var(--cometchat-border-radius-lg, 12px)",
520
+ overflow: "hidden",
521
+ display: "flex",
522
+ flexDirection: "column",
523
+ boxShadow: "0 20px 60px rgba(0, 0, 0, 0.3)",
524
+ }}
525
+ >
526
+ {/* Close button */}
527
+ <button
528
+ onClick={onClose}
529
+ style={{
530
+ position: "absolute",
531
+ top: 8,
532
+ right: 8,
533
+ zIndex: 10,
534
+ background: "none",
535
+ border: "none",
536
+ fontSize: 20,
537
+ cursor: "pointer",
538
+ padding: "4px 8px",
539
+ }}
540
+ aria-label="Close chat"
541
+ >
542
+ X
543
+ </button>
544
+
545
+ {loading ? (
546
+ <div style={{ flex: 1, display: "flex", alignItems: "center", justifyContent: "center" }}>
547
+ Loading...
548
+ </div>
549
+ ) : (
550
+ <>
551
+ {user && <CometChatMessageHeader user={user} />}
552
+ {group && <CometChatMessageHeader group={group} />}
553
+ <div style={{ flex: 1, overflow: "hidden" }}>
554
+ {user && <CometChatMessageList user={user} hideReplyInThreadOption />}
555
+ {group && <CometChatMessageList group={group} hideReplyInThreadOption />}
556
+ </div>
557
+ {user && <CometChatMessageComposer user={user} />}
558
+ {group && <CometChatMessageComposer group={group} />}
559
+ </>
560
+ )}
561
+ </div>
562
+ </div>
563
+ );
564
+ }
565
+ ```
566
+
567
+ #### 2. Wire the trigger
568
+
569
+ **Read the project's components to find the right trigger point.** This could be:
570
+
571
+ - A "Message" button on a user profile page
572
+ - A "Contact Seller" button on a product card
573
+ - A chat icon in a navbar
574
+ - A "Send Message" link in a user list
575
+
576
+ ```tsx
577
+ // Example: adding a chat button to an existing product card
578
+ import { useState } from "react";
579
+ import { ChatModal } from "./ChatModal";
580
+
581
+ function ProductCard({ product }) {
582
+ const [showChat, setShowChat] = useState(false);
583
+
584
+ return (
585
+ <div>
586
+ {/* existing product card content */}
587
+ <button onClick={() => setShowChat(true)}>
588
+ Message Seller
589
+ </button>
590
+ <ChatModal
591
+ isOpen={showChat}
592
+ onClose={() => setShowChat(false)}
593
+ targetUserId={product.sellerId}
594
+ />
595
+ </div>
596
+ );
597
+ }
598
+ ```
599
+
600
+ #### 3. CometChatProvider placement
601
+
602
+ The `CometChatProvider` (or equivalent init logic) MUST be at the app root, NOT inside the modal. If init is inside the modal, it re-runs every time the modal opens, causing flicker and reconnection delays.
603
+
604
+ ---
605
+
606
+ ## Drawer placement
607
+
608
+ A side panel that slides in from the right. Better than a modal for ongoing conversations because the user can keep it open while browsing.
609
+
610
+ ### Steps
611
+
612
+ #### 1. Create a ChatDrawer component
613
+
614
+ ```tsx
615
+ // ChatDrawer.tsx
616
+ import { useEffect, useState } from "react";
617
+ import {
618
+ CometChatMessageHeader,
619
+ CometChatMessageList,
620
+ CometChatMessageComposer,
621
+ } from "@cometchat/chat-uikit-react";
622
+ import { CometChat } from "@cometchat/chat-sdk-javascript";
623
+
624
+ interface ChatDrawerProps {
625
+ isOpen: boolean;
626
+ onClose: () => void;
627
+ targetUserId?: string;
628
+ targetGroupId?: string;
629
+ }
630
+
631
+ export function ChatDrawer({ isOpen, onClose, targetUserId, targetGroupId }: ChatDrawerProps) {
632
+ const [user, setUser] = useState<CometChat.User>();
633
+ const [group, setGroup] = useState<CometChat.Group>();
634
+ const [loading, setLoading] = useState(true);
635
+
636
+ useEffect(() => {
637
+ if (!isOpen) return;
638
+ setLoading(true);
639
+
640
+ if (targetUserId) {
641
+ CometChat.getUser(targetUserId)
642
+ .then((u) => {
643
+ setUser(u);
644
+ setGroup(undefined);
645
+ setLoading(false);
646
+ })
647
+ .catch(() => setLoading(false));
648
+ } else if (targetGroupId) {
649
+ CometChat.getGroup(targetGroupId)
650
+ .then((g) => {
651
+ setUser(undefined);
652
+ setGroup(g);
653
+ setLoading(false);
654
+ })
655
+ .catch(() => setLoading(false));
656
+ }
657
+ }, [isOpen, targetUserId, targetGroupId]);
658
+
659
+ return (
660
+ <>
661
+ {/* Backdrop */}
662
+ {isOpen && (
663
+ <div
664
+ onClick={onClose}
665
+ style={{
666
+ position: "fixed",
667
+ inset: 0,
668
+ zIndex: 999,
669
+ backgroundColor: "rgba(0, 0, 0, 0.3)",
670
+ }}
671
+ />
672
+ )}
673
+
674
+ {/* Drawer */}
675
+ {/*
676
+ IMPORTANT: do NOT animate with `transform: translateX(...)`.
677
+ `transform` on an element creates a new containing block for
678
+ `position: fixed` descendants (per the CSS spec), so every
679
+ fixed-positioned popover that CometChat renders inside the drawer
680
+ — message options menu, emoji picker, file preview, reactions
681
+ popover, thread panel — becomes anchored to the transformed
682
+ drawer instead of the viewport. The result is popovers that
683
+ appear clipped, offset, or drift as the drawer animates. Animate
684
+ the `right` offset instead; no transform, no containing-block
685
+ takeover, fixed popovers stay anchored to the viewport.
686
+ */}
687
+ <div
688
+ style={{
689
+ position: "fixed",
690
+ top: 0,
691
+ right: isOpen ? 0 : "-400px", // matches width; slides off-screen when closed
692
+ bottom: 0,
693
+ width: "400px",
694
+ maxWidth: "100vw",
695
+ zIndex: 1000,
696
+ backgroundColor: "var(--cometchat-background-color-01, #fff)",
697
+ boxShadow: "-4px 0 20px rgba(0, 0, 0, 0.15)",
698
+ display: "flex",
699
+ flexDirection: "column",
700
+ transition: "right 0.3s ease-in-out",
701
+ }}
702
+ >
703
+ {/* Header with close button */}
704
+ <div style={{
705
+ display: "flex",
706
+ alignItems: "center",
707
+ justifyContent: "space-between",
708
+ padding: "8px 12px",
709
+ borderBottom: "1px solid #eee",
710
+ }}>
711
+ <span style={{ fontWeight: 600 }}>Chat</span>
712
+ <button
713
+ onClick={onClose}
714
+ style={{
715
+ background: "none",
716
+ border: "none",
717
+ fontSize: 18,
718
+ cursor: "pointer",
719
+ padding: "4px 8px",
720
+ }}
721
+ aria-label="Close chat"
722
+ >
723
+ X
724
+ </button>
725
+ </div>
726
+
727
+ {/* Chat content */}
728
+ {loading ? (
729
+ <div style={{ flex: 1, display: "flex", alignItems: "center", justifyContent: "center" }}>
730
+ Loading...
731
+ </div>
732
+ ) : (
733
+ <>
734
+ {user && <CometChatMessageHeader user={user} />}
735
+ {group && <CometChatMessageHeader group={group} />}
736
+ <div style={{ flex: 1, overflow: "hidden" }}>
737
+ {user && <CometChatMessageList user={user} hideReplyInThreadOption />}
738
+ {group && <CometChatMessageList group={group} hideReplyInThreadOption />}
739
+ </div>
740
+ {user && <CometChatMessageComposer user={user} />}
741
+ {group && <CometChatMessageComposer group={group} />}
742
+ </>
743
+ )}
744
+ </div>
745
+ </>
746
+ );
747
+ }
748
+ ```
749
+
750
+ #### 2. Wire the trigger
751
+
752
+ Same approach as the modal -- find the right trigger point in the existing project:
753
+
754
+ ```tsx
755
+ import { useState } from "react";
756
+ import { ChatDrawer } from "./ChatDrawer";
757
+
758
+ function UserProfile({ userId }) {
759
+ const [showChat, setShowChat] = useState(false);
760
+
761
+ return (
762
+ <div>
763
+ {/* existing profile content */}
764
+ <button onClick={() => setShowChat(true)}>
765
+ Chat with this user
766
+ </button>
767
+ <ChatDrawer
768
+ isOpen={showChat}
769
+ onClose={() => setShowChat(false)}
770
+ targetUserId={userId}
771
+ />
772
+ </div>
773
+ );
774
+ }
775
+ ```
776
+
777
+ #### 3. Multi-conversation drawer variant
778
+
779
+ For a drawer that shows the full conversation list (not just a single thread):
780
+
781
+ ```tsx
782
+ // ConversationDrawer.tsx -- shows conversation list + message view
783
+ import { useState } from "react";
784
+ import {
785
+ CometChatConversations,
786
+ CometChatMessageHeader,
787
+ CometChatMessageList,
788
+ CometChatMessageComposer,
789
+ } from "@cometchat/chat-uikit-react";
790
+ import { CometChat } from "@cometchat/chat-sdk-javascript";
791
+
792
+ interface ConversationDrawerProps {
793
+ isOpen: boolean;
794
+ onClose: () => void;
795
+ }
796
+
797
+ export function ConversationDrawer({ isOpen, onClose }: ConversationDrawerProps) {
798
+ const [selectedUser, setSelectedUser] = useState<CometChat.User>();
799
+ const [selectedGroup, setSelectedGroup] = useState<CometChat.Group>();
800
+
801
+ function handleConversationClick(conversation: CometChat.Conversation) {
802
+ const entity = conversation.getConversationWith();
803
+ if (entity instanceof CometChat.User) {
804
+ setSelectedUser(entity);
805
+ setSelectedGroup(undefined);
806
+ } else if (entity instanceof CometChat.Group) {
807
+ setSelectedUser(undefined);
808
+ setSelectedGroup(entity);
809
+ }
810
+ }
811
+
812
+ const hasSelection = selectedUser || selectedGroup;
813
+
814
+ return (
815
+ <>
816
+ {isOpen && (
817
+ <div
818
+ onClick={onClose}
819
+ style={{ position: "fixed", inset: 0, zIndex: 999, backgroundColor: "rgba(0,0,0,0.3)" }}
820
+ />
821
+ )}
822
+ {/*
823
+ Animate the `right` offset, never `transform: translateX(...)` —
824
+ `transform` creates a new containing block, which re-anchors
825
+ CometChat's fixed-positioned popovers (emoji picker, message
826
+ options, file preview, thread panel) to the drawer instead of
827
+ the viewport and makes them misalign.
828
+ */}
829
+ <div
830
+ style={{
831
+ position: "fixed",
832
+ top: 0,
833
+ right: isOpen ? 0 : "-720px", // off-screen by widest width when closed
834
+ bottom: 0,
835
+ width: hasSelection ? "720px" : "360px",
836
+ maxWidth: "100vw",
837
+ zIndex: 1000,
838
+ backgroundColor: "var(--cometchat-background-color-01, #fff)",
839
+ boxShadow: "-4px 0 20px rgba(0,0,0,0.15)",
840
+ display: "flex",
841
+ transition: "right 0.3s ease-in-out, width 0.3s ease-in-out",
842
+ }}
843
+ >
844
+ {/* Conversation list */}
845
+ <div style={{ width: "360px", borderRight: hasSelection ? "1px solid #eee" : "none", display: "flex", flexDirection: "column" }}>
846
+ <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", padding: "8px 12px", borderBottom: "1px solid #eee" }}>
847
+ <span style={{ fontWeight: 600 }}>Messages</span>
848
+ <button onClick={onClose} style={{ background: "none", border: "none", fontSize: 18, cursor: "pointer" }} aria-label="Close">&times;</button>
849
+ </div>
850
+ <div style={{ flex: 1 }}>
851
+ <CometChatConversations onItemClick={handleConversationClick} />
852
+ </div>
853
+ </div>
854
+
855
+ {/* Message view */}
856
+ {hasSelection && (
857
+ <div style={{ flex: 1, display: "flex", flexDirection: "column" }}>
858
+ {selectedUser && <CometChatMessageHeader user={selectedUser} />}
859
+ {selectedGroup && <CometChatMessageHeader group={selectedGroup} />}
860
+ <div style={{ flex: 1, overflow: "hidden" }}>
861
+ {selectedUser && <CometChatMessageList user={selectedUser} hideReplyInThreadOption />}
862
+ {selectedGroup && <CometChatMessageList group={selectedGroup} hideReplyInThreadOption />}
863
+ </div>
864
+ {selectedUser && <CometChatMessageComposer user={selectedUser} />}
865
+ {selectedGroup && <CometChatMessageComposer group={selectedGroup} />}
866
+ </div>
867
+ )}
868
+ </div>
869
+ </>
870
+ );
871
+ }
872
+ ```
873
+
874
+ ---
875
+
876
+ ## Floating widget
877
+
878
+ A button-in-the-corner chat overlay. Available on every page of the app
879
+ without a dedicated route. Common for support widgets, helpdesk chat,
880
+ "contact us" overlays.
881
+
882
+ **When to use:** chat is a secondary concern (not the core product), and
883
+ should be accessible from anywhere without navigating. **When NOT to use:**
884
+ if chat is central to the app, use a route placement instead — widgets
885
+ don't scale to inbox-style usage.
886
+
887
+ ### Steps
888
+
889
+ #### 1. Create the ChatWidget component
890
+
891
+ ```tsx
892
+ // src/components/ChatWidget.tsx (or components/ChatWidget.tsx in Next.js)
893
+ import { useState } from "react";
894
+ import {
895
+ CometChatConversations,
896
+ CometChatMessageHeader,
897
+ CometChatMessageList,
898
+ CometChatMessageComposer,
899
+ } from "@cometchat/chat-uikit-react";
900
+ import { CometChat } from "@cometchat/chat-sdk-javascript";
901
+
902
+ export default function ChatWidget() {
903
+ const [open, setOpen] = useState(false);
904
+ const [selected, setSelected] = useState<CometChat.User | CometChat.Group>();
905
+
906
+ function handleConversationClick(conv: CometChat.Conversation) {
907
+ const entity = conv.getConversationWith();
908
+ if (entity instanceof CometChat.User || entity instanceof CometChat.Group) {
909
+ setSelected(entity);
910
+ }
911
+ }
912
+
913
+ return (
914
+ <>
915
+ {/* Floating trigger button — always visible */}
916
+ <button
917
+ type="button"
918
+ aria-label={open ? "Close chat" : "Open chat"}
919
+ onClick={() => setOpen((v) => !v)}
920
+ style={{
921
+ position: "fixed",
922
+ bottom: 24,
923
+ right: 24,
924
+ width: 56,
925
+ height: 56,
926
+ borderRadius: "50%",
927
+ background: "var(--cometchat-primary-color, #6c63ff)",
928
+ color: "white",
929
+ border: "none",
930
+ cursor: "pointer",
931
+ fontSize: 24,
932
+ boxShadow: "0 4px 16px rgba(0, 0, 0, 0.15)",
933
+ zIndex: 9999,
934
+ }}
935
+ >
936
+ {open ? "×" : "💬"}
937
+ </button>
938
+
939
+ {/* Widget panel — overlay, not a full-page drawer */}
940
+ {open && (
941
+ <div
942
+ style={{
943
+ position: "fixed",
944
+ bottom: 96, // leave room for the button
945
+ right: 24,
946
+ width: "min(380px, calc(100vw - 48px))",
947
+ height: "min(600px, calc(100vh - 120px))",
948
+ background: "var(--cometchat-background-color-01, white)",
949
+ border: "1px solid var(--cometchat-border-color-light, #eee)",
950
+ borderRadius: 12,
951
+ boxShadow: "0 8px 32px rgba(0, 0, 0, 0.2)",
952
+ display: "flex",
953
+ flexDirection: "column",
954
+ overflow: "hidden",
955
+ zIndex: 9998,
956
+ }}
957
+ >
958
+ {selected ? (
959
+ <>
960
+ <button
961
+ onClick={() => setSelected(undefined)}
962
+ style={{ alignSelf: "flex-start", background: "none", border: "none", padding: 12, cursor: "pointer" }}
963
+ >
964
+ ← Back
965
+ </button>
966
+ {selected instanceof CometChat.User && (
967
+ <>
968
+ <CometChatMessageHeader user={selected} />
969
+ <div style={{ flex: 1, overflow: "hidden" }}>
970
+ <CometChatMessageList user={selected} hideReplyInThreadOption />
971
+ </div>
972
+ <CometChatMessageComposer user={selected} />
973
+ </>
974
+ )}
975
+ {selected instanceof CometChat.Group && (
976
+ <>
977
+ <CometChatMessageHeader group={selected} />
978
+ <div style={{ flex: 1, overflow: "hidden" }}>
979
+ <CometChatMessageList group={selected} hideReplyInThreadOption />
980
+ </div>
981
+ <CometChatMessageComposer group={selected} />
982
+ </>
983
+ )}
984
+ </>
985
+ ) : (
986
+ <CometChatConversations onItemClick={handleConversationClick} />
987
+ )}
988
+ </div>
989
+ )}
990
+ </>
991
+ );
992
+ }
993
+ ```
994
+
995
+ **Key points:**
996
+ - Button and panel are both `position: fixed`. `z-index: 9999` / `9998` ensures they float above the app's content but don't conflict with modals (which typically go to `z-index: 10000+`).
997
+ - Width/height clamp against viewport (`min(...)`) so the widget shrinks gracefully on mobile. On screens < 428px you may want to switch to full-screen (`width: 100vw; height: 100vh; bottom: 0; right: 0`) — gate with a `useEffect` + `window.innerWidth` or CSS media query.
998
+ - The panel renders conversation list by default; clicking a conversation swaps to the single-thread view with a back arrow. Mirrors WhatsApp / iMessage widget UX.
999
+ - Wrap everything in `{open && (...)}` rather than animating transforms — the CometChat components subscribe to SDK events on mount, so keeping the panel in the tree when closed wastes resources.
1000
+
1001
+ #### 2. Mount at the app root
1002
+
1003
+ Where to mount depends on the framework. The widget must be a sibling (not a descendant) of the app's main layout so its fixed positioning escapes any overflow-hidden containers.
1004
+
1005
+ | Framework | Mount location |
1006
+ |---|---|
1007
+ | React (Vite / CRA) | `src/main.tsx` or `src/App.tsx`, sibling of `<Routes>` |
1008
+ | Next.js App Router | `app/layout.tsx`, sibling of `{children}` (inside `<body>`, outside any main container with `overflow: hidden`) |
1009
+ | Next.js Pages Router | `pages/_app.tsx`, sibling of `<Component />` |
1010
+ | React Router | `app/root.tsx` (v7) or wherever `<Outlet />` lives (v6) |
1011
+ | Astro | Inside a `client:only="react"` island in `src/layouts/BaseLayout.astro` |
1012
+
1013
+ **Example — Next.js App Router:**
1014
+
1015
+ ```tsx
1016
+ // app/layout.tsx
1017
+ import ChatWidget from "@/components/ChatWidget";
1018
+
1019
+ export default function RootLayout({ children }) {
1020
+ return (
1021
+ <html lang="en">
1022
+ <body>
1023
+ {children}
1024
+ <ChatWidget />
1025
+ </body>
1026
+ </html>
1027
+ );
1028
+ }
1029
+ ```
1030
+
1031
+ In the Pages Router and Vite cases, the provider (`CometChatProvider`) must wrap both the app content *and* the widget — otherwise the widget won't have access to the init'd SDK.
1032
+
1033
+ #### 3. Conditional rendering per route (optional)
1034
+
1035
+ Most apps want to hide the widget on auth pages (login, signup, password reset). Options:
1036
+
1037
+ **React Router / Next.js** — use `useLocation()` / `usePathname()`:
1038
+
1039
+ ```tsx
1040
+ // Wrap the export with a router-aware gate
1041
+ import { useLocation } from "react-router-dom"; // or `usePathname` in Next.js
1042
+
1043
+ export default function ChatWidgetGate() {
1044
+ const pathname = useLocation().pathname;
1045
+ const hiddenOn = ["/login", "/signup", "/forgot-password"];
1046
+ if (hiddenOn.some((p) => pathname.startsWith(p))) return null;
1047
+ return <ChatWidget />;
1048
+ }
1049
+ ```
1050
+
1051
+ **Astro** — render the island only on the pages that want it, rather than globally in the layout.
1052
+
1053
+ #### 4. Feature toggle via config
1054
+
1055
+ Record the choice in `.cometchat/config.json` so the integration skill knows the widget was used:
1056
+
1057
+ ```bash
1058
+ npx @cometchat/skills-cli config save --placement widget --json
1059
+ ```
1060
+
1061
+ **Do not invoke `cometchat add-widget`** — that's a v2 CLI command that writes a template-based widget. v3 skills write the widget directly using the pattern above, so it fits the project's existing code style. `add-widget` is retained only for backward compatibility with v2 integrations.
1062
+
1063
+ ---
1064
+
1065
+ ## Embedded placement
1066
+
1067
+ Chat embedded directly in an existing page, alongside other content. Common for marketplace product pages, dashboards, or support panels.
1068
+
1069
+ ### Steps
1070
+
1071
+ #### 1. Create a ChatPanel component
1072
+
1073
+ The critical thing about embedded placement is the container MUST have an explicit height. CometChat components fill 100% of their container -- if the container has no height constraint, the components either collapse to zero or overflow the page.
1074
+
1075
+ ```tsx
1076
+ // ChatPanel.tsx
1077
+ import { useEffect, useState } from "react";
1078
+ import {
1079
+ CometChatMessageHeader,
1080
+ CometChatMessageList,
1081
+ CometChatMessageComposer,
1082
+ } from "@cometchat/chat-uikit-react";
1083
+ import { CometChat } from "@cometchat/chat-sdk-javascript";
1084
+
1085
+ interface ChatPanelProps {
1086
+ targetUserId?: string;
1087
+ targetGroupId?: string;
1088
+ height?: string; // e.g., "500px", "60vh"
1089
+ conversationMode?: boolean; // Show conversation list instead of single thread
1090
+ }
1091
+
1092
+ export function ChatPanel({
1093
+ targetUserId,
1094
+ targetGroupId,
1095
+ height = "500px",
1096
+ }: ChatPanelProps) {
1097
+ const [user, setUser] = useState<CometChat.User>();
1098
+ const [group, setGroup] = useState<CometChat.Group>();
1099
+ const [loading, setLoading] = useState(true);
1100
+
1101
+ useEffect(() => {
1102
+ if (targetUserId) {
1103
+ CometChat.getUser(targetUserId)
1104
+ .then((u) => {
1105
+ setUser(u);
1106
+ setGroup(undefined);
1107
+ setLoading(false);
1108
+ })
1109
+ .catch(() => setLoading(false));
1110
+ } else if (targetGroupId) {
1111
+ CometChat.getGroup(targetGroupId)
1112
+ .then((g) => {
1113
+ setUser(undefined);
1114
+ setGroup(g);
1115
+ setLoading(false);
1116
+ })
1117
+ .catch(() => setLoading(false));
1118
+ }
1119
+ }, [targetUserId, targetGroupId]);
1120
+
1121
+ return (
1122
+ <div
1123
+ style={{
1124
+ height,
1125
+ display: "flex",
1126
+ flexDirection: "column",
1127
+ border: "1px solid #eee",
1128
+ borderRadius: "var(--cometchat-border-radius-lg, 8px)",
1129
+ overflow: "hidden",
1130
+ }}
1131
+ >
1132
+ {loading ? (
1133
+ <div style={{ flex: 1, display: "flex", alignItems: "center", justifyContent: "center" }}>
1134
+ Loading chat...
1135
+ </div>
1136
+ ) : (
1137
+ <>
1138
+ {user && <CometChatMessageHeader user={user} />}
1139
+ {group && <CometChatMessageHeader group={group} />}
1140
+ <div style={{ flex: 1, overflow: "hidden" }}>
1141
+ {user && <CometChatMessageList user={user} hideReplyInThreadOption />}
1142
+ {group && <CometChatMessageList group={group} hideReplyInThreadOption />}
1143
+ </div>
1144
+ {user && <CometChatMessageComposer user={user} />}
1145
+ {group && <CometChatMessageComposer group={group} />}
1146
+ </>
1147
+ )}
1148
+ </div>
1149
+ );
1150
+ }
1151
+ ```
1152
+
1153
+ #### 2. Find the container in the existing page
1154
+
1155
+ **Read the existing page where chat should be embedded.** Understand the layout before adding anything. Common patterns:
1156
+
1157
+ - **Product page:** Chat panel below or beside the product description
1158
+ - **Dashboard:** Chat panel in a sidebar or a dashboard card
1159
+ - **Profile page:** Chat panel as a tab or section
1160
+
1161
+ #### 3. Render the panel with a target
1162
+
1163
+ Connect the target user/group based on the page's data:
1164
+
1165
+ ```tsx
1166
+ // Example: embedding on a product page
1167
+ function ProductPage({ product }) {
1168
+ return (
1169
+ <div style={{ display: "flex", gap: 24 }}>
1170
+ <div style={{ flex: 1 }}>
1171
+ <h1>{product.name}</h1>
1172
+ <p>{product.description}</p>
1173
+ {/* other product content */}
1174
+ </div>
1175
+ <div style={{ width: 400 }}>
1176
+ <h3>Chat with the seller</h3>
1177
+ <ChatPanel
1178
+ targetUserId={product.sellerId}
1179
+ height="500px"
1180
+ />
1181
+ </div>
1182
+ </div>
1183
+ );
1184
+ }
1185
+ ```
1186
+
1187
+ #### 4. Conversation-list embedded panel
1188
+
1189
+ If you want an embedded panel with a conversation list (not just a single thread), use the multi-conversation composition from `cometchat-components` inside a container with explicit height:
1190
+
1191
+ ```tsx
1192
+ // EmbeddedInbox.tsx
1193
+ import { useState } from "react";
1194
+ import {
1195
+ CometChatConversations,
1196
+ CometChatMessageHeader,
1197
+ CometChatMessageList,
1198
+ CometChatMessageComposer,
1199
+ } from "@cometchat/chat-uikit-react";
1200
+ import { CometChat } from "@cometchat/chat-sdk-javascript";
1201
+
1202
+ interface EmbeddedInboxProps {
1203
+ height?: string;
1204
+ }
1205
+
1206
+ export function EmbeddedInbox({ height = "600px" }: EmbeddedInboxProps) {
1207
+ const [selectedUser, setSelectedUser] = useState<CometChat.User>();
1208
+ const [selectedGroup, setSelectedGroup] = useState<CometChat.Group>();
1209
+
1210
+ function handleConversationClick(conversation: CometChat.Conversation) {
1211
+ const entity = conversation.getConversationWith();
1212
+ if (entity instanceof CometChat.User) {
1213
+ setSelectedUser(entity);
1214
+ setSelectedGroup(undefined);
1215
+ } else if (entity instanceof CometChat.Group) {
1216
+ setSelectedUser(undefined);
1217
+ setSelectedGroup(entity);
1218
+ }
1219
+ }
1220
+
1221
+ return (
1222
+ <div style={{ height, display: "flex", border: "1px solid #eee", borderRadius: 8, overflow: "hidden" }}>
1223
+ <div style={{ width: "300px", borderRight: "1px solid #eee" }}>
1224
+ <CometChatConversations onItemClick={handleConversationClick} />
1225
+ </div>
1226
+ <div style={{ flex: 1, display: "flex", flexDirection: "column" }}>
1227
+ {selectedUser && (
1228
+ <>
1229
+ <CometChatMessageHeader user={selectedUser} />
1230
+ <div style={{ flex: 1, overflow: "hidden" }}>
1231
+ <CometChatMessageList user={selectedUser} hideReplyInThreadOption />
1232
+ </div>
1233
+ <CometChatMessageComposer user={selectedUser} />
1234
+ </>
1235
+ )}
1236
+ {selectedGroup && (
1237
+ <>
1238
+ <CometChatMessageHeader group={selectedGroup} />
1239
+ <div style={{ flex: 1, overflow: "hidden" }}>
1240
+ <CometChatMessageList group={selectedGroup} hideReplyInThreadOption />
1241
+ </div>
1242
+ <CometChatMessageComposer group={selectedGroup} />
1243
+ </>
1244
+ )}
1245
+ {!selectedUser && !selectedGroup && (
1246
+ <div style={{ flex: 1, display: "flex", alignItems: "center", justifyContent: "center", color: "#999" }}>
1247
+ Select a conversation
1248
+ </div>
1249
+ )}
1250
+ </div>
1251
+ </div>
1252
+ );
1253
+ }
1254
+ ```
1255
+
1256
+ ---
1257
+
1258
+ ## Hard rules
1259
+
1260
+ These rules apply to ALL placement patterns. Violating any of them causes integration bugs or destroys the user's existing project.
1261
+
1262
+ 1. **NEVER replace the project's existing entry file** (`App.tsx`, `page.tsx`, `layout.tsx`, `main.tsx`, etc.) unless the user explicitly chose "demo mode." Replacing the entry file destroys all existing functionality.
1263
+
1264
+ 2. **ALWAYS read the project's existing files before deciding where to put things.** Do not assume a project structure. Read the router config, the nav component, the layout files. Understand what exists before adding to it.
1265
+
1266
+ 3. **ALWAYS create new files alongside existing ones.** Do not modify files you do not fully understand. The exceptions are:
1267
+ - The router config (to add a route) -- read it first
1268
+ - The nav component (to add a link) -- read it first
1269
+ - The root CSS file (to add the css-variables.css import) -- read it first
1270
+
1271
+ 4. **CometChat CSS must be imported exactly once.** Before adding the import, check if it already exists:
1272
+ ```bash
1273
+ grep -r "css-variables.css" src/ app/ pages/ 2>/dev/null
1274
+ ```
1275
+ If it is already imported, do not add a duplicate.
1276
+
1277
+ 5. **CometChatProvider or init must be at the app root**, not inside a modal/drawer/panel. Re-initializing on every open causes flicker, dropped WebSocket connections, and race conditions.
1278
+
1279
+ 6. **Every CometChat container must have explicit dimensions.** Components fill 100% of their parent. If the parent has no height, the components collapse to zero. Always set `height`, `min-height`, or use flex/grid layout with a bounded container.
1280
+
1281
+ 7. **Resolve target users/groups before rendering CometChat components.** Use `CometChat.getUser(uid)` or `CometChat.getGroup(guid)` to get the full `CometChat.User` or `CometChat.Group` object. Do not pass a raw UID string to `user` props -- they expect object instances.
1282
+
1283
+ 8. **For SSR frameworks, wrap CometChat components appropriately.** See the `cometchat-core` skill, section 5 (SSR safety), for framework-specific patterns.
1284
+
1285
+ 9. **Every `<CometChatMessageList>` MUST include `hideReplyInThreadOption`** unless the integration also wires a thread panel (`CometChatThreadHeader` + scoped `MessageList` + scoped `MessageComposer` with `parentMessageId`). The kit's default (`false`) puts a "Reply in Thread" entry in the message action menu that silently does nothing when no panel is wired. Drawer, widget, modal, and embed patterns never wire a thread panel, so the prop is mandatory there. Two-pane route patterns (full messenger, social) MAY omit it if-and-only-if they implement the full thread-panel plumbing; otherwise keep it. Writing `<CometChatMessageList user={user} />` without the prop in a drawer or widget is a generation bug — every example in this skill includes it for a reason.
1286
+
1287
+ 10. **Never animate a CometChat-containing element with `transform`, `translate-*`, or any `transition-transform`.** This includes:
1288
+ - Inline `transform: translateX(...)`, `transform: scale(...)`, etc.
1289
+ - Tailwind utilities `translate-x-*`, `-translate-x-*`, `translate-x-0`, `translate-x-full`, `translate-y-*`, `scale-*`, `rotate-*`, `skew-*`, `transform-*`
1290
+ - `transition-transform`, `motion-safe:translate-*`, `will-change: transform`
1291
+ - `filter`, `perspective`, `backdrop-filter`
1292
+
1293
+ Any of these creates a new containing block for `position: fixed` descendants, which reparents CometChat's fixed-positioned popovers (emoji picker, message options menu, file preview, reactions popover, thread panel) to the animated container instead of the viewport. Animate the `right` / `left` offset instead (inline: `right: isOpen ? 0 : '-420px'`; Tailwind: `right-0` / `right-[-420px]` with `transition-[right]`). See `cometchat-core` § 8 anti-pattern 11 for the full CSS-spec explanation.