@mademi_dev/chatemi 1.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ChatEmi contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,451 @@
1
+ # ChatEmi
2
+
3
+ ChatEmi is a publish-ready React messaging package for building in-app messenger experiences. It includes:
4
+
5
+ - A typed REST API client for conversations, messages, attachments, reactions, read receipts, and search.
6
+ - A typed WebSocket client with reconnects, outbound queuing, typing, presence, receipts, and realtime conversation/message events.
7
+ - Group, channel, direct, and bot conversation models with owner/admin/member roles.
8
+ - Delivered/read receipts, last-seen presence, replies, forwards, avatars, voice messages, images, videos, and files.
9
+ - A modern floating launcher with notification badge, draggable/resizable modal, and compact notification tray.
10
+ - Optional external user-directory API integration.
11
+ - Optional server-side MongoDB connection helpers for API backends.
12
+ - `ChatEmiProvider` for application layout/state.
13
+ - `useChatEmi` for product code that needs chat actions and state.
14
+ - `ChatEmiMessenger`, a default responsive light/dark Telegram-style UI that can be used immediately or customized.
15
+
16
+ ## Install
17
+
18
+ ```bash
19
+ npm install chatemi
20
+ ```
21
+
22
+ ## Documentation and examples
23
+
24
+ - Full implementation guide: [`docs/IMPLEMENTATION_GUIDE.md`](docs/IMPLEMENTATION_GUIDE.md)
25
+ - Backend REST/WebSocket contract: [`docs/BACKEND_CONTRACT.md`](docs/BACKEND_CONTRACT.md)
26
+ - Next.js launcher example: [`examples/nextjs-chat-widget`](examples/nextjs-chat-widget)
27
+
28
+ ```tsx
29
+ import { ChatEmiLauncher, ChatEmiMessenger, ChatEmiProvider, useChatEmi } from "chatemi";
30
+ import "chatemi/styles.css";
31
+
32
+ export function App() {
33
+ return (
34
+ <ChatEmiProvider
35
+ config={{
36
+ apiBaseUrl: "https://api.example.com/chat",
37
+ socketUrl: "wss://api.example.com/chat/socket",
38
+ token: () => localStorage.getItem("access_token") ?? undefined,
39
+ theme: "violet",
40
+ notifications: {
41
+ enabled: true,
42
+ browser: true,
43
+ maxStored: 50
44
+ },
45
+ userDirectory: {
46
+ baseUrl: "https://identity.example.com",
47
+ searchPath: "/users/search",
48
+ headers: () => ({
49
+ Authorization: `Bearer ${localStorage.getItem("identity_token")}`
50
+ })
51
+ }
52
+ }}
53
+ >
54
+ <ChatEmiLauncher theme="violet" />
55
+ </ChatEmiProvider>
56
+ );
57
+ }
58
+ ```
59
+
60
+ ## Next.js app usage
61
+
62
+ Import package CSS once from `app/layout.tsx`:
63
+
64
+ ```tsx
65
+ import "chatemi/styles.css";
66
+ import type { Metadata } from "next";
67
+
68
+ export const metadata: Metadata = {
69
+ title: "My app"
70
+ };
71
+
72
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
73
+ return (
74
+ <html lang="en">
75
+ <body>{children}</body>
76
+ </html>
77
+ );
78
+ }
79
+ ```
80
+
81
+ Create a client component for the widget:
82
+
83
+ ```tsx
84
+ "use client";
85
+
86
+ import { ChatEmiLauncher, ChatEmiProvider } from "chatemi";
87
+
88
+ export function ChatWidget() {
89
+ return (
90
+ <ChatEmiProvider
91
+ config={{
92
+ apiBaseUrl: process.env.NEXT_PUBLIC_CHAT_API_URL!,
93
+ socketUrl: process.env.NEXT_PUBLIC_CHAT_SOCKET_URL,
94
+ token: () => localStorage.getItem("access_token") ?? undefined,
95
+ theme: "glass",
96
+ notifications: {
97
+ enabled: true,
98
+ browser: true,
99
+ showWhenOpen: false
100
+ }
101
+ }}
102
+ >
103
+ <ChatEmiLauncher
104
+ defaultOpen={false}
105
+ placement="bottom-right"
106
+ theme="glass"
107
+ title="Support"
108
+ subtitle="Usually replies fast"
109
+ />
110
+ </ChatEmiProvider>
111
+ );
112
+ }
113
+ ```
114
+
115
+ Then render `<ChatWidget />` from any client component or include it in a page layout. The provider keeps the socket connected while the launcher modal is closed, so incoming `notification` and `message.created` events continue updating the badge in the background.
116
+
117
+ ## Hook usage
118
+
119
+ ```tsx
120
+ import { useChatEmi } from "chatemi";
121
+
122
+ export function SendWelcomeButton({ conversationId }: { conversationId: string }) {
123
+ const { actions, activeMessages, connectionStatus } = useChatEmi();
124
+
125
+ return (
126
+ <button
127
+ onClick={() =>
128
+ actions.sendMessage({
129
+ conversationId,
130
+ text: "Welcome to the chat",
131
+ replyToId: activeMessages.at(-1)?.id
132
+ })
133
+ }
134
+ >
135
+ Send ({activeMessages.length} loaded, socket {connectionStatus})
136
+ </button>
137
+ );
138
+ }
139
+ ```
140
+
141
+ ## Expected REST API contract
142
+
143
+ By default ChatEmi calls these paths under `apiBaseUrl`:
144
+
145
+ | Feature | Method/path |
146
+ | --- | --- |
147
+ | Current user | `GET /me` |
148
+ | Users | `GET /users?q=...`, `GET /users/:userId` |
149
+ | Conversations | `GET /conversations`, `POST /conversations` |
150
+ | Conversation detail | `GET /conversations/:conversationId`, `PATCH /conversations/:conversationId`, `DELETE /conversations/:conversationId` |
151
+ | Avatar | `PATCH /conversations/:conversationId/avatar`, `PATCH /users/:userId` |
152
+ | Members/admins | `POST /conversations/:conversationId/members`, `PATCH /conversations/:conversationId/members/:userId`, `DELETE /conversations/:conversationId/members/:userId` |
153
+ | Messages | `GET /conversations/:conversationId/messages`, `POST /conversations/:conversationId/messages` |
154
+ | Message detail | `PATCH /conversations/:conversationId/messages/:messageId`, `DELETE /conversations/:conversationId/messages/:messageId` |
155
+ | Read receipts | `POST /conversations/:conversationId/read` |
156
+ | Delivered receipts | `POST /conversations/:conversationId/delivered` |
157
+ | Forward | `POST /conversations/:conversationId/messages/:messageId/forward` |
158
+ | Reactions | `POST /conversations/:conversationId/messages/:messageId/reactions`, `DELETE /conversations/:conversationId/messages/:messageId/reactions` |
159
+ | Attachment upload | `POST /attachments` multipart form data |
160
+ | Search | `GET /search/messages?q=...` |
161
+
162
+ If your backend uses different paths, pass `config.endpoints` to override any route.
163
+
164
+ ## Socket event contract
165
+
166
+ The socket sends and receives JSON envelopes:
167
+
168
+ ```json
169
+ {
170
+ "type": "message.created",
171
+ "payload": {}
172
+ }
173
+ ```
174
+
175
+ Built-in incoming event names include:
176
+
177
+ - `conversation.created`
178
+ - `conversation.updated`
179
+ - `conversation.deleted`
180
+ - `conversation.member.added`
181
+ - `conversation.member.updated`
182
+ - `conversation.member.removed`
183
+ - `message.created`
184
+ - `message.updated`
185
+ - `message.deleted`
186
+ - `message.receipt`
187
+ - `message.reaction`
188
+ - `typing`
189
+ - `presence`
190
+ - `notification`
191
+
192
+ Built-in outgoing helper events include:
193
+
194
+ - `conversation.subscribe`
195
+ - `conversation.unsubscribe`
196
+ - `typing`
197
+ - `message.read`
198
+ - `message.delivered`
199
+ - `message.forward`
200
+ - `conversation.member.update`
201
+ - `conversation.avatar.update`
202
+ - `presence`
203
+
204
+ ## Launcher, themes, and notifications
205
+
206
+ Use `ChatEmiLauncher` when you want a floating in-app messenger:
207
+
208
+ ```tsx
209
+ <ChatEmiLauncher
210
+ placement="bottom-right"
211
+ theme="midnight"
212
+ title="Messages"
213
+ subtitle="Team chat"
214
+ initialSize={{ width: 460, height: 720 }}
215
+ minSize={{ width: 360, height: 520 }}
216
+ maxSize={{ width: 960, height: 860 }}
217
+ />
218
+ ```
219
+
220
+ The launcher includes:
221
+
222
+ - toggle button with unread notification badge
223
+ - draggable modal header on desktop
224
+ - native CSS resize handle on desktop
225
+ - compact notification tray above the chat
226
+ - mobile-friendly full-width modal behavior
227
+
228
+ Notification events should use this envelope:
229
+
230
+ ```json
231
+ {
232
+ "type": "notification",
233
+ "payload": {
234
+ "id": "notif_1",
235
+ "kind": "message",
236
+ "title": "Ava",
237
+ "body": "Sent a new message",
238
+ "conversationId": "chat_1",
239
+ "messageId": "message_1",
240
+ "createdAt": "2026-06-19T15:43:00.000Z"
241
+ }
242
+ }
243
+ ```
244
+
245
+ If the backend only emits `message.created`, ChatEmi creates a local message notification automatically for messages sent by other users.
246
+
247
+ Browser notifications are optional and request permission from a user gesture when the launcher opens:
248
+
249
+ ```tsx
250
+ <ChatEmiProvider
251
+ config={{
252
+ apiBaseUrl: "https://api.example.com/chat",
253
+ socketUrl: "wss://api.example.com/chat/socket",
254
+ notifications: {
255
+ enabled: true,
256
+ browser: true,
257
+ showWhenOpen: false,
258
+ maxStored: 100
259
+ }
260
+ }}
261
+ >
262
+ <ChatEmiLauncher />
263
+ </ChatEmiProvider>
264
+ ```
265
+
266
+ ## Groups, channels, admins, and members
267
+
268
+ Create groups and channels by calling the typed API or hook action:
269
+
270
+ ```tsx
271
+ const { actions } = useChatEmi();
272
+
273
+ await actions.createConversation({
274
+ type: "group",
275
+ title: "Product Team",
276
+ participantIds: ["user_1", "user_2"],
277
+ avatarUrl: "https://cdn.example.com/product.png"
278
+ });
279
+
280
+ await actions.createConversation({
281
+ type: "channel",
282
+ title: "Announcements",
283
+ participantIds: ["owner_1"],
284
+ readOnly: true,
285
+ publicUsername: "company_announcements"
286
+ });
287
+ ```
288
+
289
+ Conversation members can include roles and permissions:
290
+
291
+ ```ts
292
+ {
293
+ user: { id: "user_1", name: "Ava" },
294
+ role: "admin",
295
+ permissions: ["manage_members", "pin_messages", "send_media"],
296
+ joinedAt: "2026-06-19T00:00:00.000Z"
297
+ }
298
+ ```
299
+
300
+ The default UI shows a member-management panel to owners/admins/moderators when `enableAdminControls` is enabled.
301
+
302
+ ## Messages, receipts, replies, forwards, and media
303
+
304
+ Messages support:
305
+
306
+ - text and HTML bodies
307
+ - `replyToId`/`replyTo`
308
+ - `forwardedFrom`, `forwardedFromConversationId`, and `forwardedFromMessageId`
309
+ - `deliveredTo` and `readBy` receipts
310
+ - images, videos, audio, voice messages, generic files, locations, and contacts
311
+
312
+ ```tsx
313
+ await actions.sendMessage({
314
+ conversationId: "chat_1",
315
+ text: "Here is the design",
316
+ replyToId: "message_1",
317
+ attachments: [
318
+ {
319
+ id: "attachment_1",
320
+ type: "image",
321
+ url: "https://cdn.example.com/design.png",
322
+ name: "design.png"
323
+ }
324
+ ]
325
+ });
326
+
327
+ await actions.forwardMessage({
328
+ sourceConversationId: "chat_1",
329
+ targetConversationId: "chat_2",
330
+ messageId: "message_1"
331
+ });
332
+ ```
333
+
334
+ ## Last seen and presence
335
+
336
+ Users can include `presence` and `lastSeenAt`. The default UI renders direct chats as `online`, `last seen 4m ago`, or `last seen recently`.
337
+
338
+ ```ts
339
+ {
340
+ id: "user_1",
341
+ name: "Ava",
342
+ presence: "offline",
343
+ lastSeenAt: "2026-06-19T14:00:00.000Z"
344
+ }
345
+ ```
346
+
347
+ ## External user API
348
+
349
+ Use `config.userDirectory` when users live outside your chat backend. ChatEmi will call that API for user search and user details without leaking the chat API bearer token unless you add it yourself in `userDirectory.headers`.
350
+
351
+ ```tsx
352
+ <ChatEmiProvider
353
+ config={{
354
+ apiBaseUrl: "https://api.example.com/chat",
355
+ userDirectory: {
356
+ baseUrl: "https://identity.example.com",
357
+ searchPath: "/directory/users",
358
+ userPath: (userId) => `/directory/users/${userId}`,
359
+ headers: async () => ({
360
+ Authorization: `Bearer ${await getIdentityToken()}`
361
+ }),
362
+ mapUser: (raw) => {
363
+ const user = raw as { id: string; displayName: string; photo?: string };
364
+ return {
365
+ id: user.id,
366
+ name: user.displayName,
367
+ avatarUrl: user.photo
368
+ };
369
+ }
370
+ }
371
+ }}
372
+ >
373
+ <ChatEmiMessenger />
374
+ </ChatEmiProvider>
375
+ ```
376
+
377
+ ## MongoDB backend integration
378
+
379
+ MongoDB must be connected from your API server, not from browser React code. Install the optional peer dependency in your backend:
380
+
381
+ ```bash
382
+ npm install chatemi mongodb
383
+ ```
384
+
385
+ ```ts
386
+ import { createChatEmiMongoConnection } from "chatemi/server";
387
+
388
+ const chatDb = await createChatEmiMongoConnection({
389
+ uri: process.env.MONGODB_URI!,
390
+ databaseName: "chatemi",
391
+ // Pass MongoClient options that match your deployment. ChatEmi intentionally
392
+ // does not guess pool sizes because serverless and long-running servers need
393
+ // different connection strategies.
394
+ clientOptions: {
395
+ appName: "chatemi-api"
396
+ }
397
+ });
398
+
399
+ await chatDb.ensureIndexes();
400
+
401
+ export const conversations = chatDb.collections.conversations;
402
+ export const messages = chatDb.collections.messages;
403
+ export const members = chatDb.collections.members;
404
+ export const receipts = chatDb.collections.receipts;
405
+ export const attachments = chatDb.collections.attachments;
406
+ ```
407
+
408
+ Connection guidance:
409
+
410
+ - Create one MongoDB client per server process and reuse it.
411
+ - Do not put MongoDB credentials in React/browser code.
412
+ - For serverless functions, initialize the connection outside the handler so warm invocations reuse it.
413
+ - For long-running servers, pass pool/timeouts through `clientOptions` based on observed concurrency and MongoDB connection metrics.
414
+
415
+ ## Light mode and dark mode
416
+
417
+ Use `theme="light"`, `theme="dark"`, `theme="system"`, `theme="midnight"`, `theme="glass"`, `theme="emerald"`, or `theme="violet"`:
418
+
419
+ ```tsx
420
+ <ChatEmiLauncher theme="glass" />
421
+ ```
422
+
423
+ ## Customizing the UI
424
+
425
+ ```tsx
426
+ <ChatEmiMessenger
427
+ composerPlaceholder="Message the team"
428
+ renderConversation={(conversation, isActive) => (
429
+ <span style={{ fontWeight: isActive ? 800 : 500 }}>{conversation.title}</span>
430
+ )}
431
+ renderMessage={(message, isMine) => (
432
+ <div className={isMine ? "mine" : "theirs"}>{message.text}</div>
433
+ )}
434
+ />
435
+ ```
436
+
437
+ ## Development
438
+
439
+ ```bash
440
+ npm install
441
+ npm run typecheck
442
+ npm run build
443
+ ```
444
+
445
+ ## Publishing
446
+
447
+ Update the version in `package.json`, then run:
448
+
449
+ ```bash
450
+ npm publish
451
+ ```
package/dist/api.d.ts ADDED
@@ -0,0 +1,43 @@
1
+ import type { ChatEmiAttachment, ChatEmiConfig, ChatEmiConversation, ChatEmiCreateConversationInput, ChatEmiEditMessageInput, ChatEmiForwardMessageInput, ChatEmiID, ChatEmiListOptions, ChatEmiManageMemberInput, ChatEmiMember, ChatEmiMessage, ChatEmiMessageListOptions, ChatEmiPage, ChatEmiSendMessageInput, ChatEmiUpdateAvatarInput, ChatEmiUpdateMemberInput, ChatEmiUploadAttachmentInput, ChatEmiUser, ChatEmiUserSearchOptions } from "./types";
2
+ export declare class ChatEmiApiError extends Error {
3
+ status: number;
4
+ payload: unknown;
5
+ constructor(message: string, status: number, payload: unknown);
6
+ }
7
+ export declare class ChatEmiApi {
8
+ private readonly config;
9
+ private readonly fetcher;
10
+ constructor(config: ChatEmiConfig);
11
+ getMe(signal?: AbortSignal): Promise<ChatEmiUser>;
12
+ searchUsers(options: ChatEmiUserSearchOptions, signal?: AbortSignal): Promise<ChatEmiPage<ChatEmiUser>>;
13
+ getUser(userId: ChatEmiID, signal?: AbortSignal): Promise<ChatEmiUser>;
14
+ listConversations(options?: ChatEmiListOptions, signal?: AbortSignal): Promise<ChatEmiPage<ChatEmiConversation>>;
15
+ getConversation(conversationId: ChatEmiID, signal?: AbortSignal): Promise<ChatEmiConversation>;
16
+ createConversation(input: ChatEmiCreateConversationInput): Promise<ChatEmiConversation>;
17
+ createGroup(input: Omit<ChatEmiCreateConversationInput, "type">): Promise<ChatEmiConversation>;
18
+ createChannel(input: Omit<ChatEmiCreateConversationInput, "type">): Promise<ChatEmiConversation>;
19
+ updateConversation(conversationId: ChatEmiID, input: Partial<ChatEmiConversation>): Promise<ChatEmiConversation>;
20
+ archiveConversation(conversationId: ChatEmiID): Promise<void>;
21
+ updateConversationAvatar(input: ChatEmiUpdateAvatarInput): Promise<ChatEmiConversation | ChatEmiUser>;
22
+ addMembers(conversationId: ChatEmiID, userIds: ChatEmiID[]): Promise<ChatEmiMember[]>;
23
+ updateMember(input: ChatEmiUpdateMemberInput): Promise<ChatEmiMember>;
24
+ removeMember(input: ChatEmiManageMemberInput): Promise<void>;
25
+ listMessages(conversationId: ChatEmiID, options?: ChatEmiMessageListOptions, signal?: AbortSignal): Promise<ChatEmiPage<ChatEmiMessage>>;
26
+ sendMessage(input: ChatEmiSendMessageInput): Promise<ChatEmiMessage>;
27
+ forwardMessage(input: ChatEmiForwardMessageInput): Promise<ChatEmiMessage>;
28
+ editMessage(input: ChatEmiEditMessageInput): Promise<ChatEmiMessage>;
29
+ deleteMessage(conversationId: ChatEmiID, messageId: ChatEmiID): Promise<void>;
30
+ markConversationRead(conversationId: ChatEmiID, messageIds?: ChatEmiID[]): Promise<void>;
31
+ markConversationDelivered(conversationId: ChatEmiID, messageIds?: ChatEmiID[]): Promise<void>;
32
+ addReaction(conversationId: ChatEmiID, messageId: ChatEmiID, emoji: string): Promise<ChatEmiMessage>;
33
+ removeReaction(conversationId: ChatEmiID, messageId: ChatEmiID, emoji: string): Promise<ChatEmiMessage>;
34
+ uploadAttachment(input: ChatEmiUploadAttachmentInput): Promise<ChatEmiAttachment>;
35
+ searchMessages(query: string, options?: ChatEmiListOptions, signal?: AbortSignal): Promise<ChatEmiPage<ChatEmiMessage>>;
36
+ private endpoint;
37
+ private request;
38
+ private buildUrl;
39
+ private resolveHeaders;
40
+ private resolveUserDirectoryHeaders;
41
+ private parseResponse;
42
+ private errorMessage;
43
+ }
@@ -0,0 +1,2 @@
1
+ .chatemi{--chatemi-bg:#f7f8fc;--chatemi-panel:#ffffffeb;--chatemi-border:#94a3b847;--chatemi-text:#0f172a;--chatemi-muted:#64748b;--chatemi-primary:#2563eb;--chatemi-primary-dark:#1d4ed8;--chatemi-bubble:#eef2ff;--chatemi-bubble-mine:linear-gradient(135deg, #2563eb, #7c3aed);--chatemi-danger:#d33f49;--chatemi-shadow:0 24px 80px #0f172a2e;background:var(--chatemi-bg);border:1px solid var(--chatemi-border);box-shadow:var(--chatemi-shadow);color:var(--chatemi-text);border-radius:28px;grid-template-columns:minmax(260px,340px) minmax(0,1fr);width:100%;height:min(760px,100vh);min-height:520px;font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;display:grid;overflow:hidden}.chatemi-launcher{--chatemi-bg:#f7f8fc;--chatemi-panel:#ffffffeb;--chatemi-border:#94a3b847;--chatemi-text:#0f172a;--chatemi-muted:#64748b;--chatemi-primary:#2563eb;--chatemi-primary-dark:#1d4ed8;--chatemi-bubble:#eef2ff;--chatemi-bubble-mine:linear-gradient(135deg, #2563eb, #7c3aed);--chatemi-danger:#d33f49;--chatemi-shadow:0 24px 80px #0f172a2e}.chatemi[data-theme=dark],.chatemi-launcher[data-theme=dark]{--chatemi-bg:#101722;--chatemi-panel:#172231;--chatemi-border:#2a3a4e;--chatemi-text:#eef5ff;--chatemi-muted:#94a7bd;--chatemi-primary:#2f9bff;--chatemi-primary-dark:#1877d6;--chatemi-bubble:#223044;--chatemi-bubble-mine:linear-gradient(135deg, #1e88e5, #7c4dff);--chatemi-danger:#ff7380;--chatemi-shadow:0 24px 80px #00000070}.chatemi[data-theme=midnight],.chatemi-launcher[data-theme=midnight]{--chatemi-bg:#070b18;--chatemi-panel:#0d1220f5;--chatemi-border:#6366f138;--chatemi-text:#edf2ff;--chatemi-muted:#9aa8c7;--chatemi-primary:#8b5cf6;--chatemi-primary-dark:#6d28d9;--chatemi-bubble:#151f35;--chatemi-bubble-mine:linear-gradient(135deg, #8b5cf6, #06b6d4);--chatemi-danger:#fb7185;--chatemi-shadow:0 30px 90px #050712a8}.chatemi[data-theme=glass],.chatemi-launcher[data-theme=glass]{--chatemi-bg:linear-gradient(135deg, #eff6ffd1, #faf5ffd1);--chatemi-panel:#ffffffb8;--chatemi-border:#ffffff6b;--chatemi-text:#111827;--chatemi-muted:#64748b;--chatemi-primary:#7c3aed;--chatemi-primary-dark:#5b21b6;--chatemi-bubble:#ffffffbd;--chatemi-bubble-mine:linear-gradient(135deg, #7c3aed, #2563eb);--chatemi-danger:#e11d48;--chatemi-shadow:0 30px 90px #4f46e538}.chatemi[data-theme=emerald],.chatemi-launcher[data-theme=emerald]{--chatemi-bg:#f0fdfa;--chatemi-panel:#fffffff0;--chatemi-border:#14b8a638;--chatemi-text:#042f2e;--chatemi-muted:#4b8079;--chatemi-primary:#0f766e;--chatemi-primary-dark:#115e59;--chatemi-bubble:#ccfbf1;--chatemi-bubble-mine:linear-gradient(135deg, #0f766e, #10b981);--chatemi-danger:#dc2626}.chatemi[data-theme=violet],.chatemi-launcher[data-theme=violet]{--chatemi-bg:#faf5ff;--chatemi-panel:#fffffff0;--chatemi-border:#a855f73d;--chatemi-text:#2e1065;--chatemi-muted:#7e6a9e;--chatemi-primary:#9333ea;--chatemi-primary-dark:#7e22ce;--chatemi-bubble:#f3e8ff;--chatemi-bubble-mine:linear-gradient(135deg, #9333ea, #ec4899);--chatemi-danger:#be123c}@media (prefers-color-scheme:dark){.chatemi[data-theme=system],.chatemi-launcher[data-theme=system]{--chatemi-bg:#101722;--chatemi-panel:#172231;--chatemi-border:#2a3a4e;--chatemi-text:#eef5ff;--chatemi-muted:#94a7bd;--chatemi-primary:#2f9bff;--chatemi-primary-dark:#1877d6;--chatemi-bubble:#223044;--chatemi-bubble-mine:linear-gradient(135deg, #1e88e5, #7c4dff);--chatemi-danger:#ff7380;--chatemi-shadow:0 24px 80px #00000070}}.chatemi *{box-sizing:border-box}.chatemi button,.chatemi input,.chatemi textarea{font:inherit}.chatemi-launcher{color:var(--chatemi-text);z-index:2147483000;font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;position:fixed}.chatemi-launcher--bottom-right{bottom:24px;right:24px}.chatemi-launcher--bottom-left{bottom:24px;left:24px}.chatemi-launcher--top-right{top:24px;right:24px}.chatemi-launcher--top-left{top:24px;left:24px}.chatemi-launcher__toggle{background:var(--chatemi-bubble-mine);box-shadow:var(--chatemi-shadow);color:#fff;cursor:pointer;border:0;border-radius:24px;justify-content:center;align-items:center;width:64px;height:64px;transition:transform .2s,box-shadow .2s;display:inline-flex;position:relative}.chatemi-launcher__toggle:hover{transform:translateY(-2px)scale(1.02);box-shadow:0 22px 70px #2563eb5c}.chatemi-launcher__badge{color:#fff;background:#ef4444;border:3px solid #fff;border-radius:999px;justify-content:center;align-items:center;min-width:26px;min-height:26px;padding:0 6px;font-size:.72rem;font-weight:900;display:inline-flex;position:absolute;top:-7px;right:-7px}.chatemi-launcher__modal{background:var(--chatemi-panel);border:1px solid var(--chatemi-border);box-shadow:var(--chatemi-shadow);resize:both;touch-action:none;border-radius:28px;grid-template-rows:auto auto minmax(0,1fr);display:grid;position:absolute;overflow:hidden}.chatemi-launcher--bottom-right .chatemi-launcher__modal{bottom:82px;right:0}.chatemi-launcher--bottom-left .chatemi-launcher__modal{bottom:82px;left:0}.chatemi-launcher--top-right .chatemi-launcher__modal{top:82px;right:0}.chatemi-launcher--top-left .chatemi-launcher__modal{top:82px;left:0}.chatemi-launcher__modal-header{background:linear-gradient(135deg, #ffffff2e, transparent), var(--chatemi-bubble-mine);color:#fff;cursor:grab;-webkit-user-select:none;user-select:none;justify-content:space-between;align-items:center;gap:14px;padding:14px 16px;display:flex}.chatemi-launcher__modal-header:active{cursor:grabbing}.chatemi-launcher__modal-header div{gap:2px;display:grid}.chatemi-launcher__modal-header span{color:#ffffffc2;font-size:.78rem}.chatemi-launcher__modal-header button,.chatemi-launcher__clear{color:inherit;cursor:pointer;background:#ffffff2e;border:1px solid #ffffff52;border-radius:999px;padding:8px 12px;font-weight:800}.chatemi-launcher__notifications{background:color-mix(in srgb, var(--chatemi-panel) 86%, var(--chatemi-primary) 14%);border-bottom:1px solid var(--chatemi-border);grid-template-columns:minmax(0,1fr) auto;gap:8px;padding:10px;display:grid}.chatemi-launcher__notification{border:1px solid var(--chatemi-border);color:var(--chatemi-text);cursor:pointer;text-align:left;background:#ffffffb8;border-radius:18px;grid-column:1;grid-template-columns:auto minmax(0,1fr);align-items:center;gap:10px;padding:8px;display:grid}.chatemi-launcher__notification--unread{box-shadow:inset 3px 0 0 var(--chatemi-primary)}.chatemi-launcher__notification img,.chatemi-launcher__notification>span:first-child{background:var(--chatemi-bubble-mine);color:#fff;object-fit:cover;border-radius:14px;justify-content:center;align-items:center;width:36px;height:36px;font-size:.72rem;font-weight:900;display:inline-flex}.chatemi-launcher__notification span:last-child{min-width:0;display:grid}.chatemi-launcher__notification small{color:var(--chatemi-muted);text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.chatemi-launcher__clear{border-color:var(--chatemi-border);color:var(--chatemi-muted);background:0 0;grid-area:1/2;align-self:start}.chatemi-launcher__modal .chatemi{box-shadow:none;border:0;border-radius:0;height:100%;min-height:0}.chatemi__sidebar{border-right:1px solid var(--chatemi-border);background:#ffffffd1;flex-direction:column;min-width:0;display:flex}.chatemi__brand{justify-content:space-between;align-items:center;gap:12px;padding:20px;display:flex}.chatemi__brand div{gap:4px;display:grid}.chatemi__brand span,.chatemi__conversation small,.chatemi__header span,.chatemi__message footer{color:var(--chatemi-muted);font-size:.82rem}.chatemi__status{background:var(--chatemi-muted);border-radius:999px;width:10px;height:10px}.chatemi__status--connected{background:#24b47e}.chatemi__status--connecting,.chatemi__status--reconnecting{background:#f2a900}.chatemi__status--error{background:var(--chatemi-danger)}.chatemi__search{padding:0 16px 14px}.chatemi__search input{background:#eef4fb;border:1px solid #0000;border-radius:999px;outline:none;width:100%;padding:12px 16px;transition:background .2s,border-color .2s}.chatemi__search input:focus{border-color:var(--chatemi-primary);background:#fff}.chatemi__conversation-list{gap:4px;padding:0 10px 12px;display:grid;overflow:auto}.chatemi__conversation{color:inherit;cursor:pointer;text-align:left;background:0 0;border:0;border-radius:18px;grid-template-columns:auto minmax(0,1fr) auto;align-items:center;gap:12px;padding:12px;display:grid}.chatemi__conversation:hover,.chatemi__conversation--active{background:#eaf4ff}.chatemi__conversation-body{gap:4px;min-width:0;display:grid}.chatemi__conversation-body strong,.chatemi__conversation-body small{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.chatemi__badge{background:var(--chatemi-primary);color:#fff;text-align:center;border-radius:999px;min-width:24px;padding:4px 8px;font-size:.75rem;font-weight:700}.chatemi__main{background:radial-gradient(circle at top right, #1687ff17, transparent 32%), var(--chatemi-panel);grid-template-rows:auto minmax(0,1fr) auto auto;min-width:0;display:grid;position:relative}.chatemi__header{border-bottom:1px solid var(--chatemi-border);grid-template-columns:auto minmax(0,1fr) auto;align-items:center;gap:14px;padding:16px 20px;display:grid}.chatemi__header div{gap:4px;display:grid}.chatemi__header-action,.chatemi__members button,.chatemi__message-actions button,.chatemi__replying button{border:1px solid var(--chatemi-border);color:var(--chatemi-text);cursor:pointer;background:#eef4fb;border-radius:999px;padding:8px 12px;font-size:.82rem;font-weight:700}.chatemi__avatar{background:linear-gradient(135deg, var(--chatemi-primary), #7c4dff);color:#fff;object-fit:cover;border-radius:18px;flex:0 0 44px;justify-content:center;align-items:center;width:44px;height:44px;font-weight:800;display:inline-flex}.chatemi__avatar--fallback{letter-spacing:-.04em}.chatemi__messages{flex-direction:column;gap:10px;padding:24px;display:flex;overflow:auto}.chatemi__members{border-bottom:1px solid var(--chatemi-border);background:#ffffffc2;gap:12px;padding:14px 18px;display:grid}.chatemi[data-theme=dark] .chatemi__members{background:#1017229e}.chatemi__members-list{gap:8px;max-height:190px;display:grid;overflow:auto}.chatemi__member,.chatemi__user-results button{grid-template-columns:auto minmax(0,1fr) auto;align-items:center;gap:10px;display:grid}.chatemi__member-avatar,.chatemi__user-results img{background:linear-gradient(135deg, var(--chatemi-primary), #7c4dff);color:#fff;object-fit:cover;border-radius:999px;justify-content:center;align-items:center;width:32px;height:32px;font-size:.75rem;font-weight:800;display:inline-flex}.chatemi__member span{display:grid}.chatemi__member small,.chatemi__member-search span{color:var(--chatemi-muted);font-size:.78rem}.chatemi__member-actions{flex-wrap:wrap;justify-content:flex-end;gap:6px;display:flex}.chatemi__member-search{gap:6px;display:grid}.chatemi__member-search input{border:1px solid var(--chatemi-border);color:var(--chatemi-text);background:#eef4fb;border-radius:14px;padding:10px 12px}.chatemi__user-results{flex-wrap:wrap;gap:8px;display:flex}.chatemi__message-shell{display:contents}.chatemi__message{background:var(--chatemi-bubble);border-radius:18px 18px 18px 6px;align-self:flex-start;max-width:min(72%,680px);padding:12px 14px 8px}.chatemi__message--mine{background:var(--chatemi-bubble-mine);color:#fff;border-radius:18px 18px 6px;align-self:flex-end}.chatemi__message p{white-space:pre-wrap;margin:4px 0 0}.chatemi__forwarded,.chatemi__reply-preview{border-left:3px solid var(--chatemi-primary);color:var(--chatemi-muted);margin:0 0 8px;padding-left:8px;font-size:.82rem}.chatemi__reply-preview{gap:2px;display:grid}.chatemi__message-sender{margin-bottom:4px;font-size:.82rem;display:block}.chatemi__message footer{justify-content:flex-end;align-items:center;gap:8px;margin-top:8px;display:flex}.chatemi__message--mine footer{color:#ffffffc7}.chatemi__message-attachments,.chatemi__reactions,.chatemi__attachments{flex-wrap:wrap;gap:8px;margin-top:10px;display:flex}.chatemi__media,.chatemi__attachment-pill{border:1px solid var(--chatemi-border);color:inherit;background:#ffffffad;border-radius:12px;align-items:center;gap:8px;padding:8px 10px;text-decoration:none;display:inline-flex}.chatemi__media--image,.chatemi__media--video,.chatemi__media--audio{align-items:stretch;max-width:320px;padding:8px;display:grid}.chatemi__media--image img,.chatemi__media--video video{object-fit:cover;border-radius:12px;width:100%;max-height:260px}.chatemi__media--audio audio{width:100%;max-width:260px}.chatemi__media--file img{object-fit:cover;border-radius:8px;width:44px;height:44px}.chatemi__media small{color:var(--chatemi-muted)}.chatemi__reactions span{color:var(--chatemi-text);background:#ffffffb3;border-radius:999px;padding:4px 8px;font-size:.82rem}.chatemi__attachments{border-top:1px solid var(--chatemi-border);margin:0;padding:10px 16px 0}.chatemi__replying{border-top:1px solid var(--chatemi-border);justify-content:space-between;align-items:center;gap:12px;padding:10px 16px 0;display:flex}.chatemi__replying span{border-left:3px solid var(--chatemi-primary);color:var(--chatemi-muted);padding-left:10px}.chatemi__attachment-pill{cursor:pointer}.chatemi__composer{grid-template-columns:auto minmax(0,1fr) auto;align-items:end;gap:10px;padding:16px;display:grid}.chatemi__composer textarea{resize:vertical;background:#eef4fb;border:1px solid #0000;border-radius:18px;outline:none;min-height:46px;max-height:160px;padding:12px 14px}.chatemi__composer textarea:focus{border-color:var(--chatemi-primary);background:#fff}.chatemi__composer button,.chatemi__upload{cursor:pointer;border-radius:14px;justify-content:center;align-items:center;min-height:46px;padding:0 16px;font-weight:700;display:inline-flex}.chatemi__composer button{background:var(--chatemi-primary);color:#fff;border:0}.chatemi__composer button:hover{background:var(--chatemi-primary-dark)}.chatemi__composer button:disabled{cursor:not-allowed;opacity:.6}.chatemi__upload:has(input:disabled){cursor:not-allowed;opacity:.6}.chatemi__message-actions{opacity:0;justify-content:flex-end;gap:6px;margin-top:8px;transition:opacity .2s;display:flex}.chatemi__message:hover .chatemi__message-actions,.chatemi__message:focus-within .chatemi__message-actions{opacity:1}.chatemi__upload{border:1px solid var(--chatemi-border);color:var(--chatemi-text);background:#eef4fb;position:relative;overflow:hidden}.chatemi__upload input{opacity:0;position:absolute;inset:0}.chatemi__empty{color:var(--chatemi-muted);text-align:center;margin:auto;padding:24px}.chatemi__empty--screen{place-self:center}.chatemi__error{color:var(--chatemi-danger);background:#fff2f2;border-top:1px solid #ffd5d5;padding:10px 16px;font-size:.9rem;position:absolute;bottom:0;left:0;right:0}.chatemi__sr-only{clip:rect(0, 0, 0, 0);white-space:nowrap;border:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}@media (width<=760px){.chatemi-launcher{inset:auto 16px 16px}.chatemi-launcher__toggle{margin-left:auto}.chatemi-launcher__modal,.chatemi-launcher--bottom-right .chatemi-launcher__modal,.chatemi-launcher--bottom-left .chatemi-launcher__modal,.chatemi-launcher--top-right .chatemi-launcher__modal,.chatemi-launcher--top-left .chatemi-launcher__modal{resize:none;inset:auto 0 84px;width:100%!important;min-width:0!important;max-width:none!important;height:min(78vh,720px)!important;max-height:78vh!important;transform:none!important}.chatemi{border-radius:0;grid-template-columns:1fr;height:100vh}.chatemi__sidebar{display:none}.chatemi__composer{grid-template-columns:1fr auto}.chatemi__upload{grid-column:1/-1}.chatemi__message{max-width:88%}}
2
+ /*$vite$:1*/