@mieweb/ui 0.9.0-dev.4 → 0.9.0-dev.6

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.
Files changed (50) hide show
  1. package/dist/{chunk-DG6LIEUJ.js → chunk-22CYDCXA.js} +13 -1183
  2. package/dist/chunk-22CYDCXA.js.map +1 -0
  3. package/dist/chunk-3SD7PDCV.js +1878 -0
  4. package/dist/chunk-3SD7PDCV.js.map +1 -0
  5. package/dist/{chunk-USZ3VEPE.js → chunk-NORSXAXL.js} +853 -779
  6. package/dist/chunk-NORSXAXL.js.map +1 -0
  7. package/dist/{chunk-Z3RYJWSV.cjs → chunk-PY7XAG4D.cjs} +3 -4
  8. package/dist/chunk-PY7XAG4D.cjs.map +1 -0
  9. package/dist/chunk-T6PTKWM6.cjs +1912 -0
  10. package/dist/chunk-T6PTKWM6.cjs.map +1 -0
  11. package/dist/{chunk-DH7I5HMK.cjs → chunk-WIAQQNFU.cjs} +12 -1184
  12. package/dist/chunk-WIAQQNFU.cjs.map +1 -0
  13. package/dist/{chunk-XEZCHQHB.js → chunk-X5YM3KXY.js} +3 -4
  14. package/dist/chunk-X5YM3KXY.js.map +1 -0
  15. package/dist/{chunk-3MHG4HYB.cjs → chunk-XCNDZNY7.cjs} +855 -780
  16. package/dist/chunk-XCNDZNY7.cjs.map +1 -0
  17. package/dist/components/SuperChat/index.cjs +8 -8
  18. package/dist/components/SuperChat/index.d.cts +4 -4
  19. package/dist/components/SuperChat/index.d.ts +4 -4
  20. package/dist/components/SuperChat/index.js +2 -2
  21. package/dist/components/SuperChat/plugins/index.cjs +10 -3
  22. package/dist/components/SuperChat/plugins/index.cjs.map +1 -1
  23. package/dist/components/SuperChat/plugins/index.d.cts +3 -3
  24. package/dist/components/SuperChat/plugins/index.d.ts +3 -3
  25. package/dist/components/SuperChat/plugins/index.js +9 -2
  26. package/dist/components/SuperChat/plugins/index.js.map +1 -1
  27. package/dist/index.cjs +171 -114
  28. package/dist/index.cjs.map +1 -1
  29. package/dist/index.d.cts +471 -410
  30. package/dist/index.d.ts +471 -410
  31. package/dist/index.js +95 -41
  32. package/dist/index.js.map +1 -1
  33. package/dist/styles.css +1 -1
  34. package/dist/tailwind-preset.cjs +4 -4
  35. package/dist/tailwind-preset.js +1 -1
  36. package/dist/{types-DZDroUW8.d.cts → types-B97JNRNQ.d.cts} +1 -1
  37. package/dist/{types-B2RLmdVd.d.cts → types-CnPEwzlp.d.cts} +6 -2
  38. package/dist/{types-B2RLmdVd.d.ts → types-CnPEwzlp.d.ts} +6 -2
  39. package/dist/{types-9vK_YdEb.d.ts → types-gZXrfLj6.d.ts} +1 -1
  40. package/package.json +1 -1
  41. package/dist/chunk-3MHG4HYB.cjs.map +0 -1
  42. package/dist/chunk-5VXRKOPR.cjs +0 -735
  43. package/dist/chunk-5VXRKOPR.cjs.map +0 -1
  44. package/dist/chunk-DG6LIEUJ.js.map +0 -1
  45. package/dist/chunk-DH7I5HMK.cjs.map +0 -1
  46. package/dist/chunk-IF3X2WZP.js +0 -705
  47. package/dist/chunk-IF3X2WZP.js.map +0 -1
  48. package/dist/chunk-USZ3VEPE.js.map +0 -1
  49. package/dist/chunk-XEZCHQHB.js.map +0 -1
  50. package/dist/chunk-Z3RYJWSV.cjs.map +0 -1
@@ -1,514 +1,288 @@
1
- import { useMentionAutocomplete, DragDropZone, AttachmentPreviewItem, AttachmentPicker, CameraButton, MentionMenu, getFileType, generateAttachmentId } from './chunk-IF3X2WZP.js';
1
+ import { useMentionAutocomplete, DragDropZone, DEFAULT_MAX_FILE_SIZE, DEFAULT_ACCEPTED_FILE_TYPES, AttachmentPreviewItem, AttachmentPicker, CameraButton, MentionMenu, ChatComposer, getFileType, generateAttachmentId } from './chunk-3SD7PDCV.js';
2
2
  import { cn } from './chunk-TQONQSJO.js';
3
- import * as React6 from 'react';
3
+ import * as React from 'react';
4
4
  import { cva } from 'class-variance-authority';
5
5
  import { jsx, jsxs } from 'react/jsx-runtime';
6
6
 
7
- function isFileAccepted(file, accepted) {
8
- if (!accepted || accepted.length === 0) return true;
9
- const mime = file.type;
10
- const name = file.name.toLowerCase();
11
- return accepted.some((token) => {
12
- if (token.startsWith(".")) return name.endsWith(token.toLowerCase());
13
- if (token.endsWith("/*")) return mime.startsWith(token.slice(0, -1));
14
- if (token.includes("/")) return mime === token;
15
- return false;
16
- });
17
- }
18
- function CharacterCounter({
19
- current,
20
- max,
21
- showWarningAt = 0.9,
22
- className
23
- }) {
24
- const percentage = current / max;
25
- const isWarning = percentage >= showWarningAt && percentage < 1;
26
- const isOver = current > max;
27
- return /* @__PURE__ */ jsxs(
28
- "span",
29
- {
30
- className: cn(
31
- "text-xs tabular-nums",
32
- isOver ? "font-medium text-red-700 dark:text-red-400" : isWarning ? "text-amber-700 dark:text-amber-400" : "text-neutral-600 dark:text-neutral-400",
33
- className
34
- ),
35
- "aria-live": "polite",
36
- "aria-label": `${current} of ${max} characters`,
37
- children: [
38
- current,
39
- "/",
40
- max
41
- ]
42
- }
43
- );
44
- }
45
- CharacterCounter.displayName = "CharacterCounter";
46
- var sendButtonVariants = cva(
47
- [
48
- "inline-flex items-center justify-center",
49
- "rounded-full p-3 self-start",
50
- "transition-all duration-200",
51
- "focus:outline-none focus:ring-2 focus:ring-primary-500",
52
- "disabled:opacity-50 disabled:cursor-not-allowed"
53
- ],
54
- {
55
- variants: {
56
- variant: {
57
- primary: [
58
- "bg-primary-800 text-white",
59
- "hover:bg-primary-900",
60
- "active:scale-95"
61
- ],
62
- subtle: [
63
- "bg-transparent text-primary-800",
64
- "hover:bg-primary-50 dark:hover:bg-primary-900/20"
65
- ]
66
- },
67
- canSend: {
68
- true: "",
69
- false: "opacity-50 cursor-not-allowed"
7
+ function useMessages(options) {
8
+ const {
9
+ initialMessages = [],
10
+ currentUser,
11
+ onSend,
12
+ onRetry,
13
+ onLoadMore
14
+ } = options;
15
+ const [messages, setMessages] = React.useState(initialMessages);
16
+ const [isSending, setIsSending] = React.useState(false);
17
+ const [isLoadingMore, setIsLoadingMore] = React.useState(false);
18
+ React.useEffect(() => {
19
+ setMessages(initialMessages);
20
+ }, [initialMessages]);
21
+ const addMessage = React.useCallback((message) => {
22
+ setMessages((prev) => {
23
+ if (prev.some((m) => m.id === message.id)) {
24
+ return prev;
70
25
  }
26
+ return [...prev, message];
27
+ });
28
+ }, []);
29
+ const updateMessage = React.useCallback(
30
+ (messageId, updates) => {
31
+ setMessages(
32
+ (prev) => prev.map((m) => m.id === messageId ? { ...m, ...updates } : m)
33
+ );
71
34
  },
72
- defaultVariants: {
73
- variant: "primary",
74
- canSend: false
75
- }
76
- }
77
- );
78
- var SendButton = React6.forwardRef(
79
- ({ className, variant, canSend, isLoading, disabled, ...props }, ref) => {
80
- return /* @__PURE__ */ jsx(
81
- "button",
82
- {
83
- ref,
84
- type: "submit",
85
- disabled: disabled || !canSend || isLoading,
86
- "data-slot": "composer-send-button",
87
- className: cn(sendButtonVariants({ variant, canSend }), className),
88
- "aria-label": isLoading ? "Sending message" : "Send message",
89
- ...props,
90
- children: isLoading ? /* @__PURE__ */ jsxs(
91
- "svg",
92
- {
93
- "aria-hidden": "true",
94
- className: "h-5 w-5 animate-spin",
95
- fill: "none",
96
- viewBox: "0 0 24 24",
97
- children: [
98
- /* @__PURE__ */ jsx(
99
- "circle",
100
- {
101
- className: "opacity-25",
102
- cx: "12",
103
- cy: "12",
104
- r: "10",
105
- stroke: "currentColor",
106
- strokeWidth: "4"
107
- }
108
- ),
109
- /* @__PURE__ */ jsx(
110
- "path",
111
- {
112
- className: "opacity-75",
113
- fill: "currentColor",
114
- d: "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
115
- }
116
- )
117
- ]
118
- }
119
- ) : /* @__PURE__ */ jsx(
120
- "svg",
121
- {
122
- "aria-hidden": "true",
123
- className: "h-5 w-5",
124
- fill: "none",
125
- viewBox: "0 0 24 24",
126
- stroke: "currentColor",
127
- children: /* @__PURE__ */ jsx(
128
- "path",
129
- {
130
- strokeLinecap: "round",
131
- strokeLinejoin: "round",
132
- strokeWidth: 2,
133
- d: "M12 19l9 2-9-18-9 18 9-2zm0 0v-8"
134
- }
135
- )
136
- }
137
- )
138
- }
139
- );
140
- }
141
- );
142
- SendButton.displayName = "SendButton";
143
- var MessageComposer = React6.forwardRef(
144
- ({
145
- onSend,
146
- onTypingStart,
147
- onTypingStop,
148
- value: controlledValue,
149
- onValueChange,
150
- placeholder = "Type a message...",
151
- maxLength = 1600,
152
- showCharacterCount = false,
153
- disabled = false,
154
- isSending = false,
155
- showAttachmentPicker = true,
156
- showCameraButton = false,
157
- acceptedFileTypes = ["image/*", "video/*", ".pdf", ".doc", ".docx"],
158
- maxFileSize = 25 * 1024 * 1024,
159
- maxAttachments = 10,
160
- onError,
161
- autoFocus = false,
162
- replyTo = null,
163
- onCancelReply,
164
- variant = "default",
165
- inputTrailing,
166
- mentionOptions,
167
- className
168
- }, ref) => {
169
- const textareaRef = React6.useRef(null);
170
- const [internalContent, setInternalContent] = React6.useState("");
171
- const isControlled = controlledValue !== void 0;
172
- const content = isControlled ? controlledValue : internalContent;
173
- const setContent = React6.useCallback(
174
- (val) => {
175
- if (isControlled) {
176
- const newVal = typeof val === "function" ? val(controlledValue) : val;
177
- onValueChange?.(newVal);
35
+ []
36
+ );
37
+ const removeMessage = React.useCallback((messageId) => {
38
+ setMessages((prev) => prev.filter((m) => m.id !== messageId));
39
+ }, []);
40
+ const updateStatus = React.useCallback(
41
+ (messageId, status) => {
42
+ updateMessage(messageId, { status });
43
+ },
44
+ [updateMessage]
45
+ );
46
+ const markAsRead = React.useCallback(
47
+ (messageId) => {
48
+ updateStatus(messageId, "read");
49
+ },
50
+ [updateStatus]
51
+ );
52
+ const sendMessage = React.useCallback(
53
+ async (newMessage) => {
54
+ const optimisticId = `optimistic-${Date.now()}`;
55
+ const optimisticMessage = {
56
+ id: optimisticId,
57
+ type: "text",
58
+ content: newMessage.content,
59
+ sender: currentUser,
60
+ timestamp: /* @__PURE__ */ new Date(),
61
+ status: "sending",
62
+ attachments: []
63
+ // Would handle attachment uploads here
64
+ };
65
+ addMessage(optimisticMessage);
66
+ setIsSending(true);
67
+ try {
68
+ if (onSend) {
69
+ const sentMessage = await onSend(newMessage);
70
+ setMessages(
71
+ (prev) => prev.map((m) => m.id === optimisticId ? sentMessage : m)
72
+ );
178
73
  } else {
179
- setInternalContent(val);
74
+ updateStatus(optimisticId, "sent");
180
75
  }
181
- },
182
- [isControlled, controlledValue, onValueChange]
183
- );
184
- const [attachments, setAttachments] = React6.useState(
185
- []
186
- );
187
- const [isTyping, setIsTyping] = React6.useState(false);
188
- const typingTimeoutRef = React6.useRef(null);
189
- React6.useImperativeHandle(ref, () => textareaRef.current);
190
- React6.useEffect(() => {
191
- const textarea = textareaRef.current;
192
- if (textarea) {
193
- textarea.style.height = "auto";
194
- textarea.style.height = `${Math.min(textarea.scrollHeight, 150)}px`;
195
- }
196
- }, [content]);
197
- React6.useEffect(() => {
198
- if (content.length > 0 && !isTyping) {
199
- setIsTyping(true);
200
- onTypingStart?.();
201
- }
202
- if (typingTimeoutRef.current) {
203
- clearTimeout(typingTimeoutRef.current);
76
+ } catch {
77
+ updateStatus(optimisticId, "failed");
78
+ } finally {
79
+ setIsSending(false);
204
80
  }
205
- typingTimeoutRef.current = setTimeout(() => {
206
- if (isTyping) {
207
- setIsTyping(false);
208
- onTypingStop?.();
81
+ },
82
+ [currentUser, onSend, addMessage, updateStatus]
83
+ );
84
+ const retryMessage = React.useCallback(
85
+ async (messageId) => {
86
+ updateStatus(messageId, "sending");
87
+ try {
88
+ if (onRetry) {
89
+ await onRetry(messageId);
90
+ updateStatus(messageId, "sent");
209
91
  }
210
- }, 2e3);
211
- return () => {
212
- if (typingTimeoutRef.current) {
213
- clearTimeout(typingTimeoutRef.current);
214
- }
215
- };
216
- }, [content, isTyping, onTypingStart, onTypingStop]);
217
- React6.useEffect(() => {
218
- if (autoFocus) {
219
- textareaRef.current?.focus();
220
- }
221
- }, [autoFocus]);
222
- const replyToId = replyTo?.id;
223
- React6.useEffect(() => {
224
- if (replyToId !== void 0) {
225
- textareaRef.current?.focus();
226
- }
227
- }, [replyToId]);
228
- const mention = useMentionAutocomplete({
229
- options: mentionOptions,
230
- value: content,
231
- setValue: setContent,
232
- textareaRef
233
- });
234
- const canSend = (content.trim().length > 0 || attachments.length > 0) && content.length <= maxLength && !disabled && !isSending;
235
- const handleSubmit = async (event) => {
236
- event.preventDefault();
237
- if (!canSend) return;
238
- const message = {
239
- content: content.trim(),
240
- attachments: attachments.map((a) => a.file),
241
- replyToId: replyTo?.id
242
- };
243
- setContent("");
244
- setAttachments([]);
245
- setIsTyping(false);
246
- onTypingStop?.();
247
- try {
248
- await onSend(message);
249
92
  } catch {
250
- setContent(message.content);
251
- onError?.("Failed to send message");
252
- }
253
- };
254
- const handleKeyDown = (event) => {
255
- if (mention.handleKeyDown(event)) return;
256
- if (event.key === "Enter" && !event.shiftKey) {
257
- event.preventDefault();
258
- if (canSend) {
259
- handleSubmit(event);
260
- }
93
+ updateStatus(messageId, "failed");
261
94
  }
262
- };
263
- const handleFilesSelected = (files) => {
264
- const remainingSlots = maxAttachments - attachments.length;
265
- const filesToAdd = files.slice(0, remainingSlots);
266
- if (files.length > remainingSlots) {
267
- onError?.(`Maximum ${maxAttachments} attachments allowed`);
95
+ },
96
+ [onRetry, updateStatus]
97
+ );
98
+ const loadMore = React.useCallback(async () => {
99
+ if (isLoadingMore || !onLoadMore) return;
100
+ setIsLoadingMore(true);
101
+ try {
102
+ const olderMessages = await onLoadMore();
103
+ setMessages((prev) => [...olderMessages, ...prev]);
104
+ } finally {
105
+ setIsLoadingMore(false);
106
+ }
107
+ }, [isLoadingMore, onLoadMore]);
108
+ return {
109
+ messages,
110
+ addMessage,
111
+ updateMessage,
112
+ removeMessage,
113
+ sendMessage,
114
+ retryMessage,
115
+ loadMore,
116
+ isSending,
117
+ isLoadingMore,
118
+ markAsRead,
119
+ updateStatus
120
+ };
121
+ }
122
+ function useTypingIndicator(options = {}) {
123
+ const {
124
+ typingParticipants: initialParticipants = [],
125
+ debounceTime = 2e3,
126
+ onTypingStart,
127
+ onTypingStop
128
+ } = options;
129
+ const [participants, setParticipants] = React.useState(initialParticipants);
130
+ const [isLocalTyping, setIsLocalTyping] = React.useState(false);
131
+ const typingTimeoutRef = React.useRef(null);
132
+ React.useEffect(() => {
133
+ setParticipants(initialParticipants);
134
+ }, [initialParticipants]);
135
+ const startTyping = React.useCallback(() => {
136
+ if (!isLocalTyping) {
137
+ setIsLocalTyping(true);
138
+ onTypingStart?.();
139
+ }
140
+ if (typingTimeoutRef.current) {
141
+ clearTimeout(typingTimeoutRef.current);
142
+ }
143
+ typingTimeoutRef.current = setTimeout(() => {
144
+ setIsLocalTyping(false);
145
+ onTypingStop?.();
146
+ }, debounceTime);
147
+ }, [isLocalTyping, debounceTime, onTypingStart, onTypingStop]);
148
+ const stopTyping = React.useCallback(() => {
149
+ if (typingTimeoutRef.current) {
150
+ clearTimeout(typingTimeoutRef.current);
151
+ }
152
+ setIsLocalTyping(false);
153
+ onTypingStop?.();
154
+ }, [onTypingStop]);
155
+ React.useEffect(() => {
156
+ return () => {
157
+ if (typingTimeoutRef.current) {
158
+ clearTimeout(typingTimeoutRef.current);
268
159
  }
269
- const newAttachments = filesToAdd.map((file) => {
270
- const type = getFileType(file.type);
271
- let previewUrl;
272
- if (type === "image" || type === "video") {
273
- previewUrl = URL.createObjectURL(file);
274
- }
275
- return {
276
- id: generateAttachmentId(),
277
- file,
278
- previewUrl,
279
- type,
280
- state: "pending"
281
- };
282
- });
283
- setAttachments((prev) => [...prev, ...newAttachments]);
284
- };
285
- const handlePaste = (event) => {
286
- if (!showAttachmentPicker || disabled) return;
287
- const pasted = Array.from(event.clipboardData.items).filter((item) => item.kind === "file").map((item) => item.getAsFile()).filter((f) => f !== null).filter((f) => isFileAccepted(f, acceptedFileTypes));
288
- if (pasted.length === 0) return;
289
- event.preventDefault();
290
- handleFilesSelected(pasted);
291
160
  };
292
- const handleRemoveAttachment = (attachmentId) => {
293
- setAttachments((prev) => {
294
- const attachment = prev.find((a) => a.id === attachmentId);
295
- if (attachment?.previewUrl) {
296
- URL.revokeObjectURL(attachment.previewUrl);
297
- }
298
- return prev.filter((a) => a.id !== attachmentId);
299
- });
161
+ }, []);
162
+ const typingState = React.useMemo(
163
+ () => ({
164
+ participants,
165
+ lastUpdated: /* @__PURE__ */ new Date()
166
+ }),
167
+ [participants]
168
+ );
169
+ return {
170
+ typingState,
171
+ startTyping,
172
+ stopTyping,
173
+ setTypingParticipants: setParticipants
174
+ };
175
+ }
176
+ function useMessageScroll(options) {
177
+ const { messages, currentUserId, threshold = 100 } = options;
178
+ const scrollContainerRef = React.useRef(null);
179
+ const bottomRef = React.useRef(null);
180
+ const [isScrolledUp, setIsScrolledUp] = React.useState(false);
181
+ const prevMessageCountRef = React.useRef(messages.length);
182
+ React.useEffect(() => {
183
+ const container = scrollContainerRef.current;
184
+ if (!container) return;
185
+ const handleScroll = () => {
186
+ const { scrollTop, scrollHeight, clientHeight } = container;
187
+ const isAtBottom = scrollHeight - scrollTop - clientHeight < threshold;
188
+ setIsScrolledUp(!isAtBottom);
300
189
  };
301
- React6.useEffect(() => {
302
- const currentAttachments = attachments;
303
- return () => {
304
- currentAttachments.forEach((a) => {
305
- if (a.previewUrl) {
306
- URL.revokeObjectURL(a.previewUrl);
190
+ container.addEventListener("scroll", handleScroll);
191
+ return () => container.removeEventListener("scroll", handleScroll);
192
+ }, [threshold]);
193
+ const scrollToBottom = React.useCallback((smooth = true) => {
194
+ bottomRef.current?.scrollIntoView({
195
+ behavior: smooth ? "smooth" : "auto"
196
+ });
197
+ }, []);
198
+ React.useEffect(() => {
199
+ const messageCountChanged = messages.length !== prevMessageCountRef.current;
200
+ prevMessageCountRef.current = messages.length;
201
+ if (!messageCountChanged) return;
202
+ const lastMessage = messages[messages.length - 1];
203
+ const isOutgoing = lastMessage?.sender.id === currentUserId;
204
+ if (isOutgoing || !isScrolledUp) {
205
+ scrollToBottom(true);
206
+ }
207
+ }, [messages, currentUserId, isScrolledUp, scrollToBottom]);
208
+ return {
209
+ scrollContainerRef,
210
+ bottomRef,
211
+ isScrolledUp,
212
+ scrollToBottom
213
+ };
214
+ }
215
+ function useReadReceipts(options) {
216
+ const { currentUserId, onMarkRead, threshold = 0.5 } = options;
217
+ const observerRef = React.useRef(null);
218
+ const observedMessagesRef = React.useRef(/* @__PURE__ */ new Set());
219
+ React.useEffect(() => {
220
+ observerRef.current = new IntersectionObserver(
221
+ (entries) => {
222
+ entries.forEach((entry) => {
223
+ if (entry.isIntersecting) {
224
+ const messageId = entry.target.getAttribute("data-message-id");
225
+ if (messageId && !observedMessagesRef.current.has(messageId)) {
226
+ observedMessagesRef.current.add(messageId);
227
+ onMarkRead?.(messageId);
228
+ }
307
229
  }
308
230
  });
309
- };
310
- }, [attachments]);
311
- return /* @__PURE__ */ jsx(
312
- DragDropZone,
313
- {
314
- onFilesDropped: handleFilesSelected,
315
- acceptedTypes: acceptedFileTypes,
316
- maxFileSize,
317
- maxFiles: maxAttachments - attachments.length,
318
- disabled: disabled || attachments.length >= maxAttachments,
319
- onError,
320
- className: cn("w-full", className),
321
- children: /* @__PURE__ */ jsxs(
322
- "form",
323
- {
324
- onSubmit: handleSubmit,
325
- "data-slot": "message-composer",
326
- className: "w-full",
327
- children: [
328
- replyTo && /* @__PURE__ */ jsxs(
329
- "div",
330
- {
331
- "data-slot": "composer-reply-preview",
332
- className: cn(
333
- "flex items-center gap-2 px-4 py-2",
334
- "bg-neutral-50 dark:bg-neutral-800/50",
335
- "border-primary-500 border-s-4"
336
- ),
337
- children: [
338
- /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
339
- /* @__PURE__ */ jsxs("span", { className: "text-primary-800 dark:text-primary-400 text-xs font-medium", children: [
340
- "Replying to ",
341
- replyTo.senderName
342
- ] }),
343
- /* @__PURE__ */ jsx("p", { className: "truncate text-sm text-neutral-600 dark:text-neutral-300", children: replyTo.content })
344
- ] }),
345
- /* @__PURE__ */ jsx(
346
- "button",
347
- {
348
- type: "button",
349
- onClick: onCancelReply,
350
- className: cn(
351
- "shrink-0 rounded p-1",
352
- "text-neutral-500 hover:text-neutral-700",
353
- "dark:text-neutral-400 dark:hover:text-neutral-200",
354
- "focus:ring-primary-500 focus:ring-2 focus:outline-none"
355
- ),
356
- "aria-label": "Cancel reply",
357
- children: /* @__PURE__ */ jsx(
358
- "svg",
359
- {
360
- "aria-hidden": "true",
361
- className: "h-4 w-4",
362
- fill: "none",
363
- viewBox: "0 0 24 24",
364
- stroke: "currentColor",
365
- children: /* @__PURE__ */ jsx(
366
- "path",
367
- {
368
- strokeLinecap: "round",
369
- strokeLinejoin: "round",
370
- strokeWidth: 2,
371
- d: "M6 18L18 6M6 6l12 12"
372
- }
373
- )
374
- }
375
- )
376
- }
377
- )
378
- ]
379
- }
380
- ),
381
- attachments.length > 0 && /* @__PURE__ */ jsx(
382
- "div",
383
- {
384
- "data-slot": "composer-attachments",
385
- className: cn(
386
- "flex flex-wrap gap-2 p-3",
387
- "border-t border-neutral-200 dark:border-neutral-700"
388
- ),
389
- children: attachments.map((attachment) => /* @__PURE__ */ jsx(
390
- AttachmentPreviewItem,
391
- {
392
- attachment,
393
- onRemove: () => handleRemoveAttachment(attachment.id)
394
- },
395
- attachment.id
396
- ))
397
- }
398
- ),
399
- /* @__PURE__ */ jsxs(
400
- "div",
401
- {
402
- "data-slot": "composer-input-area",
403
- className: cn(
404
- "flex items-center gap-2 p-3",
405
- "bg-white dark:bg-neutral-900",
406
- variant === "default" && "border-t border-neutral-200 dark:border-neutral-700"
407
- ),
408
- children: [
409
- showAttachmentPicker && /* @__PURE__ */ jsx(
410
- AttachmentPicker,
411
- {
412
- onFilesSelected: handleFilesSelected,
413
- acceptedTypes: acceptedFileTypes,
414
- maxFileSize,
415
- maxFiles: maxAttachments - attachments.length,
416
- disabled: disabled || attachments.length >= maxAttachments,
417
- onError
418
- }
419
- ),
420
- showCameraButton && /* @__PURE__ */ jsx(
421
- CameraButton,
422
- {
423
- onCapture: (file) => handleFilesSelected([file]),
424
- disabled: disabled || attachments.length >= maxAttachments
425
- }
426
- ),
427
- /* @__PURE__ */ jsxs(
428
- "div",
429
- {
430
- "data-slot": "composer-input-wrapper",
431
- className: "relative flex-1",
432
- ref: mention.anchorRef,
433
- children: [
434
- /* @__PURE__ */ jsx(MentionMenu, { mention }),
435
- /* @__PURE__ */ jsx(
436
- "textarea",
437
- {
438
- ref: textareaRef,
439
- "data-slot": "composer-input",
440
- value: content,
441
- onChange: (e) => {
442
- setContent(e.target.value);
443
- mention.sync(
444
- e.target.value,
445
- e.target.selectionStart ?? e.target.value.length
446
- );
447
- },
448
- onClick: (e) => {
449
- const el = e.currentTarget;
450
- mention.sync(el.value, el.selectionStart ?? el.value.length);
451
- },
452
- onKeyDown: handleKeyDown,
453
- onPaste: handlePaste,
454
- placeholder,
455
- disabled: disabled || isSending,
456
- rows: 1,
457
- className: cn(
458
- "w-full resize-none rounded-2xl py-2.5",
459
- inputTrailing ? "ps-4 pe-10" : "px-4",
460
- "bg-neutral-100 dark:bg-neutral-800",
461
- "text-neutral-900 dark:text-neutral-100",
462
- "placeholder:text-neutral-400 dark:placeholder:text-neutral-500",
463
- "focus:ring-primary-500 focus:ring-2 focus:outline-none",
464
- "disabled:cursor-not-allowed disabled:opacity-50",
465
- "transition-colors",
466
- "max-h-[150px]"
467
- ),
468
- "aria-label": "Message",
469
- "aria-describedby": showCharacterCount ? "char-count" : void 0,
470
- ...mention.inputProps
471
- }
472
- ),
473
- inputTrailing && /* @__PURE__ */ jsx(
474
- "div",
475
- {
476
- "data-slot": "composer-input-trailing",
477
- className: "pointer-events-none absolute end-1 top-0 flex h-[44px] items-center [&>*]:pointer-events-auto",
478
- children: inputTrailing
479
- }
480
- ),
481
- showCharacterCount && /* @__PURE__ */ jsx(
482
- "div",
483
- {
484
- "data-slot": "composer-char-count",
485
- id: "char-count",
486
- className: "absolute end-3 bottom-1.5",
487
- children: /* @__PURE__ */ jsx(CharacterCounter, { current: content.length, max: maxLength })
488
- }
489
- )
490
- ]
491
- }
492
- ),
493
- /* @__PURE__ */ jsx(
494
- SendButton,
495
- {
496
- canSend,
497
- isLoading: isSending,
498
- disabled
499
- }
500
- )
501
- ]
502
- }
503
- )
504
- ]
505
- }
506
- )
507
- }
231
+ },
232
+ { threshold }
508
233
  );
509
- }
510
- );
511
- MessageComposer.displayName = "MessageComposer";
234
+ return () => {
235
+ observerRef.current?.disconnect();
236
+ };
237
+ }, [onMarkRead, threshold]);
238
+ const observeMessage = React.useCallback(
239
+ (element, message) => {
240
+ if (!element || !observerRef.current) return;
241
+ if (message.sender.id !== currentUserId && message.status !== "read" && !observedMessagesRef.current.has(message.id)) {
242
+ element.setAttribute("data-message-id", message.id);
243
+ observerRef.current.observe(element);
244
+ }
245
+ },
246
+ [currentUserId]
247
+ );
248
+ return { observeMessage };
249
+ }
250
+ function useTypingEmulation({
251
+ value,
252
+ onTypingStart,
253
+ onTypingStop
254
+ }) {
255
+ const [isTyping, setIsTyping] = React.useState(false);
256
+ const typingTimeoutRef = React.useRef(
257
+ null
258
+ );
259
+ React.useEffect(() => {
260
+ if (!onTypingStart && !onTypingStop) return;
261
+ if (value.length > 0 && !isTyping) {
262
+ setIsTyping(true);
263
+ onTypingStart?.();
264
+ }
265
+ if (typingTimeoutRef.current) {
266
+ clearTimeout(typingTimeoutRef.current);
267
+ }
268
+ typingTimeoutRef.current = setTimeout(() => {
269
+ if (isTyping) {
270
+ setIsTyping(false);
271
+ onTypingStop?.();
272
+ }
273
+ }, 2e3);
274
+ return () => {
275
+ if (typingTimeoutRef.current) {
276
+ clearTimeout(typingTimeoutRef.current);
277
+ }
278
+ };
279
+ }, [value, isTyping, onTypingStart, onTypingStop]);
280
+ const stopTyping = React.useCallback(() => {
281
+ setIsTyping(false);
282
+ onTypingStop?.();
283
+ }, [onTypingStop]);
284
+ return { stopTyping };
285
+ }
512
286
  var statusIconVariants = cva(
513
287
  "inline-flex items-center gap-0.5 text-current",
514
288
  {
@@ -889,7 +663,7 @@ function defaultFormatTimestamp(timestamp) {
889
663
  minute: "2-digit"
890
664
  });
891
665
  }
892
- var MessageBubble = React6.forwardRef(
666
+ var MessageBubble = React.forwardRef(
893
667
  ({
894
668
  className,
895
669
  message,
@@ -1364,7 +1138,7 @@ function LoadMoreButton({
1364
1138
  );
1365
1139
  }
1366
1140
  LoadMoreButton.displayName = "LoadMoreButton";
1367
- var MessageList = React6.forwardRef(
1141
+ var MessageList = React.forwardRef(
1368
1142
  ({
1369
1143
  messages,
1370
1144
  currentUser,
@@ -1383,19 +1157,19 @@ var MessageList = React6.forwardRef(
1383
1157
  className,
1384
1158
  autoScroll = "onNewMessage"
1385
1159
  }, ref) => {
1386
- const scrollContainerRef = React6.useRef(null);
1387
- const bottomRef = React6.useRef(null);
1388
- const [isUserScrolled, setIsUserScrolled] = React6.useState(false);
1389
- const prevMessageCountRef = React6.useRef(messages.length);
1390
- React6.useImperativeHandle(ref, () => scrollContainerRef.current);
1391
- const handleScroll = React6.useCallback(() => {
1160
+ const scrollContainerRef = React.useRef(null);
1161
+ const bottomRef = React.useRef(null);
1162
+ const [isUserScrolled, setIsUserScrolled] = React.useState(false);
1163
+ const prevMessageCountRef = React.useRef(messages.length);
1164
+ React.useImperativeHandle(ref, () => scrollContainerRef.current);
1165
+ const handleScroll = React.useCallback(() => {
1392
1166
  const container = scrollContainerRef.current;
1393
1167
  if (!container) return;
1394
1168
  const { scrollTop, scrollHeight, clientHeight } = container;
1395
1169
  const isAtBottom = scrollHeight - scrollTop - clientHeight < 100;
1396
1170
  setIsUserScrolled(!isAtBottom);
1397
1171
  }, []);
1398
- React6.useEffect(() => {
1172
+ React.useEffect(() => {
1399
1173
  const container = scrollContainerRef.current;
1400
1174
  const bottom = bottomRef.current;
1401
1175
  if (!container || !bottom) return;
@@ -1411,7 +1185,7 @@ var MessageList = React6.forwardRef(
1411
1185
  }
1412
1186
  }
1413
1187
  }, [messages, currentUser.id, autoScroll, isUserScrolled]);
1414
- React6.useEffect(() => {
1188
+ React.useEffect(() => {
1415
1189
  const bottom = bottomRef.current;
1416
1190
  if (bottom && !isLoading) {
1417
1191
  bottom.scrollIntoView();
@@ -1464,7 +1238,7 @@ var MessageList = React6.forwardRef(
1464
1238
  {
1465
1239
  "data-slot": "message-list-content",
1466
1240
  className: "flex flex-col gap-1 p-4",
1467
- children: messageGroups.map((group) => /* @__PURE__ */ jsxs(React6.Fragment, { children: [
1241
+ children: messageGroups.map((group) => /* @__PURE__ */ jsxs(React.Fragment, { children: [
1468
1242
  groupByDate && group.label && /* @__PURE__ */ jsx(DateSeparator, { label: group.label }),
1469
1243
  group.messages.map((message, index) => {
1470
1244
  const prevMessage = group.messages[index - 1];
@@ -1536,14 +1310,519 @@ var MessageList = React6.forwardRef(
1536
1310
  )
1537
1311
  }
1538
1312
  )
1539
- }
1540
- )
1541
- ]
1313
+ }
1314
+ )
1315
+ ]
1316
+ }
1317
+ );
1318
+ }
1319
+ );
1320
+ MessageList.displayName = "MessageList";
1321
+ function isFileAccepted(file, accepted) {
1322
+ if (!accepted || accepted.length === 0) return true;
1323
+ const mime = file.type;
1324
+ const name = file.name.toLowerCase();
1325
+ return accepted.some((token) => {
1326
+ if (token.startsWith(".")) return name.endsWith(token.toLowerCase());
1327
+ if (token.endsWith("/*")) return mime.startsWith(token.slice(0, -1));
1328
+ if (token.includes("/")) return mime === token;
1329
+ return false;
1330
+ });
1331
+ }
1332
+ function CharacterCounter({
1333
+ current,
1334
+ max,
1335
+ showWarningAt = 0.9,
1336
+ className
1337
+ }) {
1338
+ const percentage = current / max;
1339
+ const isWarning = percentage >= showWarningAt && percentage < 1;
1340
+ const isOver = current > max;
1341
+ return /* @__PURE__ */ jsxs(
1342
+ "span",
1343
+ {
1344
+ className: cn(
1345
+ "text-xs tabular-nums",
1346
+ isOver ? "font-medium text-red-700 dark:text-red-400" : isWarning ? "text-amber-700 dark:text-amber-400" : "text-neutral-600 dark:text-neutral-400",
1347
+ className
1348
+ ),
1349
+ "aria-live": "polite",
1350
+ "aria-label": `${current} of ${max} characters`,
1351
+ children: [
1352
+ current,
1353
+ "/",
1354
+ max
1355
+ ]
1356
+ }
1357
+ );
1358
+ }
1359
+ CharacterCounter.displayName = "CharacterCounter";
1360
+ var sendButtonVariants = cva(
1361
+ [
1362
+ "inline-flex items-center justify-center",
1363
+ "rounded-full p-3 self-start",
1364
+ "transition-all duration-200",
1365
+ "focus:outline-none focus:ring-2 focus:ring-primary-500",
1366
+ "disabled:opacity-50 disabled:cursor-not-allowed"
1367
+ ],
1368
+ {
1369
+ variants: {
1370
+ variant: {
1371
+ primary: [
1372
+ "bg-primary-800 text-white",
1373
+ "hover:bg-primary-900",
1374
+ "active:scale-95"
1375
+ ],
1376
+ subtle: [
1377
+ "bg-transparent text-primary-800",
1378
+ "hover:bg-primary-50 dark:hover:bg-primary-900/20"
1379
+ ]
1380
+ },
1381
+ canSend: {
1382
+ true: "",
1383
+ false: "opacity-50 cursor-not-allowed"
1384
+ }
1385
+ },
1386
+ defaultVariants: {
1387
+ variant: "primary",
1388
+ canSend: false
1389
+ }
1390
+ }
1391
+ );
1392
+ var SendButton = React.forwardRef(
1393
+ ({ className, variant, canSend, isLoading, disabled, ...props }, ref) => {
1394
+ return /* @__PURE__ */ jsx(
1395
+ "button",
1396
+ {
1397
+ ref,
1398
+ type: "submit",
1399
+ disabled: disabled || !canSend || isLoading,
1400
+ "data-slot": "composer-send-button",
1401
+ className: cn(sendButtonVariants({ variant, canSend }), className),
1402
+ "aria-label": isLoading ? "Sending message" : "Send message",
1403
+ ...props,
1404
+ children: isLoading ? /* @__PURE__ */ jsxs(
1405
+ "svg",
1406
+ {
1407
+ "aria-hidden": "true",
1408
+ className: "h-5 w-5 animate-spin",
1409
+ fill: "none",
1410
+ viewBox: "0 0 24 24",
1411
+ children: [
1412
+ /* @__PURE__ */ jsx(
1413
+ "circle",
1414
+ {
1415
+ className: "opacity-25",
1416
+ cx: "12",
1417
+ cy: "12",
1418
+ r: "10",
1419
+ stroke: "currentColor",
1420
+ strokeWidth: "4"
1421
+ }
1422
+ ),
1423
+ /* @__PURE__ */ jsx(
1424
+ "path",
1425
+ {
1426
+ className: "opacity-75",
1427
+ fill: "currentColor",
1428
+ d: "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
1429
+ }
1430
+ )
1431
+ ]
1432
+ }
1433
+ ) : /* @__PURE__ */ jsx(
1434
+ "svg",
1435
+ {
1436
+ "aria-hidden": "true",
1437
+ className: "h-5 w-5",
1438
+ fill: "none",
1439
+ viewBox: "0 0 24 24",
1440
+ stroke: "currentColor",
1441
+ children: /* @__PURE__ */ jsx(
1442
+ "path",
1443
+ {
1444
+ strokeLinecap: "round",
1445
+ strokeLinejoin: "round",
1446
+ strokeWidth: 2,
1447
+ d: "M12 19l9 2-9-18-9 18 9-2zm0 0v-8"
1448
+ }
1449
+ )
1450
+ }
1451
+ )
1452
+ }
1453
+ );
1454
+ }
1455
+ );
1456
+ SendButton.displayName = "SendButton";
1457
+ var MessageComposer = React.forwardRef(
1458
+ ({
1459
+ onSend,
1460
+ onTypingStart,
1461
+ onTypingStop,
1462
+ value: controlledValue,
1463
+ onValueChange,
1464
+ placeholder = "Type a message...",
1465
+ maxLength = 1600,
1466
+ showCharacterCount = false,
1467
+ disabled = false,
1468
+ isSending = false,
1469
+ showAttachmentPicker = true,
1470
+ showCameraButton = false,
1471
+ acceptedFileTypes = DEFAULT_ACCEPTED_FILE_TYPES,
1472
+ maxFileSize = DEFAULT_MAX_FILE_SIZE,
1473
+ maxAttachments = 10,
1474
+ onError,
1475
+ autoFocus = false,
1476
+ replyTo = null,
1477
+ onCancelReply,
1478
+ variant = "default",
1479
+ inputTrailing,
1480
+ mentionOptions,
1481
+ className
1482
+ }, ref) => {
1483
+ const textareaRef = React.useRef(null);
1484
+ const [internalContent, setInternalContent] = React.useState("");
1485
+ const isControlled = controlledValue !== void 0;
1486
+ const content = isControlled ? controlledValue : internalContent;
1487
+ const setContent = React.useCallback(
1488
+ (val) => {
1489
+ if (isControlled) {
1490
+ const newVal = typeof val === "function" ? val(controlledValue) : val;
1491
+ onValueChange?.(newVal);
1492
+ } else {
1493
+ setInternalContent(val);
1494
+ }
1495
+ },
1496
+ [isControlled, controlledValue, onValueChange]
1497
+ );
1498
+ const [attachments, setAttachments] = React.useState(
1499
+ []
1500
+ );
1501
+ const [isTyping, setIsTyping] = React.useState(false);
1502
+ const typingTimeoutRef = React.useRef(null);
1503
+ React.useImperativeHandle(ref, () => textareaRef.current);
1504
+ React.useEffect(() => {
1505
+ const textarea = textareaRef.current;
1506
+ if (textarea) {
1507
+ textarea.style.height = "auto";
1508
+ textarea.style.height = `${Math.min(textarea.scrollHeight, 150)}px`;
1509
+ }
1510
+ }, [content]);
1511
+ React.useEffect(() => {
1512
+ if (content.length > 0 && !isTyping) {
1513
+ setIsTyping(true);
1514
+ onTypingStart?.();
1515
+ }
1516
+ if (typingTimeoutRef.current) {
1517
+ clearTimeout(typingTimeoutRef.current);
1518
+ }
1519
+ typingTimeoutRef.current = setTimeout(() => {
1520
+ if (isTyping) {
1521
+ setIsTyping(false);
1522
+ onTypingStop?.();
1523
+ }
1524
+ }, 2e3);
1525
+ return () => {
1526
+ if (typingTimeoutRef.current) {
1527
+ clearTimeout(typingTimeoutRef.current);
1528
+ }
1529
+ };
1530
+ }, [content, isTyping, onTypingStart, onTypingStop]);
1531
+ React.useEffect(() => {
1532
+ if (autoFocus) {
1533
+ textareaRef.current?.focus();
1534
+ }
1535
+ }, [autoFocus]);
1536
+ const replyToId = replyTo?.id;
1537
+ React.useEffect(() => {
1538
+ if (replyToId !== void 0) {
1539
+ textareaRef.current?.focus();
1540
+ }
1541
+ }, [replyToId]);
1542
+ const mention = useMentionAutocomplete({
1543
+ options: mentionOptions,
1544
+ value: content,
1545
+ setValue: setContent,
1546
+ textareaRef
1547
+ });
1548
+ const canSend = (content.trim().length > 0 || attachments.length > 0) && content.length <= maxLength && !disabled && !isSending;
1549
+ const handleSubmit = async (event) => {
1550
+ event.preventDefault();
1551
+ if (!canSend) return;
1552
+ const message = {
1553
+ content: content.trim(),
1554
+ attachments: attachments.map((a) => a.file),
1555
+ replyToId: replyTo?.id
1556
+ };
1557
+ setContent("");
1558
+ setAttachments([]);
1559
+ setIsTyping(false);
1560
+ onTypingStop?.();
1561
+ try {
1562
+ await onSend(message);
1563
+ } catch {
1564
+ setContent(message.content);
1565
+ onError?.("Failed to send message");
1566
+ }
1567
+ };
1568
+ const handleKeyDown = (event) => {
1569
+ if (mention.handleKeyDown(event)) return;
1570
+ if (event.key === "Enter" && !event.shiftKey) {
1571
+ event.preventDefault();
1572
+ if (canSend) {
1573
+ handleSubmit(event);
1574
+ }
1575
+ }
1576
+ };
1577
+ const handleFilesSelected = (files) => {
1578
+ const remainingSlots = maxAttachments - attachments.length;
1579
+ const filesToAdd = files.slice(0, remainingSlots);
1580
+ if (files.length > remainingSlots) {
1581
+ onError?.(`Maximum ${maxAttachments} attachments allowed`);
1582
+ }
1583
+ const newAttachments = filesToAdd.map((file) => {
1584
+ const type = getFileType(file.type);
1585
+ let previewUrl;
1586
+ if (type === "image" || type === "video") {
1587
+ previewUrl = URL.createObjectURL(file);
1588
+ }
1589
+ return {
1590
+ id: generateAttachmentId(),
1591
+ file,
1592
+ previewUrl,
1593
+ type,
1594
+ state: "pending"
1595
+ };
1596
+ });
1597
+ setAttachments((prev) => [...prev, ...newAttachments]);
1598
+ };
1599
+ const handlePaste = (event) => {
1600
+ if (!showAttachmentPicker || disabled) return;
1601
+ const pasted = Array.from(event.clipboardData.items).filter((item) => item.kind === "file").map((item) => item.getAsFile()).filter((f) => f !== null).filter((f) => isFileAccepted(f, acceptedFileTypes));
1602
+ if (pasted.length === 0) return;
1603
+ event.preventDefault();
1604
+ handleFilesSelected(pasted);
1605
+ };
1606
+ const handleRemoveAttachment = (attachmentId) => {
1607
+ setAttachments((prev) => {
1608
+ const attachment = prev.find((a) => a.id === attachmentId);
1609
+ if (attachment?.previewUrl) {
1610
+ URL.revokeObjectURL(attachment.previewUrl);
1611
+ }
1612
+ return prev.filter((a) => a.id !== attachmentId);
1613
+ });
1614
+ };
1615
+ React.useEffect(() => {
1616
+ const currentAttachments = attachments;
1617
+ return () => {
1618
+ currentAttachments.forEach((a) => {
1619
+ if (a.previewUrl) {
1620
+ URL.revokeObjectURL(a.previewUrl);
1621
+ }
1622
+ });
1623
+ };
1624
+ }, [attachments]);
1625
+ return /* @__PURE__ */ jsx(
1626
+ DragDropZone,
1627
+ {
1628
+ onFilesDropped: handleFilesSelected,
1629
+ acceptedTypes: acceptedFileTypes,
1630
+ maxFileSize,
1631
+ maxFiles: maxAttachments - attachments.length,
1632
+ disabled: disabled || attachments.length >= maxAttachments,
1633
+ onError,
1634
+ className: cn("w-full", className),
1635
+ children: /* @__PURE__ */ jsxs(
1636
+ "form",
1637
+ {
1638
+ onSubmit: handleSubmit,
1639
+ "data-slot": "message-composer",
1640
+ className: "w-full",
1641
+ children: [
1642
+ replyTo && /* @__PURE__ */ jsxs(
1643
+ "div",
1644
+ {
1645
+ "data-slot": "composer-reply-preview",
1646
+ className: cn(
1647
+ "flex items-center gap-2 px-4 py-2",
1648
+ "bg-neutral-50 dark:bg-neutral-800/50",
1649
+ "border-primary-500 border-s-4"
1650
+ ),
1651
+ children: [
1652
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
1653
+ /* @__PURE__ */ jsxs("span", { className: "text-primary-800 dark:text-primary-400 text-xs font-medium", children: [
1654
+ "Replying to ",
1655
+ replyTo.senderName
1656
+ ] }),
1657
+ /* @__PURE__ */ jsx("p", { className: "truncate text-sm text-neutral-600 dark:text-neutral-300", children: replyTo.content })
1658
+ ] }),
1659
+ /* @__PURE__ */ jsx(
1660
+ "button",
1661
+ {
1662
+ type: "button",
1663
+ onClick: onCancelReply,
1664
+ className: cn(
1665
+ "shrink-0 rounded p-1",
1666
+ "text-neutral-500 hover:text-neutral-700",
1667
+ "dark:text-neutral-400 dark:hover:text-neutral-200",
1668
+ "focus:ring-primary-500 focus:ring-2 focus:outline-none"
1669
+ ),
1670
+ "aria-label": "Cancel reply",
1671
+ children: /* @__PURE__ */ jsx(
1672
+ "svg",
1673
+ {
1674
+ "aria-hidden": "true",
1675
+ className: "h-4 w-4",
1676
+ fill: "none",
1677
+ viewBox: "0 0 24 24",
1678
+ stroke: "currentColor",
1679
+ children: /* @__PURE__ */ jsx(
1680
+ "path",
1681
+ {
1682
+ strokeLinecap: "round",
1683
+ strokeLinejoin: "round",
1684
+ strokeWidth: 2,
1685
+ d: "M6 18L18 6M6 6l12 12"
1686
+ }
1687
+ )
1688
+ }
1689
+ )
1690
+ }
1691
+ )
1692
+ ]
1693
+ }
1694
+ ),
1695
+ attachments.length > 0 && /* @__PURE__ */ jsx(
1696
+ "div",
1697
+ {
1698
+ "data-slot": "composer-attachments",
1699
+ className: cn(
1700
+ "flex flex-wrap gap-2 p-3",
1701
+ "border-t border-neutral-200 dark:border-neutral-700"
1702
+ ),
1703
+ children: attachments.map((attachment) => /* @__PURE__ */ jsx(
1704
+ AttachmentPreviewItem,
1705
+ {
1706
+ attachment,
1707
+ onRemove: () => handleRemoveAttachment(attachment.id)
1708
+ },
1709
+ attachment.id
1710
+ ))
1711
+ }
1712
+ ),
1713
+ /* @__PURE__ */ jsxs(
1714
+ "div",
1715
+ {
1716
+ "data-slot": "composer-input-area",
1717
+ className: cn(
1718
+ "flex items-center gap-2 p-3",
1719
+ "bg-white dark:bg-neutral-900",
1720
+ variant === "default" && "border-t border-neutral-200 dark:border-neutral-700"
1721
+ ),
1722
+ children: [
1723
+ showAttachmentPicker && /* @__PURE__ */ jsx(
1724
+ AttachmentPicker,
1725
+ {
1726
+ onFilesSelected: handleFilesSelected,
1727
+ acceptedTypes: acceptedFileTypes,
1728
+ maxFileSize,
1729
+ maxFiles: maxAttachments - attachments.length,
1730
+ disabled: disabled || attachments.length >= maxAttachments,
1731
+ onError
1732
+ }
1733
+ ),
1734
+ showCameraButton && /* @__PURE__ */ jsx(
1735
+ CameraButton,
1736
+ {
1737
+ onCapture: (file) => handleFilesSelected([file]),
1738
+ disabled: disabled || attachments.length >= maxAttachments
1739
+ }
1740
+ ),
1741
+ /* @__PURE__ */ jsxs(
1742
+ "div",
1743
+ {
1744
+ "data-slot": "composer-input-wrapper",
1745
+ className: "relative flex-1",
1746
+ ref: mention.anchorRef,
1747
+ children: [
1748
+ /* @__PURE__ */ jsx(MentionMenu, { mention }),
1749
+ /* @__PURE__ */ jsx(
1750
+ "textarea",
1751
+ {
1752
+ ref: textareaRef,
1753
+ "data-slot": "composer-input",
1754
+ value: content,
1755
+ onChange: (e) => {
1756
+ setContent(e.target.value);
1757
+ mention.sync(
1758
+ e.target.value,
1759
+ e.target.selectionStart ?? e.target.value.length
1760
+ );
1761
+ },
1762
+ onClick: (e) => {
1763
+ const el = e.currentTarget;
1764
+ mention.sync(el.value, el.selectionStart ?? el.value.length);
1765
+ },
1766
+ onKeyDown: handleKeyDown,
1767
+ onPaste: handlePaste,
1768
+ placeholder,
1769
+ disabled: disabled || isSending,
1770
+ rows: 1,
1771
+ className: cn(
1772
+ "w-full resize-none rounded-2xl py-2.5",
1773
+ inputTrailing ? "ps-4 pe-10" : "px-4",
1774
+ "bg-neutral-100 dark:bg-neutral-800",
1775
+ "text-neutral-900 dark:text-neutral-100",
1776
+ "placeholder:text-neutral-400 dark:placeholder:text-neutral-500",
1777
+ "focus:ring-primary-500 focus:ring-2 focus:outline-none",
1778
+ "disabled:cursor-not-allowed disabled:opacity-50",
1779
+ "transition-colors",
1780
+ "max-h-[150px]"
1781
+ ),
1782
+ "aria-label": "Message",
1783
+ "aria-describedby": showCharacterCount ? "char-count" : void 0,
1784
+ ...mention.inputProps
1785
+ }
1786
+ ),
1787
+ inputTrailing && /* @__PURE__ */ jsx(
1788
+ "div",
1789
+ {
1790
+ "data-slot": "composer-input-trailing",
1791
+ className: "pointer-events-none absolute end-1 top-0 flex h-[44px] items-center [&>*]:pointer-events-auto",
1792
+ children: inputTrailing
1793
+ }
1794
+ ),
1795
+ showCharacterCount && /* @__PURE__ */ jsx(
1796
+ "div",
1797
+ {
1798
+ "data-slot": "composer-char-count",
1799
+ id: "char-count",
1800
+ className: "absolute end-3 bottom-1.5",
1801
+ children: /* @__PURE__ */ jsx(CharacterCounter, { current: content.length, max: maxLength })
1802
+ }
1803
+ )
1804
+ ]
1805
+ }
1806
+ ),
1807
+ /* @__PURE__ */ jsx(
1808
+ SendButton,
1809
+ {
1810
+ canSend,
1811
+ isLoading: isSending,
1812
+ disabled
1813
+ }
1814
+ )
1815
+ ]
1816
+ }
1817
+ )
1818
+ ]
1819
+ }
1820
+ )
1542
1821
  }
1543
1822
  );
1544
1823
  }
1545
1824
  );
1546
- MessageList.displayName = "MessageList";
1825
+ MessageComposer.displayName = "MessageComposer";
1547
1826
  var headerVariants = cva(
1548
1827
  [
1549
1828
  "flex items-center gap-3 px-4 py-3",
@@ -1603,7 +1882,7 @@ function formatLastSeen(date) {
1603
1882
  if (diffDays < 7) return `${diffDays}d ago`;
1604
1883
  return date.toLocaleDateString();
1605
1884
  }
1606
- var ConversationHeader = React6.forwardRef(
1885
+ var ConversationHeader = React.forwardRef(
1607
1886
  ({
1608
1887
  className,
1609
1888
  size,
@@ -1725,7 +2004,7 @@ var ConversationHeader = React6.forwardRef(
1725
2004
  }
1726
2005
  );
1727
2006
  ConversationHeader.displayName = "ConversationHeader";
1728
- var ConversationListItem = React6.forwardRef(({ className, conversation, isSelected, onSelect, ...props }, ref) => {
2007
+ var ConversationListItem = React.forwardRef(({ className, conversation, isSelected, onSelect, ...props }, ref) => {
1729
2008
  const participant = conversation.participants.find((p) => !p.isCurrentUser);
1730
2009
  const title = getConversationTitle(conversation, participant);
1731
2010
  const avatarUrl = conversation.avatarUrl || participant?.avatarUrl;
@@ -1933,7 +2212,7 @@ function ConversationListSkeleton({
1933
2212
  }
1934
2213
  ConversationListSkeleton.displayName = "ConversationListSkeleton";
1935
2214
  function LightboxModal({ attachment, onClose }) {
1936
- React6.useEffect(() => {
2215
+ React.useEffect(() => {
1937
2216
  const handleKeyDown = (event) => {
1938
2217
  if (event.key === "Escape") {
1939
2218
  onClose();
@@ -2030,7 +2309,7 @@ function LightboxModal({ attachment, onClose }) {
2030
2309
  );
2031
2310
  }
2032
2311
  LightboxModal.displayName = "LightboxModal";
2033
- var MessageThread = React6.forwardRef(
2312
+ var MessageThread = React.forwardRef(
2034
2313
  ({
2035
2314
  conversation,
2036
2315
  messages,
@@ -2061,8 +2340,20 @@ var MessageThread = React6.forwardRef(
2061
2340
  onError,
2062
2341
  className
2063
2342
  }, ref) => {
2064
- const [lightboxAttachment, setLightboxAttachment] = React6.useState(null);
2065
- const [replyTo, setReplyTo] = React6.useState(null);
2343
+ const [lightboxAttachment, setLightboxAttachment] = React.useState(null);
2344
+ const [replyTo, setReplyTo] = React.useState(null);
2345
+ const composerRef = React.useRef(null);
2346
+ const [draft, setDraft] = React.useState("");
2347
+ const draftEpochRef = React.useRef(0);
2348
+ const handleDraftChange = React.useCallback((value) => {
2349
+ draftEpochRef.current += 1;
2350
+ setDraft(value);
2351
+ }, []);
2352
+ const { stopTyping } = useTypingEmulation({
2353
+ value: draft,
2354
+ onTypingStart: eventHandlers.onTypingStart,
2355
+ onTypingStop: eventHandlers.onTypingStop
2356
+ });
2066
2357
  const participant = conversation?.type === "direct" ? conversation.participants.find((p) => p.id !== currentUser.id) : void 0;
2067
2358
  const handleAttachmentClick = (attachment, message) => {
2068
2359
  if (attachment.type === "image" || attachment.type === "video") {
@@ -2071,12 +2362,21 @@ var MessageThread = React6.forwardRef(
2071
2362
  eventHandlers.onAttachmentClick?.(attachment, message);
2072
2363
  };
2073
2364
  const handleSendMessage = async (newMessage) => {
2365
+ stopTyping();
2074
2366
  const messageWithReply = {
2075
2367
  ...newMessage,
2076
2368
  replyToId: replyTo?.id || newMessage.replyToId
2077
2369
  };
2078
2370
  setReplyTo(null);
2079
- await eventHandlers.onSendMessage?.(messageWithReply);
2371
+ const epoch = draftEpochRef.current;
2372
+ try {
2373
+ await Promise.resolve(eventHandlers.onSendMessage?.(messageWithReply));
2374
+ } catch (error) {
2375
+ if (draftEpochRef.current === epoch) {
2376
+ setDraft(newMessage.content);
2377
+ }
2378
+ throw error;
2379
+ }
2080
2380
  };
2081
2381
  return /* @__PURE__ */ jsxs(
2082
2382
  "div",
@@ -2102,9 +2402,11 @@ var MessageThread = React6.forwardRef(
2102
2402
  /* @__PURE__ */ jsx(
2103
2403
  DragDropZone,
2104
2404
  {
2105
- onFilesDropped: () => {
2106
- onError?.("Drop files on the composer to attach them");
2405
+ onFilesDropped: (files) => {
2406
+ composerRef.current?.addFiles(files);
2107
2407
  },
2408
+ maxFiles: maxAttachments,
2409
+ onError,
2108
2410
  disabled: !showAttachmentPicker,
2109
2411
  className: "flex-1 overflow-hidden",
2110
2412
  children: /* @__PURE__ */ jsx(
@@ -2130,23 +2432,38 @@ var MessageThread = React6.forwardRef(
2130
2432
  }
2131
2433
  ),
2132
2434
  /* @__PURE__ */ jsx(
2133
- MessageComposer,
2435
+ "div",
2134
2436
  {
2135
- onSend: handleSendMessage,
2136
- onTypingStart: eventHandlers.onTypingStart,
2137
- onTypingStop: eventHandlers.onTypingStop,
2138
- placeholder,
2139
- maxLength: maxMessageLength,
2140
- showCharacterCount,
2141
- isSending,
2142
- showAttachmentPicker,
2143
- showCameraButton,
2144
- acceptedFileTypes,
2145
- maxFileSize,
2146
- maxAttachments,
2147
- onError,
2148
- replyTo,
2149
- onCancelReply: () => setReplyTo(null)
2437
+ "data-slot": "message-thread-composer",
2438
+ className: "shrink-0 border-t border-neutral-200 p-3 dark:border-neutral-700",
2439
+ children: /* @__PURE__ */ jsx(
2440
+ ChatComposer,
2441
+ {
2442
+ ref: composerRef,
2443
+ value: draft,
2444
+ onValueChange: handleDraftChange,
2445
+ onSend: handleSendMessage,
2446
+ placeholder,
2447
+ maxLength: maxMessageLength,
2448
+ showCharacterCount,
2449
+ isSending,
2450
+ allowAttachments: showAttachmentPicker,
2451
+ acceptedFileTypes: acceptedFileTypes ?? DEFAULT_ACCEPTED_FILE_TYPES,
2452
+ maxFileSize: maxFileSize ?? DEFAULT_MAX_FILE_SIZE,
2453
+ maxAttachments,
2454
+ onError,
2455
+ replyTo,
2456
+ onCancelReply: () => setReplyTo(null),
2457
+ inputLabel: "Message",
2458
+ micSlot: showCameraButton ? /* @__PURE__ */ jsx(
2459
+ CameraButton,
2460
+ {
2461
+ onCapture: (file) => composerRef.current?.addFiles([file]),
2462
+ disabled: isSending
2463
+ }
2464
+ ) : void 0
2465
+ }
2466
+ )
2150
2467
  }
2151
2468
  ),
2152
2469
  /* @__PURE__ */ jsx(
@@ -2226,250 +2543,7 @@ function MessagingSplitView({
2226
2543
  );
2227
2544
  }
2228
2545
  MessagingSplitView.displayName = "MessagingSplitView";
2229
- function useMessages(options) {
2230
- const {
2231
- initialMessages = [],
2232
- currentUser,
2233
- onSend,
2234
- onRetry,
2235
- onLoadMore
2236
- } = options;
2237
- const [messages, setMessages] = React6.useState(initialMessages);
2238
- const [isSending, setIsSending] = React6.useState(false);
2239
- const [isLoadingMore, setIsLoadingMore] = React6.useState(false);
2240
- React6.useEffect(() => {
2241
- setMessages(initialMessages);
2242
- }, [initialMessages]);
2243
- const addMessage = React6.useCallback((message) => {
2244
- setMessages((prev) => {
2245
- if (prev.some((m) => m.id === message.id)) {
2246
- return prev;
2247
- }
2248
- return [...prev, message];
2249
- });
2250
- }, []);
2251
- const updateMessage = React6.useCallback(
2252
- (messageId, updates) => {
2253
- setMessages(
2254
- (prev) => prev.map((m) => m.id === messageId ? { ...m, ...updates } : m)
2255
- );
2256
- },
2257
- []
2258
- );
2259
- const removeMessage = React6.useCallback((messageId) => {
2260
- setMessages((prev) => prev.filter((m) => m.id !== messageId));
2261
- }, []);
2262
- const updateStatus = React6.useCallback(
2263
- (messageId, status) => {
2264
- updateMessage(messageId, { status });
2265
- },
2266
- [updateMessage]
2267
- );
2268
- const markAsRead = React6.useCallback(
2269
- (messageId) => {
2270
- updateStatus(messageId, "read");
2271
- },
2272
- [updateStatus]
2273
- );
2274
- const sendMessage = React6.useCallback(
2275
- async (newMessage) => {
2276
- const optimisticId = `optimistic-${Date.now()}`;
2277
- const optimisticMessage = {
2278
- id: optimisticId,
2279
- type: "text",
2280
- content: newMessage.content,
2281
- sender: currentUser,
2282
- timestamp: /* @__PURE__ */ new Date(),
2283
- status: "sending",
2284
- attachments: []
2285
- // Would handle attachment uploads here
2286
- };
2287
- addMessage(optimisticMessage);
2288
- setIsSending(true);
2289
- try {
2290
- if (onSend) {
2291
- const sentMessage = await onSend(newMessage);
2292
- setMessages(
2293
- (prev) => prev.map((m) => m.id === optimisticId ? sentMessage : m)
2294
- );
2295
- } else {
2296
- updateStatus(optimisticId, "sent");
2297
- }
2298
- } catch {
2299
- updateStatus(optimisticId, "failed");
2300
- } finally {
2301
- setIsSending(false);
2302
- }
2303
- },
2304
- [currentUser, onSend, addMessage, updateStatus]
2305
- );
2306
- const retryMessage = React6.useCallback(
2307
- async (messageId) => {
2308
- updateStatus(messageId, "sending");
2309
- try {
2310
- if (onRetry) {
2311
- await onRetry(messageId);
2312
- updateStatus(messageId, "sent");
2313
- }
2314
- } catch {
2315
- updateStatus(messageId, "failed");
2316
- }
2317
- },
2318
- [onRetry, updateStatus]
2319
- );
2320
- const loadMore = React6.useCallback(async () => {
2321
- if (isLoadingMore || !onLoadMore) return;
2322
- setIsLoadingMore(true);
2323
- try {
2324
- const olderMessages = await onLoadMore();
2325
- setMessages((prev) => [...olderMessages, ...prev]);
2326
- } finally {
2327
- setIsLoadingMore(false);
2328
- }
2329
- }, [isLoadingMore, onLoadMore]);
2330
- return {
2331
- messages,
2332
- addMessage,
2333
- updateMessage,
2334
- removeMessage,
2335
- sendMessage,
2336
- retryMessage,
2337
- loadMore,
2338
- isSending,
2339
- isLoadingMore,
2340
- markAsRead,
2341
- updateStatus
2342
- };
2343
- }
2344
- function useTypingIndicator(options = {}) {
2345
- const {
2346
- typingParticipants: initialParticipants = [],
2347
- debounceTime = 2e3,
2348
- onTypingStart,
2349
- onTypingStop
2350
- } = options;
2351
- const [participants, setParticipants] = React6.useState(initialParticipants);
2352
- const [isLocalTyping, setIsLocalTyping] = React6.useState(false);
2353
- const typingTimeoutRef = React6.useRef(null);
2354
- React6.useEffect(() => {
2355
- setParticipants(initialParticipants);
2356
- }, [initialParticipants]);
2357
- const startTyping = React6.useCallback(() => {
2358
- if (!isLocalTyping) {
2359
- setIsLocalTyping(true);
2360
- onTypingStart?.();
2361
- }
2362
- if (typingTimeoutRef.current) {
2363
- clearTimeout(typingTimeoutRef.current);
2364
- }
2365
- typingTimeoutRef.current = setTimeout(() => {
2366
- setIsLocalTyping(false);
2367
- onTypingStop?.();
2368
- }, debounceTime);
2369
- }, [isLocalTyping, debounceTime, onTypingStart, onTypingStop]);
2370
- const stopTyping = React6.useCallback(() => {
2371
- if (typingTimeoutRef.current) {
2372
- clearTimeout(typingTimeoutRef.current);
2373
- }
2374
- setIsLocalTyping(false);
2375
- onTypingStop?.();
2376
- }, [onTypingStop]);
2377
- React6.useEffect(() => {
2378
- return () => {
2379
- if (typingTimeoutRef.current) {
2380
- clearTimeout(typingTimeoutRef.current);
2381
- }
2382
- };
2383
- }, []);
2384
- const typingState = React6.useMemo(
2385
- () => ({
2386
- participants,
2387
- lastUpdated: /* @__PURE__ */ new Date()
2388
- }),
2389
- [participants]
2390
- );
2391
- return {
2392
- typingState,
2393
- startTyping,
2394
- stopTyping,
2395
- setTypingParticipants: setParticipants
2396
- };
2397
- }
2398
- function useMessageScroll(options) {
2399
- const { messages, currentUserId, threshold = 100 } = options;
2400
- const scrollContainerRef = React6.useRef(null);
2401
- const bottomRef = React6.useRef(null);
2402
- const [isScrolledUp, setIsScrolledUp] = React6.useState(false);
2403
- const prevMessageCountRef = React6.useRef(messages.length);
2404
- React6.useEffect(() => {
2405
- const container = scrollContainerRef.current;
2406
- if (!container) return;
2407
- const handleScroll = () => {
2408
- const { scrollTop, scrollHeight, clientHeight } = container;
2409
- const isAtBottom = scrollHeight - scrollTop - clientHeight < threshold;
2410
- setIsScrolledUp(!isAtBottom);
2411
- };
2412
- container.addEventListener("scroll", handleScroll);
2413
- return () => container.removeEventListener("scroll", handleScroll);
2414
- }, [threshold]);
2415
- const scrollToBottom = React6.useCallback((smooth = true) => {
2416
- bottomRef.current?.scrollIntoView({
2417
- behavior: smooth ? "smooth" : "auto"
2418
- });
2419
- }, []);
2420
- React6.useEffect(() => {
2421
- const messageCountChanged = messages.length !== prevMessageCountRef.current;
2422
- prevMessageCountRef.current = messages.length;
2423
- if (!messageCountChanged) return;
2424
- const lastMessage = messages[messages.length - 1];
2425
- const isOutgoing = lastMessage?.sender.id === currentUserId;
2426
- if (isOutgoing || !isScrolledUp) {
2427
- scrollToBottom(true);
2428
- }
2429
- }, [messages, currentUserId, isScrolledUp, scrollToBottom]);
2430
- return {
2431
- scrollContainerRef,
2432
- bottomRef,
2433
- isScrolledUp,
2434
- scrollToBottom
2435
- };
2436
- }
2437
- function useReadReceipts(options) {
2438
- const { currentUserId, onMarkRead, threshold = 0.5 } = options;
2439
- const observerRef = React6.useRef(null);
2440
- const observedMessagesRef = React6.useRef(/* @__PURE__ */ new Set());
2441
- React6.useEffect(() => {
2442
- observerRef.current = new IntersectionObserver(
2443
- (entries) => {
2444
- entries.forEach((entry) => {
2445
- if (entry.isIntersecting) {
2446
- const messageId = entry.target.getAttribute("data-message-id");
2447
- if (messageId && !observedMessagesRef.current.has(messageId)) {
2448
- observedMessagesRef.current.add(messageId);
2449
- onMarkRead?.(messageId);
2450
- }
2451
- }
2452
- });
2453
- },
2454
- { threshold }
2455
- );
2456
- return () => {
2457
- observerRef.current?.disconnect();
2458
- };
2459
- }, [onMarkRead, threshold]);
2460
- const observeMessage = React6.useCallback(
2461
- (element, message) => {
2462
- if (!element || !observerRef.current) return;
2463
- if (message.sender.id !== currentUserId && message.status !== "read" && !observedMessagesRef.current.has(message.id)) {
2464
- element.setAttribute("data-message-id", message.id);
2465
- observerRef.current.observe(element);
2466
- }
2467
- },
2468
- [currentUserId]
2469
- );
2470
- return { observeMessage };
2471
- }
2472
2546
 
2473
- export { AttachmentPreview, CharacterCounter, ConversationHeader, ConversationListItem, ConversationListSkeleton, DateSeparator, EmptyState, LightboxModal, LoadMoreButton, MessageBubble, MessageComposer, MessageList, MessageStatusIcon, MessageThread, MessagingSplitView, ReadReceiptIndicator, SendButton, SkeletonMessage, TypingIndicator, bubbleVariants, formatDateLabel, formatFileSize, formatLastSeen, getConversationSubtitle, getConversationTitle, groupMessagesByDate, headerVariants, isSameSenderGroup, sendButtonVariants, useMessageScroll, useMessages, useReadReceipts, useTypingIndicator };
2474
- //# sourceMappingURL=chunk-USZ3VEPE.js.map
2475
- //# sourceMappingURL=chunk-USZ3VEPE.js.map
2547
+ export { AttachmentPreview, CharacterCounter, ConversationHeader, ConversationListItem, ConversationListSkeleton, DateSeparator, EmptyState, LightboxModal, LoadMoreButton, MessageBubble, MessageComposer, MessageList, MessageStatusIcon, MessageThread, MessagingSplitView, ReadReceiptIndicator, SendButton, SkeletonMessage, TypingIndicator, bubbleVariants, formatDateLabel, formatFileSize, formatLastSeen, getConversationSubtitle, getConversationTitle, groupMessagesByDate, headerVariants, isSameSenderGroup, sendButtonVariants, useMessageScroll, useMessages, useReadReceipts, useTypingEmulation, useTypingIndicator };
2548
+ //# sourceMappingURL=chunk-NORSXAXL.js.map
2549
+ //# sourceMappingURL=chunk-NORSXAXL.js.map