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

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 (42) hide show
  1. package/dist/{chunk-5VXRKOPR.cjs → chunk-H3GRO43Z.cjs} +2 -2
  2. package/dist/{chunk-5VXRKOPR.cjs.map → chunk-H3GRO43Z.cjs.map} +1 -1
  3. package/dist/{chunk-IF3X2WZP.js → chunk-MD3ZY4UO.js} +2 -2
  4. package/dist/{chunk-IF3X2WZP.js.map → chunk-MD3ZY4UO.js.map} +1 -1
  5. package/dist/{chunk-USZ3VEPE.js → chunk-PL6TFCYR.js} +857 -857
  6. package/dist/chunk-PL6TFCYR.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-DG6LIEUJ.js → chunk-QWJGEDHV.js} +3 -3
  10. package/dist/{chunk-DG6LIEUJ.js.map → chunk-QWJGEDHV.js.map} +1 -1
  11. package/dist/{chunk-XEZCHQHB.js → chunk-X5YM3KXY.js} +3 -4
  12. package/dist/chunk-X5YM3KXY.js.map +1 -0
  13. package/dist/{chunk-DH7I5HMK.cjs → chunk-XJUIZA4G.cjs} +11 -11
  14. package/dist/{chunk-DH7I5HMK.cjs.map → chunk-XJUIZA4G.cjs.map} +1 -1
  15. package/dist/{chunk-3MHG4HYB.cjs → chunk-ZXRVVNT5.cjs} +902 -902
  16. package/dist/chunk-ZXRVVNT5.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 +3 -3
  22. package/dist/components/SuperChat/plugins/index.d.cts +3 -3
  23. package/dist/components/SuperChat/plugins/index.d.ts +3 -3
  24. package/dist/components/SuperChat/plugins/index.js +2 -2
  25. package/dist/index.cjs +190 -105
  26. package/dist/index.cjs.map +1 -1
  27. package/dist/index.d.cts +466 -439
  28. package/dist/index.d.ts +466 -439
  29. package/dist/index.js +123 -38
  30. package/dist/index.js.map +1 -1
  31. package/dist/styles.css +1 -1
  32. package/dist/tailwind-preset.cjs +4 -4
  33. package/dist/tailwind-preset.js +1 -1
  34. package/dist/{types-DZDroUW8.d.cts → types-B97JNRNQ.d.cts} +1 -1
  35. package/dist/{types-B2RLmdVd.d.cts → types-CnPEwzlp.d.cts} +6 -2
  36. package/dist/{types-B2RLmdVd.d.ts → types-CnPEwzlp.d.ts} +6 -2
  37. package/dist/{types-9vK_YdEb.d.ts → types-gZXrfLj6.d.ts} +1 -1
  38. package/package.json +1 -1
  39. package/dist/chunk-3MHG4HYB.cjs.map +0 -1
  40. package/dist/chunk-USZ3VEPE.js.map +0 -1
  41. package/dist/chunk-XEZCHQHB.js.map +0 -1
  42. package/dist/chunk-Z3RYJWSV.cjs.map +0 -1
@@ -1,97 +1,39 @@
1
- import { useMentionAutocomplete, DragDropZone, AttachmentPreviewItem, AttachmentPicker, CameraButton, MentionMenu, getFileType, generateAttachmentId } from './chunk-IF3X2WZP.js';
1
+ import { useMentionAutocomplete, DragDropZone, AttachmentPreviewItem, AttachmentPicker, CameraButton, MentionMenu, getFileType, generateAttachmentId } from './chunk-MD3ZY4UO.js';
2
2
  import { cn } from './chunk-TQONQSJO.js';
3
3
  import * as React6 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
- ],
7
+ var statusIconVariants = cva(
8
+ "inline-flex items-center gap-0.5 text-current",
54
9
  {
55
10
  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"
11
+ status: {
12
+ sending: "text-neutral-500",
13
+ sent: "text-neutral-500",
14
+ delivered: "text-muted-foreground",
15
+ read: "text-primary-800 dark:text-primary-300",
16
+ failed: "text-red-500"
70
17
  }
71
18
  },
72
19
  defaultVariants: {
73
- variant: "primary",
74
- canSend: false
20
+ status: "sent"
75
21
  }
76
22
  }
77
23
  );
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(
24
+ function MessageStatusIcon({ status, className }) {
25
+ return /* @__PURE__ */ jsxs(
26
+ "span",
27
+ {
28
+ className: cn(statusIconVariants({ status }), className),
29
+ role: "img",
30
+ "aria-label": `Message ${status}`,
31
+ children: [
32
+ status === "sending" && /* @__PURE__ */ jsxs(
91
33
  "svg",
92
34
  {
93
35
  "aria-hidden": "true",
94
- className: "h-5 w-5 animate-spin",
36
+ className: "h-3.5 w-3.5 animate-spin",
95
37
  fill: "none",
96
38
  viewBox: "0 0 24 24",
97
39
  children: [
@@ -116,518 +58,71 @@ var SendButton = React6.forwardRef(
116
58
  )
117
59
  ]
118
60
  }
119
- ) : /* @__PURE__ */ jsx(
61
+ ),
62
+ status === "sent" && /* @__PURE__ */ jsx(
120
63
  "svg",
121
64
  {
122
65
  "aria-hidden": "true",
123
- className: "h-5 w-5",
66
+ className: "h-3.5 w-3.5",
124
67
  fill: "none",
125
68
  viewBox: "0 0 24 24",
126
69
  stroke: "currentColor",
70
+ strokeWidth: "2",
127
71
  children: /* @__PURE__ */ jsx(
128
72
  "path",
129
73
  {
130
74
  strokeLinecap: "round",
131
75
  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);
178
- } else {
179
- setInternalContent(val);
180
- }
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);
204
- }
205
- typingTimeoutRef.current = setTimeout(() => {
206
- if (isTyping) {
207
- setIsTyping(false);
208
- onTypingStop?.();
209
- }
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
- } 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
- }
261
- }
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`);
268
- }
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
- };
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
- });
300
- };
301
- React6.useEffect(() => {
302
- const currentAttachments = attachments;
303
- return () => {
304
- currentAttachments.forEach((a) => {
305
- if (a.previewUrl) {
306
- URL.revokeObjectURL(a.previewUrl);
307
- }
308
- });
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
- }
508
- );
509
- }
510
- );
511
- MessageComposer.displayName = "MessageComposer";
512
- var statusIconVariants = cva(
513
- "inline-flex items-center gap-0.5 text-current",
514
- {
515
- variants: {
516
- status: {
517
- sending: "text-neutral-500",
518
- sent: "text-neutral-500",
519
- delivered: "text-muted-foreground",
520
- read: "text-primary-800 dark:text-primary-300",
521
- failed: "text-red-500"
522
- }
523
- },
524
- defaultVariants: {
525
- status: "sent"
526
- }
527
- }
528
- );
529
- function MessageStatusIcon({ status, className }) {
530
- return /* @__PURE__ */ jsxs(
531
- "span",
532
- {
533
- className: cn(statusIconVariants({ status }), className),
534
- role: "img",
535
- "aria-label": `Message ${status}`,
536
- children: [
537
- status === "sending" && /* @__PURE__ */ jsxs(
538
- "svg",
539
- {
540
- "aria-hidden": "true",
541
- className: "h-3.5 w-3.5 animate-spin",
542
- fill: "none",
543
- viewBox: "0 0 24 24",
544
- children: [
545
- /* @__PURE__ */ jsx(
546
- "circle",
547
- {
548
- className: "opacity-25",
549
- cx: "12",
550
- cy: "12",
551
- r: "10",
552
- stroke: "currentColor",
553
- strokeWidth: "4"
554
- }
555
- ),
556
- /* @__PURE__ */ jsx(
557
- "path",
558
- {
559
- className: "opacity-75",
560
- fill: "currentColor",
561
- d: "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
562
- }
563
- )
564
- ]
565
- }
566
- ),
567
- status === "sent" && /* @__PURE__ */ jsx(
568
- "svg",
569
- {
570
- "aria-hidden": "true",
571
- className: "h-3.5 w-3.5",
572
- fill: "none",
573
- viewBox: "0 0 24 24",
574
- stroke: "currentColor",
575
- strokeWidth: "2",
576
- children: /* @__PURE__ */ jsx(
577
- "path",
578
- {
579
- strokeLinecap: "round",
580
- strokeLinejoin: "round",
581
- d: "M5 13l4 4L19 7"
582
- }
583
- )
584
- }
585
- ),
586
- (status === "delivered" || status === "read") && /* @__PURE__ */ jsxs(
587
- "svg",
588
- {
589
- "aria-hidden": "true",
590
- className: "h-4 w-4",
591
- fill: "none",
592
- viewBox: "0 0 24 24",
593
- stroke: "currentColor",
594
- strokeWidth: "2",
595
- children: [
596
- /* @__PURE__ */ jsx(
597
- "path",
598
- {
599
- strokeLinecap: "round",
600
- strokeLinejoin: "round",
601
- d: "M5 13l4 4L19 7"
602
- }
603
- ),
604
- /* @__PURE__ */ jsx(
605
- "path",
606
- {
607
- strokeLinecap: "round",
608
- strokeLinejoin: "round",
609
- d: "M12 13l4 4L26 7",
610
- transform: "translate(-5, 0)"
611
- }
612
- )
613
- ]
614
- }
615
- ),
616
- status === "failed" && /* @__PURE__ */ jsx(
617
- "svg",
618
- {
619
- "aria-hidden": "true",
620
- className: "h-3.5 w-3.5",
621
- fill: "none",
622
- viewBox: "0 0 24 24",
623
- stroke: "currentColor",
624
- strokeWidth: "2",
625
- children: /* @__PURE__ */ jsx(
626
- "path",
627
- {
628
- strokeLinecap: "round",
629
- strokeLinejoin: "round",
630
- d: "M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
76
+ d: "M5 13l4 4L19 7"
77
+ }
78
+ )
79
+ }
80
+ ),
81
+ (status === "delivered" || status === "read") && /* @__PURE__ */ jsxs(
82
+ "svg",
83
+ {
84
+ "aria-hidden": "true",
85
+ className: "h-4 w-4",
86
+ fill: "none",
87
+ viewBox: "0 0 24 24",
88
+ stroke: "currentColor",
89
+ strokeWidth: "2",
90
+ children: [
91
+ /* @__PURE__ */ jsx(
92
+ "path",
93
+ {
94
+ strokeLinecap: "round",
95
+ strokeLinejoin: "round",
96
+ d: "M5 13l4 4L19 7"
97
+ }
98
+ ),
99
+ /* @__PURE__ */ jsx(
100
+ "path",
101
+ {
102
+ strokeLinecap: "round",
103
+ strokeLinejoin: "round",
104
+ d: "M12 13l4 4L26 7",
105
+ transform: "translate(-5, 0)"
106
+ }
107
+ )
108
+ ]
109
+ }
110
+ ),
111
+ status === "failed" && /* @__PURE__ */ jsx(
112
+ "svg",
113
+ {
114
+ "aria-hidden": "true",
115
+ className: "h-3.5 w-3.5",
116
+ fill: "none",
117
+ viewBox: "0 0 24 24",
118
+ stroke: "currentColor",
119
+ strokeWidth: "2",
120
+ children: /* @__PURE__ */ jsx(
121
+ "path",
122
+ {
123
+ strokeLinecap: "round",
124
+ strokeLinejoin: "round",
125
+ d: "M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
631
126
  }
632
127
  )
633
128
  }
@@ -1224,326 +719,831 @@ function TypingIndicator({ typingState, className }) {
1224
719
  }
1225
720
  );
1226
721
  }
1227
- TypingIndicator.displayName = "TypingIndicator";
1228
- function DateSeparator({ label, className }) {
722
+ TypingIndicator.displayName = "TypingIndicator";
723
+ function DateSeparator({ label, className }) {
724
+ return /* @__PURE__ */ jsx(
725
+ "div",
726
+ {
727
+ "data-slot": "date-separator",
728
+ className: cn("flex items-center justify-center py-4", className),
729
+ role: "separator",
730
+ "aria-label": label,
731
+ children: /* @__PURE__ */ jsx(
732
+ "span",
733
+ {
734
+ className: cn(
735
+ "rounded-full px-3 py-1 text-xs font-medium",
736
+ "bg-neutral-100 text-neutral-600",
737
+ "dark:bg-neutral-800 dark:text-neutral-400"
738
+ ),
739
+ children: label
740
+ }
741
+ )
742
+ }
743
+ );
744
+ }
745
+ DateSeparator.displayName = "DateSeparator";
746
+ function EmptyState({
747
+ title = "No messages yet",
748
+ description = "Start the conversation by sending a message below.",
749
+ icon,
750
+ action,
751
+ className
752
+ }) {
753
+ return /* @__PURE__ */ jsxs(
754
+ "div",
755
+ {
756
+ "data-slot": "message-empty-state",
757
+ className: cn(
758
+ "flex flex-1 flex-col items-center justify-center p-8 text-center",
759
+ className
760
+ ),
761
+ role: "status",
762
+ "aria-label": title,
763
+ children: [
764
+ icon || /* @__PURE__ */ jsx(
765
+ "div",
766
+ {
767
+ "data-slot": "message-empty-state-icon",
768
+ className: "mb-4 rounded-full bg-neutral-100 p-4 dark:bg-neutral-800",
769
+ children: /* @__PURE__ */ jsx(
770
+ "svg",
771
+ {
772
+ "aria-hidden": "true",
773
+ className: "h-12 w-12 text-neutral-500",
774
+ fill: "none",
775
+ viewBox: "0 0 24 24",
776
+ stroke: "currentColor",
777
+ children: /* @__PURE__ */ jsx(
778
+ "path",
779
+ {
780
+ strokeLinecap: "round",
781
+ strokeLinejoin: "round",
782
+ strokeWidth: 1.5,
783
+ d: "M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"
784
+ }
785
+ )
786
+ }
787
+ )
788
+ }
789
+ ),
790
+ /* @__PURE__ */ jsx("h3", { className: "mb-2 text-lg font-semibold text-neutral-900 dark:text-neutral-100", children: title }),
791
+ /* @__PURE__ */ jsx("p", { className: "text-muted-foreground mb-4 max-w-sm text-sm", children: description }),
792
+ action
793
+ ]
794
+ }
795
+ );
796
+ }
797
+ EmptyState.displayName = "EmptyState";
798
+ function LoadMoreButton({
799
+ isLoading,
800
+ onClick,
801
+ className
802
+ }) {
1229
803
  return /* @__PURE__ */ jsx(
1230
804
  "div",
1231
805
  {
1232
- "data-slot": "date-separator",
1233
- className: cn("flex items-center justify-center py-4", className),
1234
- role: "separator",
1235
- "aria-label": label,
806
+ "data-slot": "load-more-button",
807
+ className: cn("flex justify-center py-4", className),
1236
808
  children: /* @__PURE__ */ jsx(
1237
- "span",
809
+ "button",
1238
810
  {
811
+ type: "button",
812
+ onClick,
813
+ disabled: isLoading,
1239
814
  className: cn(
1240
- "rounded-full px-3 py-1 text-xs font-medium",
1241
- "bg-neutral-100 text-neutral-600",
1242
- "dark:bg-neutral-800 dark:text-neutral-400"
815
+ "rounded-full px-4 py-2 text-sm font-medium",
816
+ "bg-neutral-100 text-neutral-600 dark:bg-neutral-800 dark:text-neutral-300",
817
+ "hover:bg-neutral-200 dark:hover:bg-neutral-700",
818
+ "focus:ring-primary-500 focus:ring-2 focus:outline-none",
819
+ "disabled:cursor-not-allowed disabled:opacity-50",
820
+ "transition-colors"
1243
821
  ),
1244
- children: label
822
+ "aria-label": isLoading ? "Loading more messages" : "Load more messages",
823
+ children: isLoading ? /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-2", children: [
824
+ /* @__PURE__ */ jsxs(
825
+ "svg",
826
+ {
827
+ "aria-hidden": "true",
828
+ className: "h-4 w-4 animate-spin",
829
+ fill: "none",
830
+ viewBox: "0 0 24 24",
831
+ children: [
832
+ /* @__PURE__ */ jsx(
833
+ "circle",
834
+ {
835
+ className: "opacity-25",
836
+ cx: "12",
837
+ cy: "12",
838
+ r: "10",
839
+ stroke: "currentColor",
840
+ strokeWidth: "4"
841
+ }
842
+ ),
843
+ /* @__PURE__ */ jsx(
844
+ "path",
845
+ {
846
+ className: "opacity-75",
847
+ fill: "currentColor",
848
+ d: "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
849
+ }
850
+ )
851
+ ]
852
+ }
853
+ ),
854
+ "Loading..."
855
+ ] }) : "Load earlier messages"
1245
856
  }
1246
857
  )
1247
858
  }
1248
859
  );
1249
860
  }
1250
- DateSeparator.displayName = "DateSeparator";
1251
- function EmptyState({
1252
- title = "No messages yet",
1253
- description = "Start the conversation by sending a message below.",
1254
- icon,
1255
- action,
861
+ LoadMoreButton.displayName = "LoadMoreButton";
862
+ var MessageList = React6.forwardRef(
863
+ ({
864
+ messages,
865
+ currentUser,
866
+ isLoading = false,
867
+ hasMore = false,
868
+ isLoadingMore = false,
869
+ typingState,
870
+ showAvatars = true,
871
+ showSenderNames = false,
872
+ groupByDate = true,
873
+ onLoadMore,
874
+ onRetryMessage,
875
+ onAttachmentClick,
876
+ emptyState,
877
+ formatTimestamp,
878
+ className,
879
+ autoScroll = "onNewMessage"
880
+ }, ref) => {
881
+ const scrollContainerRef = React6.useRef(null);
882
+ const bottomRef = React6.useRef(null);
883
+ const [isUserScrolled, setIsUserScrolled] = React6.useState(false);
884
+ const prevMessageCountRef = React6.useRef(messages.length);
885
+ React6.useImperativeHandle(ref, () => scrollContainerRef.current);
886
+ const handleScroll = React6.useCallback(() => {
887
+ const container = scrollContainerRef.current;
888
+ if (!container) return;
889
+ const { scrollTop, scrollHeight, clientHeight } = container;
890
+ const isAtBottom = scrollHeight - scrollTop - clientHeight < 100;
891
+ setIsUserScrolled(!isAtBottom);
892
+ }, []);
893
+ React6.useEffect(() => {
894
+ const container = scrollContainerRef.current;
895
+ const bottom = bottomRef.current;
896
+ if (!container || !bottom) return;
897
+ const messageCountChanged = messages.length !== prevMessageCountRef.current;
898
+ prevMessageCountRef.current = messages.length;
899
+ if (autoScroll === "always") {
900
+ bottom.scrollIntoView({ behavior: "smooth" });
901
+ } else if (autoScroll === "onNewMessage" && messageCountChanged) {
902
+ const lastMessage = messages[messages.length - 1];
903
+ const isOutgoing = lastMessage?.sender.id === currentUser.id;
904
+ if (isOutgoing || !isUserScrolled) {
905
+ bottom.scrollIntoView({ behavior: "smooth" });
906
+ }
907
+ }
908
+ }, [messages, currentUser.id, autoScroll, isUserScrolled]);
909
+ React6.useEffect(() => {
910
+ const bottom = bottomRef.current;
911
+ if (bottom && !isLoading) {
912
+ bottom.scrollIntoView();
913
+ }
914
+ }, [isLoading]);
915
+ const messageGroups = groupByDate ? groupMessagesByDate(messages) : [{ date: "all", label: "", messages }];
916
+ if (isLoading) {
917
+ return /* @__PURE__ */ jsx(
918
+ "div",
919
+ {
920
+ role: "status",
921
+ className: cn(
922
+ "flex flex-1 flex-col gap-3 overflow-y-auto p-4",
923
+ className
924
+ ),
925
+ "aria-busy": "true",
926
+ "aria-label": "Loading messages",
927
+ children: Array.from({ length: 8 }).map((_, i) => /* @__PURE__ */ jsx(
928
+ SkeletonMessage,
929
+ {
930
+ isOutgoing: i % 3 === 0,
931
+ showAvatar: showAvatars
932
+ },
933
+ i
934
+ ))
935
+ }
936
+ );
937
+ }
938
+ if (messages.length === 0) {
939
+ return emptyState || /* @__PURE__ */ jsx(EmptyState, {});
940
+ }
941
+ return /* @__PURE__ */ jsxs(
942
+ "div",
943
+ {
944
+ ref: scrollContainerRef,
945
+ "data-slot": "message-list",
946
+ className: cn(
947
+ "flex flex-1 flex-col overflow-y-auto",
948
+ "scroll-smooth",
949
+ className
950
+ ),
951
+ onScroll: handleScroll,
952
+ role: "log",
953
+ "aria-label": "Message history",
954
+ "aria-live": "polite",
955
+ children: [
956
+ hasMore && onLoadMore && /* @__PURE__ */ jsx(LoadMoreButton, { isLoading: isLoadingMore, onClick: onLoadMore }),
957
+ /* @__PURE__ */ jsx(
958
+ "div",
959
+ {
960
+ "data-slot": "message-list-content",
961
+ className: "flex flex-col gap-1 p-4",
962
+ children: messageGroups.map((group) => /* @__PURE__ */ jsxs(React6.Fragment, { children: [
963
+ groupByDate && group.label && /* @__PURE__ */ jsx(DateSeparator, { label: group.label }),
964
+ group.messages.map((message, index) => {
965
+ const prevMessage = group.messages[index - 1];
966
+ const isOutgoing = message.sender.id === currentUser.id;
967
+ const isSameGroup = isSameSenderGroup(prevMessage, message);
968
+ return /* @__PURE__ */ jsx(
969
+ "div",
970
+ {
971
+ className: cn(
972
+ "transition-opacity duration-200",
973
+ isSameGroup ? "mt-0.5" : "mt-3",
974
+ index === 0 && "mt-0"
975
+ ),
976
+ children: /* @__PURE__ */ jsx(
977
+ MessageBubble,
978
+ {
979
+ message,
980
+ isOutgoing,
981
+ showAvatar: showAvatars && !isSameGroup && !isOutgoing,
982
+ showSenderName: showSenderNames && !isSameGroup && !isOutgoing,
983
+ showTimestamp: !isSameGroup,
984
+ onRetry: message.status === "failed" && onRetryMessage ? () => onRetryMessage(message.id) : void 0,
985
+ onAttachmentClick: (attachment) => onAttachmentClick?.(attachment, message),
986
+ formatTimestamp
987
+ }
988
+ )
989
+ },
990
+ message.id
991
+ );
992
+ })
993
+ ] }, group.date))
994
+ }
995
+ ),
996
+ typingState && typingState.participants.length > 0 && /* @__PURE__ */ jsx(TypingIndicator, { typingState }),
997
+ /* @__PURE__ */ jsx("div", { ref: bottomRef, className: "h-0", "aria-hidden": "true" }),
998
+ isUserScrolled && /* @__PURE__ */ jsx(
999
+ "button",
1000
+ {
1001
+ type: "button",
1002
+ onClick: () => {
1003
+ bottomRef.current?.scrollIntoView({ behavior: "smooth" });
1004
+ },
1005
+ className: cn(
1006
+ "fixed end-4 bottom-24 z-10",
1007
+ "rounded-full p-3 shadow-lg",
1008
+ "bg-white dark:bg-neutral-800",
1009
+ "border border-neutral-200 dark:border-neutral-700",
1010
+ "hover:bg-neutral-50 dark:hover:bg-neutral-700",
1011
+ "focus:ring-primary-500 focus:ring-2 focus:outline-none",
1012
+ "transition-all"
1013
+ ),
1014
+ "aria-label": "Scroll to bottom",
1015
+ children: /* @__PURE__ */ jsx(
1016
+ "svg",
1017
+ {
1018
+ "aria-hidden": "true",
1019
+ className: "h-5 w-5 text-neutral-600 dark:text-neutral-300",
1020
+ fill: "none",
1021
+ viewBox: "0 0 24 24",
1022
+ stroke: "currentColor",
1023
+ children: /* @__PURE__ */ jsx(
1024
+ "path",
1025
+ {
1026
+ strokeLinecap: "round",
1027
+ strokeLinejoin: "round",
1028
+ strokeWidth: 2,
1029
+ d: "M19 14l-7 7m0 0l-7-7m7 7V3"
1030
+ }
1031
+ )
1032
+ }
1033
+ )
1034
+ }
1035
+ )
1036
+ ]
1037
+ }
1038
+ );
1039
+ }
1040
+ );
1041
+ MessageList.displayName = "MessageList";
1042
+ function isFileAccepted(file, accepted) {
1043
+ if (!accepted || accepted.length === 0) return true;
1044
+ const mime = file.type;
1045
+ const name = file.name.toLowerCase();
1046
+ return accepted.some((token) => {
1047
+ if (token.startsWith(".")) return name.endsWith(token.toLowerCase());
1048
+ if (token.endsWith("/*")) return mime.startsWith(token.slice(0, -1));
1049
+ if (token.includes("/")) return mime === token;
1050
+ return false;
1051
+ });
1052
+ }
1053
+ function CharacterCounter({
1054
+ current,
1055
+ max,
1056
+ showWarningAt = 0.9,
1256
1057
  className
1257
1058
  }) {
1059
+ const percentage = current / max;
1060
+ const isWarning = percentage >= showWarningAt && percentage < 1;
1061
+ const isOver = current > max;
1258
1062
  return /* @__PURE__ */ jsxs(
1259
- "div",
1063
+ "span",
1260
1064
  {
1261
- "data-slot": "message-empty-state",
1262
1065
  className: cn(
1263
- "flex flex-1 flex-col items-center justify-center p-8 text-center",
1066
+ "text-xs tabular-nums",
1067
+ 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",
1264
1068
  className
1265
1069
  ),
1266
- role: "status",
1267
- "aria-label": title,
1070
+ "aria-live": "polite",
1071
+ "aria-label": `${current} of ${max} characters`,
1268
1072
  children: [
1269
- icon || /* @__PURE__ */ jsx(
1270
- "div",
1271
- {
1272
- "data-slot": "message-empty-state-icon",
1273
- className: "mb-4 rounded-full bg-neutral-100 p-4 dark:bg-neutral-800",
1274
- children: /* @__PURE__ */ jsx(
1275
- "svg",
1276
- {
1277
- "aria-hidden": "true",
1278
- className: "h-12 w-12 text-neutral-500",
1279
- fill: "none",
1280
- viewBox: "0 0 24 24",
1281
- stroke: "currentColor",
1282
- children: /* @__PURE__ */ jsx(
1283
- "path",
1284
- {
1285
- strokeLinecap: "round",
1286
- strokeLinejoin: "round",
1287
- strokeWidth: 1.5,
1288
- d: "M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"
1289
- }
1290
- )
1291
- }
1292
- )
1293
- }
1294
- ),
1295
- /* @__PURE__ */ jsx("h3", { className: "mb-2 text-lg font-semibold text-neutral-900 dark:text-neutral-100", children: title }),
1296
- /* @__PURE__ */ jsx("p", { className: "text-muted-foreground mb-4 max-w-sm text-sm", children: description }),
1297
- action
1073
+ current,
1074
+ "/",
1075
+ max
1298
1076
  ]
1299
1077
  }
1300
1078
  );
1301
1079
  }
1302
- EmptyState.displayName = "EmptyState";
1303
- function LoadMoreButton({
1304
- isLoading,
1305
- onClick,
1306
- className
1307
- }) {
1308
- return /* @__PURE__ */ jsx(
1309
- "div",
1310
- {
1311
- "data-slot": "load-more-button",
1312
- className: cn("flex justify-center py-4", className),
1313
- children: /* @__PURE__ */ jsx(
1314
- "button",
1315
- {
1316
- type: "button",
1317
- onClick,
1318
- disabled: isLoading,
1319
- className: cn(
1320
- "rounded-full px-4 py-2 text-sm font-medium",
1321
- "bg-neutral-100 text-neutral-600 dark:bg-neutral-800 dark:text-neutral-300",
1322
- "hover:bg-neutral-200 dark:hover:bg-neutral-700",
1323
- "focus:ring-primary-500 focus:ring-2 focus:outline-none",
1324
- "disabled:cursor-not-allowed disabled:opacity-50",
1325
- "transition-colors"
1326
- ),
1327
- "aria-label": isLoading ? "Loading more messages" : "Load more messages",
1328
- children: isLoading ? /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-2", children: [
1329
- /* @__PURE__ */ jsxs(
1330
- "svg",
1080
+ CharacterCounter.displayName = "CharacterCounter";
1081
+ var sendButtonVariants = cva(
1082
+ [
1083
+ "inline-flex items-center justify-center",
1084
+ "rounded-full p-3 self-start",
1085
+ "transition-all duration-200",
1086
+ "focus:outline-none focus:ring-2 focus:ring-primary-500",
1087
+ "disabled:opacity-50 disabled:cursor-not-allowed"
1088
+ ],
1089
+ {
1090
+ variants: {
1091
+ variant: {
1092
+ primary: [
1093
+ "bg-primary-800 text-white",
1094
+ "hover:bg-primary-900",
1095
+ "active:scale-95"
1096
+ ],
1097
+ subtle: [
1098
+ "bg-transparent text-primary-800",
1099
+ "hover:bg-primary-50 dark:hover:bg-primary-900/20"
1100
+ ]
1101
+ },
1102
+ canSend: {
1103
+ true: "",
1104
+ false: "opacity-50 cursor-not-allowed"
1105
+ }
1106
+ },
1107
+ defaultVariants: {
1108
+ variant: "primary",
1109
+ canSend: false
1110
+ }
1111
+ }
1112
+ );
1113
+ var SendButton = React6.forwardRef(
1114
+ ({ className, variant, canSend, isLoading, disabled, ...props }, ref) => {
1115
+ return /* @__PURE__ */ jsx(
1116
+ "button",
1117
+ {
1118
+ ref,
1119
+ type: "submit",
1120
+ disabled: disabled || !canSend || isLoading,
1121
+ "data-slot": "composer-send-button",
1122
+ className: cn(sendButtonVariants({ variant, canSend }), className),
1123
+ "aria-label": isLoading ? "Sending message" : "Send message",
1124
+ ...props,
1125
+ children: isLoading ? /* @__PURE__ */ jsxs(
1126
+ "svg",
1127
+ {
1128
+ "aria-hidden": "true",
1129
+ className: "h-5 w-5 animate-spin",
1130
+ fill: "none",
1131
+ viewBox: "0 0 24 24",
1132
+ children: [
1133
+ /* @__PURE__ */ jsx(
1134
+ "circle",
1135
+ {
1136
+ className: "opacity-25",
1137
+ cx: "12",
1138
+ cy: "12",
1139
+ r: "10",
1140
+ stroke: "currentColor",
1141
+ strokeWidth: "4"
1142
+ }
1143
+ ),
1144
+ /* @__PURE__ */ jsx(
1145
+ "path",
1146
+ {
1147
+ className: "opacity-75",
1148
+ fill: "currentColor",
1149
+ d: "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
1150
+ }
1151
+ )
1152
+ ]
1153
+ }
1154
+ ) : /* @__PURE__ */ jsx(
1155
+ "svg",
1156
+ {
1157
+ "aria-hidden": "true",
1158
+ className: "h-5 w-5",
1159
+ fill: "none",
1160
+ viewBox: "0 0 24 24",
1161
+ stroke: "currentColor",
1162
+ children: /* @__PURE__ */ jsx(
1163
+ "path",
1331
1164
  {
1332
- "aria-hidden": "true",
1333
- className: "h-4 w-4 animate-spin",
1334
- fill: "none",
1335
- viewBox: "0 0 24 24",
1336
- children: [
1337
- /* @__PURE__ */ jsx(
1338
- "circle",
1339
- {
1340
- className: "opacity-25",
1341
- cx: "12",
1342
- cy: "12",
1343
- r: "10",
1344
- stroke: "currentColor",
1345
- strokeWidth: "4"
1346
- }
1347
- ),
1348
- /* @__PURE__ */ jsx(
1349
- "path",
1350
- {
1351
- className: "opacity-75",
1352
- fill: "currentColor",
1353
- d: "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
1354
- }
1355
- )
1356
- ]
1165
+ strokeLinecap: "round",
1166
+ strokeLinejoin: "round",
1167
+ strokeWidth: 2,
1168
+ d: "M12 19l9 2-9-18-9 18 9-2zm0 0v-8"
1357
1169
  }
1358
- ),
1359
- "Loading..."
1360
- ] }) : "Load earlier messages"
1361
- }
1362
- )
1363
- }
1364
- );
1365
- }
1366
- LoadMoreButton.displayName = "LoadMoreButton";
1367
- var MessageList = React6.forwardRef(
1170
+ )
1171
+ }
1172
+ )
1173
+ }
1174
+ );
1175
+ }
1176
+ );
1177
+ SendButton.displayName = "SendButton";
1178
+ var MessageComposer = React6.forwardRef(
1368
1179
  ({
1369
- messages,
1370
- currentUser,
1371
- isLoading = false,
1372
- hasMore = false,
1373
- isLoadingMore = false,
1374
- typingState,
1375
- showAvatars = true,
1376
- showSenderNames = false,
1377
- groupByDate = true,
1378
- onLoadMore,
1379
- onRetryMessage,
1380
- onAttachmentClick,
1381
- emptyState,
1382
- formatTimestamp,
1383
- className,
1384
- autoScroll = "onNewMessage"
1180
+ onSend,
1181
+ onTypingStart,
1182
+ onTypingStop,
1183
+ value: controlledValue,
1184
+ onValueChange,
1185
+ placeholder = "Type a message...",
1186
+ maxLength = 1600,
1187
+ showCharacterCount = false,
1188
+ disabled = false,
1189
+ isSending = false,
1190
+ showAttachmentPicker = true,
1191
+ showCameraButton = false,
1192
+ acceptedFileTypes = ["image/*", "video/*", ".pdf", ".doc", ".docx"],
1193
+ maxFileSize = 25 * 1024 * 1024,
1194
+ maxAttachments = 10,
1195
+ onError,
1196
+ autoFocus = false,
1197
+ replyTo = null,
1198
+ onCancelReply,
1199
+ variant = "default",
1200
+ inputTrailing,
1201
+ mentionOptions,
1202
+ className
1385
1203
  }, 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(() => {
1392
- const container = scrollContainerRef.current;
1393
- if (!container) return;
1394
- const { scrollTop, scrollHeight, clientHeight } = container;
1395
- const isAtBottom = scrollHeight - scrollTop - clientHeight < 100;
1396
- setIsUserScrolled(!isAtBottom);
1397
- }, []);
1204
+ const textareaRef = React6.useRef(null);
1205
+ const [internalContent, setInternalContent] = React6.useState("");
1206
+ const isControlled = controlledValue !== void 0;
1207
+ const content = isControlled ? controlledValue : internalContent;
1208
+ const setContent = React6.useCallback(
1209
+ (val) => {
1210
+ if (isControlled) {
1211
+ const newVal = typeof val === "function" ? val(controlledValue) : val;
1212
+ onValueChange?.(newVal);
1213
+ } else {
1214
+ setInternalContent(val);
1215
+ }
1216
+ },
1217
+ [isControlled, controlledValue, onValueChange]
1218
+ );
1219
+ const [attachments, setAttachments] = React6.useState(
1220
+ []
1221
+ );
1222
+ const [isTyping, setIsTyping] = React6.useState(false);
1223
+ const typingTimeoutRef = React6.useRef(null);
1224
+ React6.useImperativeHandle(ref, () => textareaRef.current);
1398
1225
  React6.useEffect(() => {
1399
- const container = scrollContainerRef.current;
1400
- const bottom = bottomRef.current;
1401
- if (!container || !bottom) return;
1402
- const messageCountChanged = messages.length !== prevMessageCountRef.current;
1403
- prevMessageCountRef.current = messages.length;
1404
- if (autoScroll === "always") {
1405
- bottom.scrollIntoView({ behavior: "smooth" });
1406
- } else if (autoScroll === "onNewMessage" && messageCountChanged) {
1407
- const lastMessage = messages[messages.length - 1];
1408
- const isOutgoing = lastMessage?.sender.id === currentUser.id;
1409
- if (isOutgoing || !isUserScrolled) {
1410
- bottom.scrollIntoView({ behavior: "smooth" });
1226
+ const textarea = textareaRef.current;
1227
+ if (textarea) {
1228
+ textarea.style.height = "auto";
1229
+ textarea.style.height = `${Math.min(textarea.scrollHeight, 150)}px`;
1230
+ }
1231
+ }, [content]);
1232
+ React6.useEffect(() => {
1233
+ if (content.length > 0 && !isTyping) {
1234
+ setIsTyping(true);
1235
+ onTypingStart?.();
1236
+ }
1237
+ if (typingTimeoutRef.current) {
1238
+ clearTimeout(typingTimeoutRef.current);
1239
+ }
1240
+ typingTimeoutRef.current = setTimeout(() => {
1241
+ if (isTyping) {
1242
+ setIsTyping(false);
1243
+ onTypingStop?.();
1244
+ }
1245
+ }, 2e3);
1246
+ return () => {
1247
+ if (typingTimeoutRef.current) {
1248
+ clearTimeout(typingTimeoutRef.current);
1411
1249
  }
1250
+ };
1251
+ }, [content, isTyping, onTypingStart, onTypingStop]);
1252
+ React6.useEffect(() => {
1253
+ if (autoFocus) {
1254
+ textareaRef.current?.focus();
1412
1255
  }
1413
- }, [messages, currentUser.id, autoScroll, isUserScrolled]);
1256
+ }, [autoFocus]);
1257
+ const replyToId = replyTo?.id;
1414
1258
  React6.useEffect(() => {
1415
- const bottom = bottomRef.current;
1416
- if (bottom && !isLoading) {
1417
- bottom.scrollIntoView();
1259
+ if (replyToId !== void 0) {
1260
+ textareaRef.current?.focus();
1418
1261
  }
1419
- }, [isLoading]);
1420
- const messageGroups = groupByDate ? groupMessagesByDate(messages) : [{ date: "all", label: "", messages }];
1421
- if (isLoading) {
1422
- return /* @__PURE__ */ jsx(
1423
- "div",
1424
- {
1425
- role: "status",
1426
- className: cn(
1427
- "flex flex-1 flex-col gap-3 overflow-y-auto p-4",
1428
- className
1429
- ),
1430
- "aria-busy": "true",
1431
- "aria-label": "Loading messages",
1432
- children: Array.from({ length: 8 }).map((_, i) => /* @__PURE__ */ jsx(
1433
- SkeletonMessage,
1434
- {
1435
- isOutgoing: i % 3 === 0,
1436
- showAvatar: showAvatars
1437
- },
1438
- i
1439
- ))
1262
+ }, [replyToId]);
1263
+ const mention = useMentionAutocomplete({
1264
+ options: mentionOptions,
1265
+ value: content,
1266
+ setValue: setContent,
1267
+ textareaRef
1268
+ });
1269
+ const canSend = (content.trim().length > 0 || attachments.length > 0) && content.length <= maxLength && !disabled && !isSending;
1270
+ const handleSubmit = async (event) => {
1271
+ event.preventDefault();
1272
+ if (!canSend) return;
1273
+ const message = {
1274
+ content: content.trim(),
1275
+ attachments: attachments.map((a) => a.file),
1276
+ replyToId: replyTo?.id
1277
+ };
1278
+ setContent("");
1279
+ setAttachments([]);
1280
+ setIsTyping(false);
1281
+ onTypingStop?.();
1282
+ try {
1283
+ await onSend(message);
1284
+ } catch {
1285
+ setContent(message.content);
1286
+ onError?.("Failed to send message");
1287
+ }
1288
+ };
1289
+ const handleKeyDown = (event) => {
1290
+ if (mention.handleKeyDown(event)) return;
1291
+ if (event.key === "Enter" && !event.shiftKey) {
1292
+ event.preventDefault();
1293
+ if (canSend) {
1294
+ handleSubmit(event);
1440
1295
  }
1441
- );
1442
- }
1443
- if (messages.length === 0) {
1444
- return emptyState || /* @__PURE__ */ jsx(EmptyState, {});
1445
- }
1446
- return /* @__PURE__ */ jsxs(
1447
- "div",
1448
- {
1449
- ref: scrollContainerRef,
1450
- "data-slot": "message-list",
1451
- className: cn(
1452
- "flex flex-1 flex-col overflow-y-auto",
1453
- "scroll-smooth",
1454
- className
1455
- ),
1456
- onScroll: handleScroll,
1457
- role: "log",
1458
- "aria-label": "Message history",
1459
- "aria-live": "polite",
1460
- children: [
1461
- hasMore && onLoadMore && /* @__PURE__ */ jsx(LoadMoreButton, { isLoading: isLoadingMore, onClick: onLoadMore }),
1462
- /* @__PURE__ */ jsx(
1463
- "div",
1464
- {
1465
- "data-slot": "message-list-content",
1466
- className: "flex flex-col gap-1 p-4",
1467
- children: messageGroups.map((group) => /* @__PURE__ */ jsxs(React6.Fragment, { children: [
1468
- groupByDate && group.label && /* @__PURE__ */ jsx(DateSeparator, { label: group.label }),
1469
- group.messages.map((message, index) => {
1470
- const prevMessage = group.messages[index - 1];
1471
- const isOutgoing = message.sender.id === currentUser.id;
1472
- const isSameGroup = isSameSenderGroup(prevMessage, message);
1473
- return /* @__PURE__ */ jsx(
1474
- "div",
1296
+ }
1297
+ };
1298
+ const handleFilesSelected = (files) => {
1299
+ const remainingSlots = maxAttachments - attachments.length;
1300
+ const filesToAdd = files.slice(0, remainingSlots);
1301
+ if (files.length > remainingSlots) {
1302
+ onError?.(`Maximum ${maxAttachments} attachments allowed`);
1303
+ }
1304
+ const newAttachments = filesToAdd.map((file) => {
1305
+ const type = getFileType(file.type);
1306
+ let previewUrl;
1307
+ if (type === "image" || type === "video") {
1308
+ previewUrl = URL.createObjectURL(file);
1309
+ }
1310
+ return {
1311
+ id: generateAttachmentId(),
1312
+ file,
1313
+ previewUrl,
1314
+ type,
1315
+ state: "pending"
1316
+ };
1317
+ });
1318
+ setAttachments((prev) => [...prev, ...newAttachments]);
1319
+ };
1320
+ const handlePaste = (event) => {
1321
+ if (!showAttachmentPicker || disabled) return;
1322
+ 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));
1323
+ if (pasted.length === 0) return;
1324
+ event.preventDefault();
1325
+ handleFilesSelected(pasted);
1326
+ };
1327
+ const handleRemoveAttachment = (attachmentId) => {
1328
+ setAttachments((prev) => {
1329
+ const attachment = prev.find((a) => a.id === attachmentId);
1330
+ if (attachment?.previewUrl) {
1331
+ URL.revokeObjectURL(attachment.previewUrl);
1332
+ }
1333
+ return prev.filter((a) => a.id !== attachmentId);
1334
+ });
1335
+ };
1336
+ React6.useEffect(() => {
1337
+ const currentAttachments = attachments;
1338
+ return () => {
1339
+ currentAttachments.forEach((a) => {
1340
+ if (a.previewUrl) {
1341
+ URL.revokeObjectURL(a.previewUrl);
1342
+ }
1343
+ });
1344
+ };
1345
+ }, [attachments]);
1346
+ return /* @__PURE__ */ jsx(
1347
+ DragDropZone,
1348
+ {
1349
+ onFilesDropped: handleFilesSelected,
1350
+ acceptedTypes: acceptedFileTypes,
1351
+ maxFileSize,
1352
+ maxFiles: maxAttachments - attachments.length,
1353
+ disabled: disabled || attachments.length >= maxAttachments,
1354
+ onError,
1355
+ className: cn("w-full", className),
1356
+ children: /* @__PURE__ */ jsxs(
1357
+ "form",
1358
+ {
1359
+ onSubmit: handleSubmit,
1360
+ "data-slot": "message-composer",
1361
+ className: "w-full",
1362
+ children: [
1363
+ replyTo && /* @__PURE__ */ jsxs(
1364
+ "div",
1365
+ {
1366
+ "data-slot": "composer-reply-preview",
1367
+ className: cn(
1368
+ "flex items-center gap-2 px-4 py-2",
1369
+ "bg-neutral-50 dark:bg-neutral-800/50",
1370
+ "border-primary-500 border-s-4"
1371
+ ),
1372
+ children: [
1373
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
1374
+ /* @__PURE__ */ jsxs("span", { className: "text-primary-800 dark:text-primary-400 text-xs font-medium", children: [
1375
+ "Replying to ",
1376
+ replyTo.senderName
1377
+ ] }),
1378
+ /* @__PURE__ */ jsx("p", { className: "truncate text-sm text-neutral-600 dark:text-neutral-300", children: replyTo.content })
1379
+ ] }),
1380
+ /* @__PURE__ */ jsx(
1381
+ "button",
1382
+ {
1383
+ type: "button",
1384
+ onClick: onCancelReply,
1385
+ className: cn(
1386
+ "shrink-0 rounded p-1",
1387
+ "text-neutral-500 hover:text-neutral-700",
1388
+ "dark:text-neutral-400 dark:hover:text-neutral-200",
1389
+ "focus:ring-primary-500 focus:ring-2 focus:outline-none"
1390
+ ),
1391
+ "aria-label": "Cancel reply",
1392
+ children: /* @__PURE__ */ jsx(
1393
+ "svg",
1394
+ {
1395
+ "aria-hidden": "true",
1396
+ className: "h-4 w-4",
1397
+ fill: "none",
1398
+ viewBox: "0 0 24 24",
1399
+ stroke: "currentColor",
1400
+ children: /* @__PURE__ */ jsx(
1401
+ "path",
1402
+ {
1403
+ strokeLinecap: "round",
1404
+ strokeLinejoin: "round",
1405
+ strokeWidth: 2,
1406
+ d: "M6 18L18 6M6 6l12 12"
1407
+ }
1408
+ )
1409
+ }
1410
+ )
1411
+ }
1412
+ )
1413
+ ]
1414
+ }
1415
+ ),
1416
+ attachments.length > 0 && /* @__PURE__ */ jsx(
1417
+ "div",
1418
+ {
1419
+ "data-slot": "composer-attachments",
1420
+ className: cn(
1421
+ "flex flex-wrap gap-2 p-3",
1422
+ "border-t border-neutral-200 dark:border-neutral-700"
1423
+ ),
1424
+ children: attachments.map((attachment) => /* @__PURE__ */ jsx(
1425
+ AttachmentPreviewItem,
1475
1426
  {
1476
- className: cn(
1477
- "transition-opacity duration-200",
1478
- isSameGroup ? "mt-0.5" : "mt-3",
1479
- index === 0 && "mt-0"
1480
- ),
1481
- children: /* @__PURE__ */ jsx(
1482
- MessageBubble,
1483
- {
1484
- message,
1485
- isOutgoing,
1486
- showAvatar: showAvatars && !isSameGroup && !isOutgoing,
1487
- showSenderName: showSenderNames && !isSameGroup && !isOutgoing,
1488
- showTimestamp: !isSameGroup,
1489
- onRetry: message.status === "failed" && onRetryMessage ? () => onRetryMessage(message.id) : void 0,
1490
- onAttachmentClick: (attachment) => onAttachmentClick?.(attachment, message),
1491
- formatTimestamp
1492
- }
1493
- )
1427
+ attachment,
1428
+ onRemove: () => handleRemoveAttachment(attachment.id)
1494
1429
  },
1495
- message.id
1496
- );
1497
- })
1498
- ] }, group.date))
1499
- }
1500
- ),
1501
- typingState && typingState.participants.length > 0 && /* @__PURE__ */ jsx(TypingIndicator, { typingState }),
1502
- /* @__PURE__ */ jsx("div", { ref: bottomRef, className: "h-0", "aria-hidden": "true" }),
1503
- isUserScrolled && /* @__PURE__ */ jsx(
1504
- "button",
1505
- {
1506
- type: "button",
1507
- onClick: () => {
1508
- bottomRef.current?.scrollIntoView({ behavior: "smooth" });
1509
- },
1510
- className: cn(
1511
- "fixed end-4 bottom-24 z-10",
1512
- "rounded-full p-3 shadow-lg",
1513
- "bg-white dark:bg-neutral-800",
1514
- "border border-neutral-200 dark:border-neutral-700",
1515
- "hover:bg-neutral-50 dark:hover:bg-neutral-700",
1516
- "focus:ring-primary-500 focus:ring-2 focus:outline-none",
1517
- "transition-all"
1430
+ attachment.id
1431
+ ))
1432
+ }
1518
1433
  ),
1519
- "aria-label": "Scroll to bottom",
1520
- children: /* @__PURE__ */ jsx(
1521
- "svg",
1434
+ /* @__PURE__ */ jsxs(
1435
+ "div",
1522
1436
  {
1523
- "aria-hidden": "true",
1524
- className: "h-5 w-5 text-neutral-600 dark:text-neutral-300",
1525
- fill: "none",
1526
- viewBox: "0 0 24 24",
1527
- stroke: "currentColor",
1528
- children: /* @__PURE__ */ jsx(
1529
- "path",
1530
- {
1531
- strokeLinecap: "round",
1532
- strokeLinejoin: "round",
1533
- strokeWidth: 2,
1534
- d: "M19 14l-7 7m0 0l-7-7m7 7V3"
1535
- }
1536
- )
1437
+ "data-slot": "composer-input-area",
1438
+ className: cn(
1439
+ "flex items-center gap-2 p-3",
1440
+ "bg-white dark:bg-neutral-900",
1441
+ variant === "default" && "border-t border-neutral-200 dark:border-neutral-700"
1442
+ ),
1443
+ children: [
1444
+ showAttachmentPicker && /* @__PURE__ */ jsx(
1445
+ AttachmentPicker,
1446
+ {
1447
+ onFilesSelected: handleFilesSelected,
1448
+ acceptedTypes: acceptedFileTypes,
1449
+ maxFileSize,
1450
+ maxFiles: maxAttachments - attachments.length,
1451
+ disabled: disabled || attachments.length >= maxAttachments,
1452
+ onError
1453
+ }
1454
+ ),
1455
+ showCameraButton && /* @__PURE__ */ jsx(
1456
+ CameraButton,
1457
+ {
1458
+ onCapture: (file) => handleFilesSelected([file]),
1459
+ disabled: disabled || attachments.length >= maxAttachments
1460
+ }
1461
+ ),
1462
+ /* @__PURE__ */ jsxs(
1463
+ "div",
1464
+ {
1465
+ "data-slot": "composer-input-wrapper",
1466
+ className: "relative flex-1",
1467
+ ref: mention.anchorRef,
1468
+ children: [
1469
+ /* @__PURE__ */ jsx(MentionMenu, { mention }),
1470
+ /* @__PURE__ */ jsx(
1471
+ "textarea",
1472
+ {
1473
+ ref: textareaRef,
1474
+ "data-slot": "composer-input",
1475
+ value: content,
1476
+ onChange: (e) => {
1477
+ setContent(e.target.value);
1478
+ mention.sync(
1479
+ e.target.value,
1480
+ e.target.selectionStart ?? e.target.value.length
1481
+ );
1482
+ },
1483
+ onClick: (e) => {
1484
+ const el = e.currentTarget;
1485
+ mention.sync(el.value, el.selectionStart ?? el.value.length);
1486
+ },
1487
+ onKeyDown: handleKeyDown,
1488
+ onPaste: handlePaste,
1489
+ placeholder,
1490
+ disabled: disabled || isSending,
1491
+ rows: 1,
1492
+ className: cn(
1493
+ "w-full resize-none rounded-2xl py-2.5",
1494
+ inputTrailing ? "ps-4 pe-10" : "px-4",
1495
+ "bg-neutral-100 dark:bg-neutral-800",
1496
+ "text-neutral-900 dark:text-neutral-100",
1497
+ "placeholder:text-neutral-400 dark:placeholder:text-neutral-500",
1498
+ "focus:ring-primary-500 focus:ring-2 focus:outline-none",
1499
+ "disabled:cursor-not-allowed disabled:opacity-50",
1500
+ "transition-colors",
1501
+ "max-h-[150px]"
1502
+ ),
1503
+ "aria-label": "Message",
1504
+ "aria-describedby": showCharacterCount ? "char-count" : void 0,
1505
+ ...mention.inputProps
1506
+ }
1507
+ ),
1508
+ inputTrailing && /* @__PURE__ */ jsx(
1509
+ "div",
1510
+ {
1511
+ "data-slot": "composer-input-trailing",
1512
+ className: "pointer-events-none absolute end-1 top-0 flex h-[44px] items-center [&>*]:pointer-events-auto",
1513
+ children: inputTrailing
1514
+ }
1515
+ ),
1516
+ showCharacterCount && /* @__PURE__ */ jsx(
1517
+ "div",
1518
+ {
1519
+ "data-slot": "composer-char-count",
1520
+ id: "char-count",
1521
+ className: "absolute end-3 bottom-1.5",
1522
+ children: /* @__PURE__ */ jsx(CharacterCounter, { current: content.length, max: maxLength })
1523
+ }
1524
+ )
1525
+ ]
1526
+ }
1527
+ ),
1528
+ /* @__PURE__ */ jsx(
1529
+ SendButton,
1530
+ {
1531
+ canSend,
1532
+ isLoading: isSending,
1533
+ disabled
1534
+ }
1535
+ )
1536
+ ]
1537
1537
  }
1538
1538
  )
1539
- }
1540
- )
1541
- ]
1539
+ ]
1540
+ }
1541
+ )
1542
1542
  }
1543
1543
  );
1544
1544
  }
1545
1545
  );
1546
- MessageList.displayName = "MessageList";
1546
+ MessageComposer.displayName = "MessageComposer";
1547
1547
  var headerVariants = cva(
1548
1548
  [
1549
1549
  "flex items-center gap-3 px-4 py-3",
@@ -2471,5 +2471,5 @@ function useReadReceipts(options) {
2471
2471
  }
2472
2472
 
2473
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
2474
+ //# sourceMappingURL=chunk-PL6TFCYR.js.map
2475
+ //# sourceMappingURL=chunk-PL6TFCYR.js.map