@connectycube/react-ui-kit 0.1.0 → 0.1.2

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 (79) hide show
  1. package/README.md +2 -2
  2. package/gen/components/alert-dialog.jsx +2 -2
  3. package/gen/components/attachment.jsx +10 -11
  4. package/gen/components/avatar.jsx +2 -2
  5. package/gen/components/badge.jsx +2 -2
  6. package/gen/components/button.jsx +4 -3
  7. package/gen/components/chat-bubble.jsx +8 -8
  8. package/gen/components/chat-input.jsx +10 -10
  9. package/gen/components/chat-list.jsx +13 -13
  10. package/gen/components/checkbox.jsx +2 -2
  11. package/gen/components/dialog-item.jsx +2 -2
  12. package/gen/components/dialogs-list.jsx +4 -4
  13. package/gen/components/dismiss-layer.jsx +7 -7
  14. package/gen/components/file-picker.jsx +4 -4
  15. package/gen/components/formatted-date.jsx +2 -2
  16. package/gen/components/input.jsx +2 -2
  17. package/gen/components/label.jsx +2 -2
  18. package/gen/components/link-preview.jsx +7 -7
  19. package/gen/components/linkify-text.jsx +5 -5
  20. package/gen/components/placeholder-text.jsx +1 -2
  21. package/gen/components/presence.jsx +1 -0
  22. package/gen/components/quick-actions.jsx +2 -2
  23. package/gen/components/search.jsx +3 -3
  24. package/gen/components/spinner.jsx +3 -2
  25. package/gen/components/status-call.jsx +1 -0
  26. package/gen/components/status-indicator.jsx +2 -2
  27. package/gen/components/status-sent.jsx +1 -0
  28. package/gen/components/stream-view.jsx +16 -16
  29. package/package.json +14 -14
  30. package/src/components/alert-dialog.tsx +2 -3
  31. package/src/components/attachment.tsx +12 -13
  32. package/src/components/avatar.tsx +2 -3
  33. package/src/components/badge.tsx +2 -3
  34. package/src/components/button.tsx +4 -4
  35. package/src/components/chat-bubble.tsx +9 -10
  36. package/src/components/chat-input.tsx +17 -13
  37. package/src/components/chat-list.tsx +31 -24
  38. package/src/components/checkbox.tsx +2 -3
  39. package/src/components/dialog-item.tsx +2 -3
  40. package/src/components/dialogs-list.tsx +4 -5
  41. package/src/components/dismiss-layer.tsx +7 -8
  42. package/src/components/file-picker.tsx +4 -5
  43. package/src/components/formatted-date.tsx +4 -3
  44. package/src/components/input.tsx +2 -3
  45. package/src/components/label.tsx +2 -3
  46. package/src/components/link-preview.tsx +16 -26
  47. package/src/components/linkify-text.tsx +5 -6
  48. package/src/components/placeholder-text.tsx +1 -2
  49. package/src/components/presence.tsx +1 -1
  50. package/src/components/quick-actions.tsx +2 -3
  51. package/src/components/search.tsx +3 -4
  52. package/src/components/spinner.tsx +3 -2
  53. package/src/components/status-call.tsx +1 -0
  54. package/src/components/status-indicator.tsx +2 -3
  55. package/src/components/status-sent.tsx +1 -0
  56. package/src/components/stream-view.tsx +18 -17
  57. package/src/components/connectycube-ui/attachment.tsx +0 -269
  58. package/src/components/connectycube-ui/avatar.jsx +0 -54
  59. package/src/components/connectycube-ui/avatar.tsx +0 -77
  60. package/src/components/connectycube-ui/badge.jsx +0 -45
  61. package/src/components/connectycube-ui/badge.tsx +0 -42
  62. package/src/components/connectycube-ui/chat-input.tsx +0 -174
  63. package/src/components/connectycube-ui/chat-message.tsx +0 -138
  64. package/src/components/connectycube-ui/dialog-item.jsx +0 -149
  65. package/src/components/connectycube-ui/dialog-item.tsx +0 -188
  66. package/src/components/connectycube-ui/file-picker.jsx +0 -200
  67. package/src/components/connectycube-ui/file-picker.tsx +0 -231
  68. package/src/components/connectycube-ui/formatted-date.jsx +0 -57
  69. package/src/components/connectycube-ui/formatted-date.tsx +0 -57
  70. package/src/components/connectycube-ui/label.jsx +0 -22
  71. package/src/components/connectycube-ui/label.tsx +0 -23
  72. package/src/components/connectycube-ui/link-preview.tsx +0 -149
  73. package/src/components/connectycube-ui/linkify-text.tsx +0 -40
  74. package/src/components/connectycube-ui/presence.jsx +0 -81
  75. package/src/components/connectycube-ui/presence.tsx +0 -96
  76. package/src/components/connectycube-ui/status-sent.jsx +0 -21
  77. package/src/components/connectycube-ui/status-sent.tsx +0 -25
  78. package/src/components/connectycube-ui/utils.js +0 -10
  79. package/src/components/connectycube-ui/utils.ts +0 -10
@@ -1,269 +0,0 @@
1
- import type React from 'react';
2
- import { forwardRef, memo, useImperativeHandle, useRef, useState } from 'react';
3
- import { File, FileXCorner, type LucideProps } from 'lucide-react';
4
- import { Spinner } from './spinner';
5
- import { cn, getRandomString } from './utils';
6
-
7
- interface AttachmentProps {
8
- uid?: string;
9
- url?: string;
10
- mimeType?: string;
11
- pending?: boolean;
12
- onReady?: (skipOnce?: boolean) => void;
13
- linkProps?: AttachmentLinkProps;
14
- containerProps?: React.ComponentProps<'div'>;
15
- }
16
-
17
- interface AttachmentLinkProps
18
- extends React.ComponentProps<'a'>, Omit<AttachmentProps, 'containerProps' & 'mimeType' & 'onReady'> {
19
- children?: React.ReactNode;
20
- }
21
-
22
- interface AttachmentImageProps
23
- extends React.ComponentProps<'img'>, Omit<AttachmentProps, 'containerProps' & 'mimeType'> {}
24
-
25
- interface AttachmentAudioProps
26
- extends React.ComponentProps<'audio'>, Omit<AttachmentProps, 'linkProps' & 'mimeType' & 'onReady'> {}
27
-
28
- interface AttachmentVideoProps extends React.ComponentProps<'video'>, Omit<AttachmentProps, 'linkProps' & 'mimeType'> {
29
- maxSize?: number;
30
- }
31
-
32
- interface AttachmentFileProps extends LucideProps, Omit<AttachmentProps, 'containerProps' & 'mimeType'> {
33
- name?: string | undefined;
34
- iconElement?: React.ReactNode;
35
- }
36
-
37
- interface AttachmentFailedProps extends LucideProps, Omit<AttachmentProps, 'linkProps' & 'mimeType'> {
38
- name?: string | undefined;
39
- iconElement?: React.ReactNode;
40
- }
41
-
42
- function AttachmentLinkBase(
43
- { url, pending = false, children, ...props }: AttachmentLinkProps,
44
- ref: React.ForwardedRef<HTMLAnchorElement>
45
- ) {
46
- return (
47
- <a
48
- ref={ref}
49
- target="_blank"
50
- rel="noopener noreferrer"
51
- {...props}
52
- href={url}
53
- className={cn(
54
- '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',
55
- props?.className
56
- )}
57
- >
58
- {children}
59
- <Spinner loading={pending} layout="overlay" />
60
- </a>
61
- );
62
- }
63
-
64
- const AttachmentLink = forwardRef<HTMLAnchorElement, AttachmentLinkProps>(AttachmentLinkBase);
65
-
66
- AttachmentLink.displayName = 'AttachmentLink';
67
-
68
- function AttachmentAudioBase(
69
- { uid, url, pending = false, containerProps, ...props }: AttachmentAudioProps,
70
- ref: React.ForwardedRef<HTMLAudioElement>
71
- ) {
72
- const audioId = `attachment_audio_${uid || getRandomString()}`;
73
-
74
- return (
75
- <div
76
- {...containerProps}
77
- className={cn('relative min-h-8 min-w-8 w-full rounded-md overflow-hidden', containerProps?.className)}
78
- >
79
- <audio ref={ref} src={url} id={audioId} controls {...props} />
80
- <Spinner loading={pending} layout="overlay" />
81
- </div>
82
- );
83
- }
84
-
85
- const AttachmentAudio = forwardRef<HTMLAudioElement, AttachmentAudioProps>(AttachmentAudioBase);
86
-
87
- function AttachmentVideoBase(
88
- { uid, url, maxSize = 360, pending = false, onReady = () => {}, containerProps, ...props }: AttachmentVideoProps,
89
- ref: React.ForwardedRef<HTMLVideoElement>
90
- ) {
91
- const videoId = `attachment_video_${uid || getRandomString()}`;
92
- const videoMaxSize = `${maxSize}px`;
93
- const playerRef = useRef<HTMLVideoElement>(null);
94
- const [style, setStyle] = useState<React.CSSProperties>({
95
- maxHeight: videoMaxSize,
96
- maxWidth: videoMaxSize,
97
- });
98
- const handleCanPlay = (event: React.SyntheticEvent<HTMLVideoElement, Event>) => {
99
- const player = playerRef.current;
100
-
101
- if (player) {
102
- const { videoWidth, videoHeight } = player;
103
- const ratio = videoWidth / videoHeight || 1;
104
- const height = ratio < 1 ? videoMaxSize : `${Math.round(maxSize / ratio)}px`;
105
- const width = ratio < 1 ? `${Math.round(maxSize * ratio)}px` : videoMaxSize;
106
-
107
- setStyle({ height, width, maxHeight: height, maxWidth: width });
108
- props.onCanPlay?.(event);
109
- onReady();
110
- }
111
- };
112
-
113
- useImperativeHandle(ref, () => playerRef.current!, []);
114
-
115
- return (
116
- <div
117
- {...containerProps}
118
- className={cn('relative min-h-20 w-full rounded-md overflow-hidden', containerProps?.className)}
119
- >
120
- <video
121
- id={videoId}
122
- ref={playerRef}
123
- src={url}
124
- controls
125
- preload="metadata"
126
- style={style}
127
- {...props}
128
- onCanPlay={handleCanPlay}
129
- className={cn('size-full', props?.className)}
130
- />
131
- <Spinner loading={pending} layout="overlay" />
132
- </div>
133
- );
134
- }
135
-
136
- const AttachmentVideo = forwardRef<HTMLVideoElement, AttachmentVideoProps>(AttachmentVideoBase);
137
-
138
- AttachmentVideo.displayName = 'AttachmentVideo';
139
-
140
- function AttachmentImageBase(
141
- { uid, url, pending = false, onReady = () => {}, linkProps, ...props }: AttachmentImageProps,
142
- ref: React.ForwardedRef<HTMLImageElement>
143
- ) {
144
- const imageId = `attachment_image_${uid || getRandomString()}`;
145
- const handleLoad = (event: React.SyntheticEvent<HTMLImageElement, Event>) => {
146
- props.onLoad?.(event);
147
- onReady();
148
- };
149
-
150
- return (
151
- <AttachmentLink href={url} pending={pending} {...linkProps}>
152
- <img
153
- ref={ref}
154
- src={url}
155
- id={imageId}
156
- alt="attachment"
157
- {...props}
158
- className={cn(
159
- 'rounded-md object-cover min-h-8 min-w-8 max-h-[360px] group-hover:scale-102 transition-transform duration-300 ease-out',
160
- props?.className
161
- )}
162
- onLoad={handleLoad}
163
- />
164
- </AttachmentLink>
165
- );
166
- }
167
-
168
- const AttachmentImage = forwardRef<HTMLImageElement, AttachmentImageProps>(AttachmentImageBase);
169
-
170
- AttachmentImage.displayName = 'AttachmentImage';
171
-
172
- function AttachmentFile({ url, name, pending = false, iconElement, linkProps, ...props }: AttachmentFileProps) {
173
- const fileId = `attachment_file_${props.id || getRandomString()}`;
174
-
175
- return (
176
- <AttachmentLink
177
- href={url}
178
- pending={pending}
179
- {...linkProps}
180
- className={cn('flex-row gap-1.5 p-2 hover:shadow', linkProps?.className)}
181
- >
182
- {iconElement || (
183
- <File
184
- id={fileId}
185
- {...props}
186
- className={cn(
187
- 'size-5 shrink-0 text-foreground/80 group-hover:text-foreground duration-300 ease-out',
188
- props?.className
189
- )}
190
- />
191
- )}
192
- {name && (
193
- <span className="font-medium line-clamp-1 break-all text-foreground/80 group-hover:text-foreground duration-300 ease-out">
194
- {name}
195
- </span>
196
- )}
197
- </AttachmentLink>
198
- );
199
- }
200
-
201
- AttachmentFile.displayName = 'AttachmentFile';
202
-
203
- function AttachmentFailed({
204
- name = 'Unknown file',
205
- pending = false,
206
- iconElement,
207
- containerProps,
208
- ...props
209
- }: AttachmentFailedProps) {
210
- const failedId = `attachment_failed_${props.id || getRandomString()}`;
211
-
212
- return (
213
- <div
214
- {...containerProps}
215
- className={cn(
216
- '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',
217
- containerProps?.className
218
- )}
219
- >
220
- {iconElement || (
221
- <FileXCorner id={failedId} {...props} className={cn('size-6 shrink-0 text-red-600', props?.className)} />
222
- )}
223
- <span className="font-medium line-clamp-1 break-all text-red-600">{name}</span>
224
- <Spinner loading={pending} layout="overlay" />
225
- </div>
226
- );
227
- }
228
-
229
- AttachmentFailed.displayName = 'AttachmentFailed';
230
-
231
- function AttachmentBase({ mimeType, ...props }: AttachmentProps) {
232
- const [type = ''] = mimeType?.split('/') || [];
233
-
234
- if (!props.url) {
235
- return <AttachmentFailed {...props} />;
236
- }
237
-
238
- switch (type) {
239
- case 'image':
240
- return <AttachmentImage {...props} />;
241
- case 'video':
242
- return <AttachmentVideo {...props} />;
243
- case 'audio':
244
- return <AttachmentAudio {...props} />;
245
- default:
246
- return <AttachmentFile name={mimeType} {...props} />;
247
- }
248
- }
249
-
250
- const Attachment = memo(AttachmentBase);
251
-
252
- Attachment.displayName = 'Attachment';
253
-
254
- export {
255
- Attachment,
256
- AttachmentLink,
257
- AttachmentImage,
258
- AttachmentAudio,
259
- AttachmentVideo,
260
- AttachmentFile,
261
- AttachmentFailed,
262
- type AttachmentLinkProps,
263
- type AttachmentImageProps,
264
- type AttachmentAudioProps,
265
- type AttachmentVideoProps,
266
- type AttachmentFileProps,
267
- type AttachmentFailedProps,
268
- type AttachmentProps,
269
- };
@@ -1,54 +0,0 @@
1
- import { memo, forwardRef } from 'react';
2
- import * as AvatarPrimitive from '@radix-ui/react-avatar';
3
- import { PresenceBadge } from './presence';
4
- import { cn } from './utils';
5
-
6
- function getInitialsFromName(name) {
7
- const words = name?.trim().split(/\s+/).filter(Boolean) ?? [];
8
- const result = words.length > 1 ? `${words[0]?.[0]}${words[1]?.[0]}` : (words[0]?.slice(0, 2) ?? 'NA');
9
-
10
- return result.toUpperCase();
11
- }
12
-
13
- function AvatarBase(
14
- { src, name = 'NA', online, presence, className, onlineProps, presenceProps, imageProps, fallbackProps, ...props },
15
- ref
16
- ) {
17
- const initials = getInitialsFromName(name);
18
-
19
- return (
20
- <AvatarPrimitive.Root ref={ref} {...props} className={cn('relative flex size-8 shrink-0 rounded-full', className)}>
21
- <AvatarPrimitive.Image
22
- {...imageProps}
23
- src={src}
24
- className={cn('aspect-square size-full rounded-full overflow-hidden object-cover', imageProps?.className)}
25
- />
26
- <AvatarPrimitive.Fallback
27
- {...fallbackProps}
28
- className={cn('bg-muted size-full rounded-full flex items-center justify-center', fallbackProps?.className)}
29
- >
30
- {initials}
31
- </AvatarPrimitive.Fallback>
32
- {online && (
33
- <div
34
- {...onlineProps}
35
- className={cn(
36
- 'absolute top-0 right-0 rounded-full border-2 bg-green-600 border-green-200 size-3.5',
37
- onlineProps?.className
38
- )}
39
- />
40
- )}
41
- <PresenceBadge
42
- status={presence}
43
- {...presenceProps}
44
- className={cn('absolute bottom-0 right-0', presenceProps?.className)}
45
- />
46
- </AvatarPrimitive.Root>
47
- );
48
- }
49
-
50
- const Avatar = memo(forwardRef(AvatarBase));
51
-
52
- Avatar.displayName = 'Avatar';
53
-
54
- export { Avatar };
@@ -1,77 +0,0 @@
1
- import type React from 'react';
2
- import { memo, forwardRef } from 'react';
3
- import * as AvatarPrimitive from '@radix-ui/react-avatar';
4
- import { PresenceBadge, type PresenceStatus, type PresenceBadgeProps } from './presence';
5
- import { cn } from './utils';
6
-
7
- interface AvatarProps extends AvatarPrimitive.AvatarProps {
8
- src?: string;
9
- name?: string;
10
- online?: boolean;
11
- presence?: PresenceStatus;
12
- onlineProps?: React.ComponentProps<'div'>;
13
- presenceProps?: PresenceBadgeProps;
14
- imageProps?: AvatarPrimitive.AvatarImageProps;
15
- fallbackProps?: AvatarPrimitive.AvatarFallbackProps;
16
- }
17
-
18
- function getInitialsFromName(name?: string): string {
19
- const words = name?.trim().split(/\s+/).filter(Boolean) ?? [];
20
- const result = words.length > 1 ? `${words[0]?.[0]}${words[1]?.[0]}` : (words[0]?.slice(0, 2) ?? 'NA');
21
-
22
- return result.toUpperCase();
23
- }
24
-
25
- function AvatarBase(
26
- {
27
- src,
28
- name = 'NA',
29
- online,
30
- presence,
31
- className,
32
- onlineProps,
33
- presenceProps,
34
- imageProps,
35
- fallbackProps,
36
- ...props
37
- }: AvatarProps,
38
- ref: React.ForwardedRef<HTMLDivElement>
39
- ) {
40
- const initials = getInitialsFromName(name);
41
-
42
- return (
43
- <AvatarPrimitive.Root ref={ref} {...props} className={cn('relative flex size-8 shrink-0 rounded-full', className)}>
44
- <AvatarPrimitive.Image
45
- {...imageProps}
46
- src={src}
47
- className={cn('aspect-square size-full rounded-full overflow-hidden object-cover', imageProps?.className)}
48
- />
49
- <AvatarPrimitive.Fallback
50
- {...fallbackProps}
51
- className={cn('bg-muted size-full rounded-full flex items-center justify-center', fallbackProps?.className)}
52
- >
53
- {initials}
54
- </AvatarPrimitive.Fallback>
55
- {online && (
56
- <div
57
- {...onlineProps}
58
- className={cn(
59
- 'absolute top-0 right-0 rounded-full border-2 bg-green-600 border-green-200 size-3.5',
60
- onlineProps?.className
61
- )}
62
- />
63
- )}
64
- <PresenceBadge
65
- status={presence}
66
- {...presenceProps}
67
- className={cn('absolute bottom-0 right-0', presenceProps?.className)}
68
- />
69
- </AvatarPrimitive.Root>
70
- );
71
- }
72
-
73
- const Avatar = memo(forwardRef<HTMLDivElement, AvatarProps>(AvatarBase));
74
-
75
- Avatar.displayName = 'Avatar';
76
-
77
- export { Avatar, type AvatarProps };
@@ -1,45 +0,0 @@
1
- import { forwardRef } from 'react';
2
- import { Slot } from '@radix-ui/react-slot';
3
- import { cva } from 'class-variance-authority';
4
- import { cn } from './utils';
5
-
6
- const badgeVariants = cva(
7
- 'inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden',
8
- {
9
- variants: {
10
- variant: {
11
- default: 'border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90',
12
- secondary: 'border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90',
13
- destructive:
14
- 'border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
15
- outline: 'text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground',
16
- },
17
- },
18
- defaultVariants: {
19
- variant: 'default',
20
- },
21
- }
22
- );
23
-
24
- function BadgeBase({ className, variant, asChild = false, ...props }, ref) {
25
- const Comp = asChild ? Slot : 'span';
26
-
27
- return (
28
- <Comp
29
- ref={ref}
30
- {...props}
31
- className={cn(
32
- badgeVariants({
33
- variant,
34
- }),
35
- className
36
- )}
37
- />
38
- );
39
- }
40
-
41
- const Badge = forwardRef(BadgeBase);
42
-
43
- Badge.displayName = 'Badge';
44
-
45
- export { Badge };
@@ -1,42 +0,0 @@
1
- import type React from 'react';
2
- import { forwardRef } from 'react';
3
- import { Slot } from '@radix-ui/react-slot';
4
- import { cva, type VariantProps } from 'class-variance-authority';
5
- import { cn } from './utils';
6
-
7
- interface BadgeProps extends React.HTMLAttributes<HTMLElement>, VariantProps<typeof badgeVariants> {
8
- asChild?: boolean;
9
- }
10
-
11
- const badgeVariants = cva(
12
- 'inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden',
13
- {
14
- variants: {
15
- variant: {
16
- default: 'border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90',
17
- secondary: 'border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90',
18
- destructive:
19
- 'border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
20
- outline: 'text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground',
21
- },
22
- },
23
- defaultVariants: {
24
- variant: 'default',
25
- },
26
- }
27
- );
28
-
29
- function BadgeBase(
30
- { className, variant, asChild = false, ...props }: BadgeProps,
31
- ref?: React.ForwardedRef<HTMLElement>
32
- ) {
33
- const Comp = asChild ? Slot : 'span';
34
-
35
- return <Comp ref={ref} {...props} className={cn(badgeVariants({ variant }), className)} />;
36
- }
37
-
38
- const Badge = forwardRef<HTMLElement, BadgeProps>(BadgeBase);
39
-
40
- Badge.displayName = 'Badge';
41
-
42
- export { Badge, type BadgeProps };
@@ -1,174 +0,0 @@
1
- import type React from 'react';
2
- import { ChangeEvent, forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react';
3
- import TextareaAutosize, { type TextareaAutosizeProps, type TextareaHeightChangeMeta } from 'react-textarea-autosize';
4
- import { SendHorizontal, type LucideProps } from 'lucide-react';
5
- import { Label, type LabelProps } from './label';
6
- import { cn } from './utils';
7
-
8
- interface ChatInputSendProps extends LabelProps {
9
- onSend: () => void;
10
- iconElement?: React.ReactNode;
11
- iconProps?: LucideProps;
12
- }
13
-
14
- interface ChatInputProps extends TextareaAutosizeProps, React.ComponentProps<'textarea'> {
15
- key?: string;
16
- children?: React.ReactNode;
17
- pending?: boolean;
18
- draft?: string;
19
- onSend?: (value?: string) => void;
20
- onDraft?: (value?: string) => void;
21
- onTyping?: (typing?: boolean) => void;
22
- onHeightGrow?: (data: { height: number; shift: number }) => void;
23
- hideChatInputSend?: boolean;
24
- chatInputSendProps?: ChatInputSendProps;
25
- containerProps?: React.ComponentProps<'div'>;
26
- }
27
-
28
- function ChatInputSendBase(
29
- { onSend = () => {}, iconElement, iconProps, ...props }: ChatInputSendProps,
30
- ref: React.ForwardedRef<HTMLLabelElement>
31
- ) {
32
- return (
33
- <Label
34
- ref={ref}
35
- {...props}
36
- onClick={onSend}
37
- className={cn('group rounded-full py-2 pl-2.5 pr-1.5 cursor-pointer bg-ring/90 hover:bg-ring', props?.className)}
38
- >
39
- {iconElement || (
40
- <SendHorizontal
41
- {...iconProps}
42
- className={cn(
43
- 'size-7 text-background group-hover:scale-110 transition-all duration-200 ease-out',
44
- iconProps?.className
45
- )}
46
- />
47
- )}
48
- </Label>
49
- );
50
- }
51
-
52
- const ChatInputSend = forwardRef<HTMLLabelElement, ChatInputSendProps>(ChatInputSendBase);
53
-
54
- ChatInputSend.displayName = 'ChatInputSend';
55
-
56
- function ChatInputBase(
57
- {
58
- key,
59
- pending = false,
60
- draft,
61
- onSend = () => {},
62
- onDraft = () => {},
63
- onTyping = () => {},
64
- onHeightGrow = () => {},
65
- hideChatInputSend = false,
66
- chatInputSendProps,
67
- containerProps,
68
- children,
69
- ...props
70
- }: ChatInputProps,
71
- ref: React.ForwardedRef<HTMLTextAreaElement>
72
- ) {
73
- const [value, setValue] = useState<string>();
74
- const textareaRef = useRef<HTMLTextAreaElement>(null);
75
- const textareaHeightRef = useRef<number>(0);
76
- const typingRef = useRef<boolean>(false);
77
- const typingTimeoutRef = useRef<NodeJS.Timeout | undefined>(undefined);
78
- const handleStopTyping = () => {
79
- typingRef.current = false;
80
- onTyping(false);
81
-
82
- if (typingTimeoutRef.current) {
83
- clearTimeout(typingTimeoutRef.current);
84
- typingTimeoutRef.current = undefined;
85
- }
86
- };
87
- const handleOnChange = (event: ChangeEvent<HTMLTextAreaElement>) => {
88
- const { data } = event.nativeEvent as InputEvent;
89
-
90
- setValue(event.target.value);
91
-
92
- if (!typingRef.current && typeof data === 'string' && data.length > 0) {
93
- typingRef.current = true;
94
- onTyping(true);
95
- }
96
-
97
- clearTimeout(typingTimeoutRef.current);
98
- typingTimeoutRef.current = setTimeout(handleStopTyping, 5000);
99
- };
100
- const handleOnSend = async () => {
101
- handleStopTyping();
102
-
103
- if (typeof value === 'string' && value.length > 0) {
104
- onSend(value.trim());
105
- setValue('');
106
- textareaRef.current?.focus();
107
- }
108
- };
109
- const handleOnHeightChange = (height: number, meta: TextareaHeightChangeMeta) => {
110
- if (!height && !meta) return;
111
-
112
- if (height !== textareaHeightRef.current) {
113
- const shift = height > textareaHeightRef.current ? meta.rowHeight : -meta.rowHeight;
114
-
115
- onHeightGrow({ height, shift });
116
- }
117
-
118
- textareaHeightRef.current = height;
119
- };
120
- const handleOnKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
121
- if (event.key === 'Enter' && event.shiftKey === false) {
122
- event.preventDefault();
123
- props.onKeyDown?.(event);
124
- handleOnSend();
125
- }
126
- };
127
-
128
- useEffect(() => {
129
- const textarea = textareaRef.current;
130
-
131
- handleStopTyping();
132
- queueMicrotask(() => setValue(draft));
133
- textarea?.focus();
134
-
135
- return () => {
136
- onDraft(textarea?.value);
137
- };
138
- }, [key]);
139
-
140
- useImperativeHandle(ref, () => textareaRef.current!, [textareaRef]);
141
-
142
- return (
143
- <div {...containerProps} className={cn('flex items-end gap-2', containerProps?.className)}>
144
- <TextareaAutosize
145
- key={key}
146
- ref={textareaRef}
147
- autoFocus
148
- minRows={1}
149
- maxRows={8}
150
- {...props}
151
- value={value}
152
- onChange={handleOnChange}
153
- onKeyDown={handleOnKeyDown}
154
- onHeightChange={handleOnHeightChange}
155
- className={cn(
156
- 'file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-11 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
157
- 'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring',
158
- 'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
159
- 'resize-none py-2.5',
160
- pending && 'pointer-events-none bg-muted-foreground border-ring ring-ring/50 animate-pulse',
161
- props?.className
162
- )}
163
- />
164
- {!hideChatInputSend && <ChatInputSend onSend={handleOnSend} {...chatInputSendProps} />}
165
- {children}
166
- </div>
167
- );
168
- }
169
-
170
- const ChatInput = forwardRef<HTMLTextAreaElement, ChatInputProps>(ChatInputBase);
171
-
172
- ChatInput.displayName = 'ChatInput';
173
-
174
- export { ChatInput };