@connectycube/react-ui-kit 0.0.19 → 0.0.22

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 (78) hide show
  1. package/configs/dependencies.json +21 -0
  2. package/configs/imports.json +7 -0
  3. package/dist/index.cjs +1 -36
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.js +1 -35
  6. package/dist/index.js.map +1 -1
  7. package/dist/types/components/attachment.d.ts +7 -8
  8. package/dist/types/components/attachment.d.ts.map +1 -1
  9. package/dist/types/components/avatar.d.ts +1 -0
  10. package/dist/types/components/avatar.d.ts.map +1 -1
  11. package/dist/types/components/badge.d.ts +1 -1
  12. package/dist/types/components/button.d.ts +2 -2
  13. package/dist/types/components/chat-bubble.d.ts +32 -0
  14. package/dist/types/components/chat-bubble.d.ts.map +1 -0
  15. package/dist/types/components/chat-input.d.ts +27 -0
  16. package/dist/types/components/chat-input.d.ts.map +1 -0
  17. package/dist/types/components/chat-list.d.ts +30 -0
  18. package/dist/types/components/chat-list.d.ts.map +1 -0
  19. package/dist/types/components/checkbox.d.ts +11 -0
  20. package/dist/types/components/checkbox.d.ts.map +1 -0
  21. package/dist/types/components/dialog-item.d.ts.map +1 -1
  22. package/dist/types/components/dialogs-list.d.ts +14 -0
  23. package/dist/types/components/dialogs-list.d.ts.map +1 -0
  24. package/dist/types/components/file-picker.d.ts +1 -1
  25. package/dist/types/components/file-picker.d.ts.map +1 -1
  26. package/dist/types/components/linkify-text.d.ts +6 -1
  27. package/dist/types/components/linkify-text.d.ts.map +1 -1
  28. package/dist/types/components/placeholder-text.d.ts.map +1 -1
  29. package/dist/types/components/quick-actions.d.ts +14 -0
  30. package/dist/types/components/quick-actions.d.ts.map +1 -0
  31. package/dist/types/components/status-call.d.ts +8 -0
  32. package/dist/types/components/status-call.d.ts.map +1 -0
  33. package/dist/types/components/switch.d.ts.map +1 -1
  34. package/dist/types/index.d.ts +8 -0
  35. package/dist/types/index.d.ts.map +1 -1
  36. package/gen/components/attachment.jsx +27 -25
  37. package/gen/components/avatar.jsx +14 -2
  38. package/gen/components/button.jsx +1 -1
  39. package/gen/components/chat-bubble.jsx +141 -0
  40. package/gen/components/chat-input.jsx +152 -0
  41. package/gen/components/chat-list.jsx +151 -0
  42. package/gen/components/checkbox.jsx +30 -0
  43. package/gen/components/dialog-item.jsx +5 -2
  44. package/gen/components/dialogs-list.jsx +73 -0
  45. package/gen/components/dismiss-layer.jsx +1 -1
  46. package/gen/components/file-picker.jsx +2 -2
  47. package/gen/components/linkify-text.jsx +41 -2
  48. package/gen/components/placeholder-text.jsx +5 -1
  49. package/gen/components/quick-actions.jsx +62 -0
  50. package/gen/components/search.jsx +1 -1
  51. package/gen/components/status-call.jsx +18 -0
  52. package/gen/components/stream-view.jsx +8 -8
  53. package/gen/components/switch.jsx +0 -2
  54. package/gen/index.js +16 -0
  55. package/package.json +17 -13
  56. package/src/components/attachment.tsx +38 -37
  57. package/src/components/avatar.tsx +3 -1
  58. package/src/components/button.tsx +1 -1
  59. package/src/components/chat-bubble.tsx +176 -0
  60. package/src/components/chat-input.tsx +172 -0
  61. package/src/components/chat-list.tsx +164 -0
  62. package/src/components/checkbox.tsx +40 -0
  63. package/src/components/connectycube-ui/attachment.tsx +269 -0
  64. package/src/components/connectycube-ui/chat-input.tsx +174 -0
  65. package/src/components/connectycube-ui/chat-message.tsx +138 -0
  66. package/src/components/connectycube-ui/link-preview.tsx +149 -0
  67. package/src/components/dialog-item.tsx +5 -2
  68. package/src/components/dialogs-list.tsx +84 -0
  69. package/src/components/dismiss-layer.tsx +1 -1
  70. package/src/components/file-picker.tsx +3 -3
  71. package/src/components/linkify-text.tsx +44 -3
  72. package/src/components/placeholder-text.tsx +5 -1
  73. package/src/components/quick-actions.tsx +74 -0
  74. package/src/components/search.tsx +1 -1
  75. package/src/components/status-call.tsx +23 -0
  76. package/src/components/stream-view.tsx +8 -8
  77. package/src/components/switch.tsx +0 -2
  78. package/src/index.ts +21 -0
@@ -0,0 +1,62 @@
1
+ import { forwardRef } from 'react';
2
+ import { cn } from './utils';
3
+
4
+ function QuickActionsBase(
5
+ {
6
+ title,
7
+ description,
8
+ actions = [],
9
+ onAction = () => {},
10
+ containerProps,
11
+ titleProps,
12
+ descriptionProps,
13
+ actionProps,
14
+ ...props
15
+ },
16
+ ref
17
+ ) {
18
+ if (!actions.length) {
19
+ return null;
20
+ }
21
+
22
+ return (
23
+ <div
24
+ ref={ref}
25
+ {...containerProps}
26
+ className={cn('flex flex-col h-full overflow-y-auto py-2', containerProps?.className)}
27
+ >
28
+ <div className="grow" />
29
+ <div {...props} className={cn('grid gap-2 m-4', props?.className)}>
30
+ {title && (
31
+ <h2 {...titleProps} className={cn('font-medium text-foreground text-lg', titleProps?.className)}>
32
+ {title}
33
+ </h2>
34
+ )}
35
+ {description && (
36
+ <span {...descriptionProps} className={cn('text-sm text-muted-foreground mb-4', descriptionProps?.className)}>
37
+ {description}
38
+ </span>
39
+ )}
40
+ {actions.map((action, index) => (
41
+ <div
42
+ key={index}
43
+ {...actionProps}
44
+ className={cn(
45
+ 'w-full border p-2 rounded-md wrap-break-word cursor-pointer overflow-hidden text-ellipsis bg-ring/5 hover:bg-ring/20 transition-colors duration-200 ease-out',
46
+ actionProps?.className
47
+ )}
48
+ onClick={() => onAction(action)}
49
+ >
50
+ {action}
51
+ </div>
52
+ ))}
53
+ </div>
54
+ </div>
55
+ );
56
+ }
57
+
58
+ const QuickActions = forwardRef(QuickActionsBase);
59
+
60
+ QuickActions.displayName = 'QuickActions';
61
+
62
+ export { QuickActions };
@@ -58,7 +58,7 @@ function SearchBase(
58
58
  onClick={handleOnCancel}
59
59
  {...cancelIconProps}
60
60
  className={cn(
61
- 'absolute top-1/2 right-2 transform -translate-y-1/2 size-5 text-muted-foreground cursor-pointer hover:text-ring transition-all duration-300 ease-in-out',
61
+ 'absolute top-1/2 right-2 transform -translate-y-1/2 size-5 text-muted-foreground cursor-pointer hover:text-ring transition-all duration-300 ease-out',
62
62
  value.length > 0 ? 'opacity-100 scale-100' : 'opacity-0 scale-75 pointer-events-none',
63
63
  cancelIconProps?.className
64
64
  )}
@@ -0,0 +1,18 @@
1
+ import { Phone, PhoneIncoming, PhoneMissed, PhoneOutgoing } from 'lucide-react';
2
+ import { cn } from './utils';
3
+
4
+ const StatusCall = ({ fromMe, status, ...props }) => {
5
+ const CallIcon =
6
+ status === 'hungUp' ? Phone : fromMe ? PhoneOutgoing : status === 'reject' ? PhoneIncoming : PhoneMissed;
7
+
8
+ return (
9
+ <CallIcon
10
+ {...props}
11
+ className={cn('size-4', status === 'hungUp' ? 'text-green-500' : 'text-red-500', props?.className)}
12
+ />
13
+ );
14
+ };
15
+
16
+ StatusCall.displayName = 'StatusCall';
17
+
18
+ export { StatusCall };
@@ -9,7 +9,7 @@ function StreamViewBase({ id, stream, mirror, className, muted, ...props }, ref)
9
9
  const defaultClassName = 'size-full object-contain';
10
10
  const mirrorClassName = mirror ? 'scale-x-[-1]' : '';
11
11
 
12
- useImperativeHandle(ref, () => innerRef.current);
12
+ useImperativeHandle(ref, () => innerRef.current || {}, []);
13
13
 
14
14
  useEffect(() => {
15
15
  if (innerRef.current && stream) {
@@ -116,13 +116,13 @@ function FullscreenStreamViewBase(
116
116
 
117
117
  useImperativeHandle(
118
118
  ref,
119
- () =>
120
- Object.assign(innerRef.current, {
121
- isFullscreen,
122
- isPictureInPicture,
123
- toggleFullscreen,
124
- togglePictureInPicture,
125
- }),
119
+ () => ({
120
+ ...(innerRef.current || {}),
121
+ isFullscreen,
122
+ isPictureInPicture,
123
+ toggleFullscreen,
124
+ togglePictureInPicture,
125
+ }),
126
126
  [isFullscreen, isPictureInPicture, toggleFullscreen, togglePictureInPicture]
127
127
  );
128
128
 
@@ -1,5 +1,3 @@
1
- 'use client';
2
-
3
1
  import * as React from 'react';
4
2
  import * as SwitchPrimitive from '@radix-ui/react-switch';
5
3
  import { cn } from './utils';
package/gen/index.js CHANGED
@@ -1,3 +1,5 @@
1
+ export { AlertDialog } from './components/alert-dialog';
2
+
1
3
  export {
2
4
  Attachment,
3
5
  AttachmentLink,
@@ -14,8 +16,18 @@ export { Badge } from './components/badge';
14
16
 
15
17
  export { Button } from './components/button';
16
18
 
19
+ export { ChatBubbleMessage, ChatBubbleInfo } from './components/chat-bubble';
20
+
21
+ export { ChatInput } from './components/chat-input';
22
+
23
+ export { ChatList } from './components/chat-list';
24
+
25
+ export { Checkbox } from './components/checkbox';
26
+
17
27
  export { DialogItem } from './components/dialog-item';
18
28
 
29
+ export { DialogsList } from './components/dialogs-list';
30
+
19
31
  export { DismissLayer } from './components/dismiss-layer';
20
32
 
21
33
  export { FilePickerInput, FilePickerDropzone } from './components/file-picker';
@@ -34,10 +46,14 @@ export { PlaceholderText } from './components/placeholder-text';
34
46
 
35
47
  export { Presence, PresenceBadge } from './components/presence';
36
48
 
49
+ export { QuickActions } from './components/quick-actions';
50
+
37
51
  export { Search } from './components/search';
38
52
 
39
53
  export { Spinner } from './components/spinner';
40
54
 
55
+ export { StatusCall } from './components/status-call';
56
+
41
57
  export { StatusIndicator } from './components/status-indicator';
42
58
 
43
59
  export { StatusSent } from './components/status-sent';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@connectycube/react-ui-kit",
3
- "version": "0.0.19",
3
+ "version": "0.0.22",
4
4
  "description": "Simple React UI Kit generator with TSX/JSX",
5
5
  "homepage": "https://github.com/ConnectyCube/react-ui-kit#readme",
6
6
  "bugs": {
@@ -64,6 +64,7 @@
64
64
  "dependencies": {
65
65
  "@radix-ui/react-alert-dialog": "^1.1.15",
66
66
  "@radix-ui/react-avatar": "^1.1.11",
67
+ "@radix-ui/react-checkbox": "^1.3.3",
67
68
  "@radix-ui/react-label": "^2.1.8",
68
69
  "@radix-ui/react-slot": "^1.2.4",
69
70
  "@radix-ui/react-switch": "^1.2.6",
@@ -71,12 +72,12 @@
71
72
  "class-variance-authority": "^0.7.1",
72
73
  "clsx": "^2.1.1",
73
74
  "date-fns": "^4.1.0",
74
- "execa": "^9.6.0",
75
- "fs-extra": "^11.3.2",
76
75
  "linkify-react": "^4.3.2",
77
- "lucide-react": "^0.555.0",
78
- "prompts": "^2.4.2",
79
- "tailwind-merge": "^3.4.0"
76
+ "lucide-react": "^0.562.0",
77
+ "react-intersection-observer": "^10.0.0",
78
+ "react-textarea-autosize": "^8.5.9",
79
+ "tailwind-merge": "^3.4.0",
80
+ "virtua": "^0.48.2"
80
81
  },
81
82
  "peerDependencies": {
82
83
  "react": ">=18",
@@ -85,25 +86,28 @@
85
86
  "devDependencies": {
86
87
  "@babel/core": "^7.28.5",
87
88
  "@babel/preset-typescript": "^7.28.5",
88
- "@eslint/js": "^9.39.1",
89
+ "@eslint/js": "^9.39.2",
89
90
  "@rollup/plugin-commonjs": "^29.0.0",
90
91
  "@rollup/plugin-node-resolve": "^16.0.3",
91
92
  "@rollup/plugin-terser": "^0.4.4",
92
93
  "@rollup/plugin-typescript": "^12.3.0",
93
94
  "@stylistic/eslint-plugin": "^5.6.1",
94
- "@types/node": "^24.10.1",
95
+ "@types/node": "^25.0.3",
95
96
  "@types/react": "^19.2.7",
96
- "eslint": "^9.39.1",
97
+ "eslint": "^9.39.2",
97
98
  "eslint-plugin-react-hooks": "^7.0.1",
98
- "eslint-plugin-react-refresh": "^0.4.24",
99
+ "eslint-plugin-react-refresh": "^0.4.26",
100
+ "execa": "^9.6.1",
99
101
  "fast-glob": "^3.3.3",
102
+ "fs-extra": "^11.3.3",
100
103
  "globals": "^16.5.0",
101
- "prettier": "^3.6.2",
102
- "rollup": "^4.53.3",
104
+ "prettier": "^3.7.4",
105
+ "prompts": "^2.4.2",
106
+ "rollup": "^4.53.5",
103
107
  "rollup-plugin-peer-deps-external": "^2.2.4",
104
108
  "tslib": "^2.8.1",
105
109
  "typescript": "^5.9.3",
106
- "typescript-eslint": "^8.48.0"
110
+ "typescript-eslint": "^8.50.0"
107
111
  },
108
112
  "engines": {
109
113
  "node": ">=18"
@@ -5,28 +5,21 @@ import { Spinner } from './spinner';
5
5
  import { cn, getRandomString } from './utils';
6
6
 
7
7
  interface AttachmentProps {
8
+ pending?: boolean;
8
9
  uid?: string;
9
10
  url?: string;
10
11
  mimeType?: string;
11
- uploading?: boolean;
12
- onReady?: (skipOnce?: boolean) => void;
12
+ onReady?: () => void;
13
13
  linkProps?: AttachmentLinkProps;
14
14
  containerProps?: React.ComponentProps<'div'>;
15
15
  }
16
16
 
17
- interface AttachmentLinkProps
18
- extends React.ComponentProps<'a'>,
19
- Omit<AttachmentProps, 'containerProps' & 'mimeType' & 'onReady'> {
20
- children?: React.ReactNode;
21
- }
17
+ interface AttachmentLinkProps extends React.ComponentProps<'a'>, Omit<AttachmentProps, 'containerProps' & 'mimeType'> {}
22
18
 
23
19
  interface AttachmentImageProps
24
- extends React.ComponentProps<'img'>,
25
- Omit<AttachmentProps, 'containerProps' & 'mimeType'> {}
20
+ extends React.ComponentProps<'img'>, Omit<AttachmentProps, 'containerProps' & 'mimeType'> {}
26
21
 
27
- interface AttachmentAudioProps
28
- extends React.ComponentProps<'audio'>,
29
- Omit<AttachmentProps, 'linkProps' & 'mimeType' & 'onReady'> {}
22
+ interface AttachmentAudioProps extends React.ComponentProps<'audio'>, Omit<AttachmentProps, 'linkProps' & 'mimeType'> {}
30
23
 
31
24
  interface AttachmentVideoProps extends React.ComponentProps<'video'>, Omit<AttachmentProps, 'linkProps' & 'mimeType'> {
32
25
  maxSize?: number;
@@ -43,7 +36,7 @@ interface AttachmentFailedProps extends LucideProps, Omit<AttachmentProps, 'link
43
36
  }
44
37
 
45
38
  function AttachmentLinkBase(
46
- { url, uploading = false, children, ...props }: AttachmentLinkProps,
39
+ { url, pending = false, children, ...props }: AttachmentLinkProps,
47
40
  ref: React.ForwardedRef<HTMLAnchorElement>
48
41
  ) {
49
42
  return (
@@ -54,12 +47,12 @@ function AttachmentLinkBase(
54
47
  {...props}
55
48
  href={url}
56
49
  className={cn(
57
- 'group relative min-h-12 min-w-12 w-full flex items-center justify-center rounded-md overflow-hidden bg-ring/10 hover:bg-ring/20 transition-color duration-300 ease-out cursor-pointer',
50
+ 'group relative min-h-8 min-w-8 w-full flex items-center justify-center rounded-md overflow-hidden bg-ring/10 hover:bg-ring/20 transition-color duration-300 ease-out cursor-pointer',
58
51
  props?.className
59
52
  )}
60
53
  >
61
54
  {children}
62
- <Spinner loading={uploading} layout="overlay" />
55
+ <Spinner loading={pending} layout="overlay" />
63
56
  </a>
64
57
  );
65
58
  }
@@ -69,7 +62,7 @@ const AttachmentLink = forwardRef<HTMLAnchorElement, AttachmentLinkProps>(Attach
69
62
  AttachmentLink.displayName = 'AttachmentLink';
70
63
 
71
64
  function AttachmentAudioBase(
72
- { uid, url, uploading = false, containerProps, ...props }: AttachmentAudioProps,
65
+ { uid, url, pending = false, containerProps, ...props }: AttachmentAudioProps,
73
66
  ref: React.ForwardedRef<HTMLAudioElement>
74
67
  ) {
75
68
  const audioId = `attachment_audio_${uid || getRandomString()}`;
@@ -77,10 +70,10 @@ function AttachmentAudioBase(
77
70
  return (
78
71
  <div
79
72
  {...containerProps}
80
- className={cn('relative min-h-12 min-w-12 w-full rounded-md overflow-hidden', containerProps?.className)}
73
+ className={cn('relative min-h-8 min-w-8 w-full rounded-md overflow-hidden', containerProps?.className)}
81
74
  >
82
75
  <audio ref={ref} src={url} id={audioId} controls {...props} />
83
- <Spinner loading={uploading} layout="overlay" />
76
+ <Spinner loading={pending} layout="overlay" />
84
77
  </div>
85
78
  );
86
79
  }
@@ -88,7 +81,7 @@ function AttachmentAudioBase(
88
81
  const AttachmentAudio = forwardRef<HTMLAudioElement, AttachmentAudioProps>(AttachmentAudioBase);
89
82
 
90
83
  function AttachmentVideoBase(
91
- { uid, url, maxSize = 360, uploading = false, onReady = () => {}, containerProps, ...props }: AttachmentVideoProps,
84
+ { uid, url, maxSize = 360, pending = false, onReady = () => {}, containerProps, ...props }: AttachmentVideoProps,
92
85
  ref: React.ForwardedRef<HTMLVideoElement>
93
86
  ) {
94
87
  const videoId = `attachment_video_${uid || getRandomString()}`;
@@ -113,7 +106,7 @@ function AttachmentVideoBase(
113
106
  }
114
107
  };
115
108
 
116
- useImperativeHandle(ref, () => playerRef.current!, []);
109
+ useImperativeHandle(ref, () => playerRef.current || ({} as HTMLVideoElement), []);
117
110
 
118
111
  return (
119
112
  <div
@@ -131,7 +124,7 @@ function AttachmentVideoBase(
131
124
  onCanPlay={handleCanPlay}
132
125
  className={cn('size-full', props?.className)}
133
126
  />
134
- <Spinner loading={uploading} layout="overlay" />
127
+ <Spinner loading={pending} layout="overlay" />
135
128
  </div>
136
129
  );
137
130
  }
@@ -141,7 +134,7 @@ const AttachmentVideo = forwardRef<HTMLVideoElement, AttachmentVideoProps>(Attac
141
134
  AttachmentVideo.displayName = 'AttachmentVideo';
142
135
 
143
136
  function AttachmentImageBase(
144
- { uid, url, uploading = false, onReady = () => {}, linkProps, ...props }: AttachmentImageProps,
137
+ { uid, url, pending = false, onReady = () => {}, linkProps, ...props }: AttachmentImageProps,
145
138
  ref: React.ForwardedRef<HTMLImageElement>
146
139
  ) {
147
140
  const imageId = `attachment_image_${uid || getRandomString()}`;
@@ -151,7 +144,7 @@ function AttachmentImageBase(
151
144
  };
152
145
 
153
146
  return (
154
- <AttachmentLink href={url} uploading={uploading} {...linkProps}>
147
+ <AttachmentLink href={url} pending={pending} {...linkProps}>
155
148
  <img
156
149
  ref={ref}
157
150
  src={url}
@@ -159,7 +152,7 @@ function AttachmentImageBase(
159
152
  alt="attachment"
160
153
  {...props}
161
154
  className={cn(
162
- 'rounded-md object-cover min-h-12 min-w-12 max-h-[360px] group-hover:scale-103 transition-transform duration-300 ease-out',
155
+ 'rounded-md object-cover min-h-8 min-w-8 max-h-[360px] group-hover:scale-102 transition-transform duration-300 ease-out',
163
156
  props?.className
164
157
  )}
165
158
  onLoad={handleLoad}
@@ -172,28 +165,28 @@ const AttachmentImage = forwardRef<HTMLImageElement, AttachmentImageProps>(Attac
172
165
 
173
166
  AttachmentImage.displayName = 'AttachmentImage';
174
167
 
175
- function AttachmentFile({ url, name, uploading = false, iconElement, linkProps, ...props }: AttachmentFileProps) {
168
+ function AttachmentFile({ url, name, pending = false, iconElement, linkProps, ...props }: AttachmentFileProps) {
176
169
  const fileId = `attachment_file_${props.id || getRandomString()}`;
177
170
 
178
171
  return (
179
172
  <AttachmentLink
180
173
  href={url}
181
- uploading={uploading}
174
+ pending={pending}
182
175
  {...linkProps}
183
- className={cn('flex-row gap-2 px-2', linkProps?.className)}
176
+ className={cn('flex-row gap-1.5 p-2 hover:shadow', linkProps?.className)}
184
177
  >
185
178
  {iconElement || (
186
179
  <File
187
180
  id={fileId}
188
181
  {...props}
189
182
  className={cn(
190
- 'size-6 shrink-0 text-foreground/85 group-hover:text-foreground duration-300 ease-out',
183
+ 'size-5 shrink-0 text-foreground/80 group-hover:text-foreground duration-300 ease-out',
191
184
  props?.className
192
185
  )}
193
186
  />
194
187
  )}
195
188
  {name && (
196
- <span className="font-medium line-clamp-1 break-all text-foreground/85 group-hover:text-foreground duration-300 ease-out">
189
+ <span className="font-medium line-clamp-1 break-all text-foreground/80 group-hover:text-foreground duration-300 ease-out">
197
190
  {name}
198
191
  </span>
199
192
  )}
@@ -205,7 +198,7 @@ AttachmentFile.displayName = 'AttachmentFile';
205
198
 
206
199
  function AttachmentFailed({
207
200
  name = 'Unknown file',
208
- uploading = false,
201
+ pending = false,
209
202
  iconElement,
210
203
  containerProps,
211
204
  ...props
@@ -216,7 +209,7 @@ function AttachmentFailed({
216
209
  <div
217
210
  {...containerProps}
218
211
  className={cn(
219
- 'relative min-h-12 min-w-12 w-full flex flex-row items-center justify-center gap-2 px-2 bg-red-600/10 rounded-md overflow-hidden',
212
+ 'relative min-h-8 min-w-8 w-full flex flex-row items-center justify-center gap-2 px-2 bg-red-600/10 rounded-md overflow-hidden',
220
213
  containerProps?.className
221
214
  )}
222
215
  >
@@ -224,14 +217,20 @@ function AttachmentFailed({
224
217
  <FileXCorner id={failedId} {...props} className={cn('size-6 shrink-0 text-red-600', props?.className)} />
225
218
  )}
226
219
  <span className="font-medium line-clamp-1 break-all text-red-600">{name}</span>
227
- <Spinner loading={uploading} layout="overlay" />
220
+ <Spinner loading={pending} layout="overlay" />
228
221
  </div>
229
222
  );
230
223
  }
231
224
 
232
225
  AttachmentFailed.displayName = 'AttachmentFailed';
233
226
 
234
- function AttachmentBase({ mimeType, ...props }: AttachmentProps) {
227
+ function AttachmentBase({
228
+ mimeType,
229
+ onReady = () => {},
230
+ containerProps = {},
231
+ linkProps = {},
232
+ ...props
233
+ }: AttachmentProps) {
235
234
  const [type = ''] = mimeType?.split('/') || [];
236
235
 
237
236
  if (!props.url) {
@@ -240,18 +239,20 @@ function AttachmentBase({ mimeType, ...props }: AttachmentProps) {
240
239
 
241
240
  switch (type) {
242
241
  case 'image':
243
- return <AttachmentImage {...props} />;
242
+ return <AttachmentImage onReady={onReady} linkProps={linkProps} {...props} />;
244
243
  case 'video':
245
- return <AttachmentVideo {...props} />;
244
+ return <AttachmentVideo onReady={onReady} containerProps={containerProps} {...props} />;
246
245
  case 'audio':
247
- return <AttachmentAudio {...props} />;
246
+ return <AttachmentAudio containerProps={containerProps} {...props} />;
248
247
  default:
249
- return <AttachmentFile name={mimeType} {...props} />;
248
+ return <AttachmentFile name={mimeType} containerProps={containerProps} {...props} />;
250
249
  }
251
250
  }
252
251
 
253
252
  const Attachment = memo(AttachmentBase);
254
253
 
254
+ Attachment.displayName = 'Attachment';
255
+
255
256
  export {
256
257
  Attachment,
257
258
  AttachmentLink,
@@ -7,6 +7,7 @@ import { cn } from './utils';
7
7
  interface AvatarProps extends AvatarPrimitive.AvatarProps {
8
8
  src?: string | undefined;
9
9
  name?: string | undefined;
10
+ fallbackIconElement?: React.ReactNode | undefined;
10
11
  online?: boolean | undefined;
11
12
  presence?: PresenceStatus;
12
13
  onlineProps?: React.ComponentProps<'div'>;
@@ -26,6 +27,7 @@ function AvatarBase(
26
27
  {
27
28
  src,
28
29
  name = 'NA',
30
+ fallbackIconElement,
29
31
  online,
30
32
  presence,
31
33
  className,
@@ -50,7 +52,7 @@ function AvatarBase(
50
52
  {...fallbackProps}
51
53
  className={cn('bg-muted size-full rounded-full flex items-center justify-center', fallbackProps?.className)}
52
54
  >
53
- {initials}
55
+ {fallbackIconElement || initials}
54
56
  </AvatarPrimitive.Fallback>
55
57
  {online && (
56
58
  <div
@@ -46,7 +46,7 @@ function ButtonBase(
46
46
  <Comp
47
47
  ref={ref}
48
48
  {...props}
49
- className={cn(buttonVariants({ variant, size, className }), 'transition-all ease-in-out duration-300')}
49
+ className={cn(buttonVariants({ variant, size, className }), 'transition-all ease-out duration-300')}
50
50
  />
51
51
  );
52
52
  }
@@ -0,0 +1,176 @@
1
+ import type React from 'react';
2
+ import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useRef } from 'react';
3
+ import { useInView } from 'react-intersection-observer';
4
+ import { Avatar, type AvatarProps } from './avatar';
5
+ import { FormattedDate, type FormattedDateProps } from './formatted-date';
6
+ import { StatusSent, type StatusSentProps } from './status-sent';
7
+ import { cn } from './utils';
8
+
9
+ interface ChatBubbleProps extends React.ComponentProps<'div'> {
10
+ onView?: () => void;
11
+ isLast?: boolean;
12
+ date?: FormattedDateProps['date'];
13
+ language?: FormattedDateProps['language'];
14
+ formattedDateProps?: FormattedDateProps;
15
+ }
16
+
17
+ interface ChatBubbleMessageProps extends ChatBubbleProps {
18
+ fromMe: boolean;
19
+ sameSenderAbove: boolean;
20
+ title?: string;
21
+ senderName?: string;
22
+ senderAvatar?: AvatarProps['src'];
23
+ statusSent?: StatusSentProps['status'];
24
+ avatarProps?: AvatarProps;
25
+ bubbleProps?: React.ComponentProps<'div'>;
26
+ titleProps?: React.ComponentProps<'span'>;
27
+ statusSentProps?: StatusSentProps;
28
+ }
29
+
30
+ interface ChatBubbleInfoProps extends ChatBubbleProps {
31
+ info?: string | undefined;
32
+ iconElement?: React.ReactNode;
33
+ infoProps?: React.ComponentProps<'span'>;
34
+ }
35
+
36
+ function ChatBubbleBase(
37
+ { onView = () => {}, isLast, children, ...props }: ChatBubbleProps,
38
+ ref: React.ForwardedRef<HTMLDivElement>
39
+ ) {
40
+ const [setRef, inView] = useInView();
41
+ const messageRef = useRef<HTMLDivElement>(null);
42
+ const setRefs = useCallback(
43
+ (node: HTMLDivElement) => {
44
+ messageRef.current = node;
45
+ setRef(node);
46
+ },
47
+ [setRef]
48
+ );
49
+
50
+ useEffect(() => {
51
+ if (inView) {
52
+ onView();
53
+ }
54
+ }, [inView, onView]);
55
+
56
+ useImperativeHandle(ref, () => messageRef.current || ({} as HTMLDivElement), []);
57
+
58
+ return (
59
+ <div ref={setRefs} {...props} className={cn('mt-2', isLast && 'mb-2', inView && 'view', props?.className)}>
60
+ {children}
61
+ </div>
62
+ );
63
+ }
64
+
65
+ const ChatBubble = forwardRef<HTMLDivElement, ChatBubbleProps>(ChatBubbleBase);
66
+
67
+ function ChatBubbleMessageBase(
68
+ {
69
+ fromMe,
70
+ sameSenderAbove,
71
+ title,
72
+ senderName,
73
+ senderAvatar,
74
+ date = new Date(),
75
+ language = 'en',
76
+ statusSent,
77
+ avatarProps,
78
+ bubbleProps,
79
+ titleProps,
80
+ formattedDateProps,
81
+ statusSentProps,
82
+ children,
83
+ ...props
84
+ }: ChatBubbleMessageProps,
85
+ ref: React.ForwardedRef<HTMLDivElement>
86
+ ) {
87
+ const hasAvatar = Boolean((avatarProps || senderAvatar) && !sameSenderAbove);
88
+ const hasAvatarMargin = Boolean((avatarProps || senderAvatar) && sameSenderAbove);
89
+
90
+ return (
91
+ <ChatBubble
92
+ ref={ref}
93
+ {...props}
94
+ className={cn(
95
+ `flex relative text-left whitespace-pre-wrap`,
96
+ fromMe ? 'self-end flex-row-reverse ml-12' : `self-start mr-12`,
97
+ sameSenderAbove && 'mt-1'
98
+ )}
99
+ >
100
+ {hasAvatar && (
101
+ <Avatar
102
+ name={senderName}
103
+ src={senderAvatar}
104
+ imageProps={{ className: 'bg-blue-200' }}
105
+ fallbackProps={{ className: 'bg-blue-200' }}
106
+ {...avatarProps}
107
+ className={cn('mt-1 mr-1', avatarProps?.className)}
108
+ />
109
+ )}
110
+
111
+ <div
112
+ className={cn(
113
+ 'relative flex flex-col min-w-42 max-w-120 rounded-xl px-2 pt-2 pb-6 shadow-sm',
114
+ fromMe ? 'bg-gray-200' : 'bg-blue-200',
115
+ hasAvatarMargin && 'ml-9',
116
+ bubbleProps?.className
117
+ )}
118
+ >
119
+ {(title || senderName) && (
120
+ <span
121
+ {...titleProps}
122
+ className={cn(
123
+ 'font-semibold',
124
+ title && 'mb-1 py-1.5 text-xs text-muted-foreground italic border-b',
125
+ titleProps?.className
126
+ )}
127
+ >
128
+ {title || senderName}
129
+ </span>
130
+ )}
131
+ {children}
132
+ <div className="absolute bottom-1 right-2 flex items-center gap-1 italic">
133
+ <FormattedDate date={date} language={language} distanceToNow {...formattedDateProps} />
134
+ <StatusSent status={statusSent} {...statusSentProps} />
135
+ </div>
136
+ </div>
137
+ </ChatBubble>
138
+ );
139
+ }
140
+
141
+ const ChatBubbleMessage = memo(forwardRef<HTMLDivElement, ChatBubbleMessageProps>(ChatBubbleMessageBase));
142
+
143
+ ChatBubbleMessage.displayName = 'ChatBubbleMessage';
144
+
145
+ function ChatBubbleInfoBase(
146
+ {
147
+ info = '',
148
+ iconElement,
149
+ date = new Date(),
150
+ language = 'en',
151
+ infoProps,
152
+ formattedDateProps,
153
+ ...props
154
+ }: ChatBubbleInfoProps,
155
+ ref: React.ForwardedRef<HTMLDivElement>
156
+ ) {
157
+ return (
158
+ <ChatBubble
159
+ ref={ref}
160
+ {...props}
161
+ className={cn('flex items-center justify-center gap-2 rounded-full w-fit bg-ring/20 mx-auto px-3 py-1.5 mt-2')}
162
+ >
163
+ {iconElement}
164
+ <span {...infoProps} className={cn('text-sm mb-px', infoProps?.className)}>
165
+ {info}
166
+ </span>
167
+ <FormattedDate date={date} language={language} distanceToNow {...formattedDateProps} />
168
+ </ChatBubble>
169
+ );
170
+ }
171
+
172
+ const ChatBubbleInfo = memo(forwardRef<HTMLDivElement, ChatBubbleInfoProps>(ChatBubbleInfoBase));
173
+
174
+ ChatBubbleInfo.displayName = 'ChatBubbleInfo';
175
+
176
+ export { ChatBubbleMessage, ChatBubbleInfo, type ChatBubbleMessageProps, type ChatBubbleInfoProps };