@connectycube/react-ui-kit 0.0.22 → 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 +33 -6
  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,138 +0,0 @@
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 { Attachment, type AttachmentProps } from './attachment';
5
- import { Avatar, type AvatarProps } from './avatar';
6
- import { FormattedDate, type FormattedDateProps } from './formatted-date';
7
- import { LinkifyText, type LinkifyTextProps } from './linkify-text';
8
- import { LinkPreview, type LinkPreviewProps } from './link-preview';
9
- import { StatusSent, type StatusSentProps } from './status-sent';
10
- import { cn } from './utils';
11
-
12
- interface ChatMessageProps extends React.ComponentProps<'div'> {
13
- isLast: boolean;
14
- fromMe: boolean;
15
- sameSenderAbove: boolean;
16
- title?: string;
17
- senderName?: string;
18
- senderAvatar?: string;
19
- attachmentElement?: React.ReactNode;
20
- linkifyTextElement?: React.ReactNode;
21
- linkPreviewElement?: React.ReactNode;
22
- onView?: () => void;
23
- avatarProps?: AvatarProps;
24
- bubbleProps?: React.ComponentProps<'div'>;
25
- titleProps?: React.ComponentProps<'span'>;
26
- formattedDateProps?: FormattedDateProps;
27
- statusSentProps?: StatusSentProps;
28
- attachmentProps?: AttachmentProps;
29
- linkifyTextProps?: LinkifyTextProps;
30
- linkPreviewProps?: LinkPreviewProps;
31
- }
32
-
33
- function ChatMessageBase(
34
- {
35
- isLast,
36
- fromMe,
37
- sameSenderAbove,
38
- title,
39
- senderName,
40
- senderAvatar,
41
- attachmentElement,
42
- linkifyTextElement,
43
- linkPreviewElement,
44
- onView = () => {},
45
- avatarProps,
46
- bubbleProps,
47
- titleProps,
48
- formattedDateProps,
49
- statusSentProps,
50
- attachmentProps,
51
- linkifyTextProps,
52
- linkPreviewProps,
53
- children,
54
- ...props
55
- }: ChatMessageProps,
56
- ref: React.ForwardedRef<HTMLDivElement>
57
- ) {
58
- const [setRef, inView] = useInView();
59
- const messageRef = useRef<HTMLDivElement>(null);
60
- const setRefs = useCallback(
61
- (node: HTMLDivElement) => {
62
- messageRef.current = node;
63
- setRef(node);
64
- },
65
- [setRef]
66
- );
67
- const hasAvatar = Boolean((avatarProps || senderAvatar) && !sameSenderAbove);
68
- const hasAvatarMargin = Boolean((avatarProps || senderAvatar) && sameSenderAbove);
69
- const hasThinMarginTop = hasAvatarMargin || Boolean(fromMe && sameSenderAbove);
70
-
71
- useEffect(() => {
72
- if (inView) {
73
- onView();
74
- }
75
- }, [inView, onView]);
76
-
77
- useImperativeHandle(ref, () => messageRef.current!, []);
78
-
79
- return (
80
- <div
81
- ref={setRefs}
82
- {...props}
83
- className={cn(
84
- `flex relative text-left whitespace-pre-wrap`,
85
- fromMe ? 'self-end flex-row-reverse ml-12' : `self-start mr-12`,
86
- isLast && 'mb-2',
87
- hasThinMarginTop ? 'mt-1' : 'mt-2',
88
- inView && 'view',
89
- props?.className
90
- )}
91
- >
92
- {hasAvatar && (
93
- <Avatar
94
- name={senderName}
95
- src={senderAvatar}
96
- imageProps={{ className: 'bg-ring/30' }}
97
- fallbackProps={{ className: 'bg-ring/30' }}
98
- {...avatarProps}
99
- className={cn('mt-1 mr-1', avatarProps?.className)}
100
- />
101
- )}
102
-
103
- <div
104
- className={cn(
105
- 'relative flex flex-col min-w-42 max-w-120 rounded-xl px-2 pt-2 pb-6',
106
- fromMe ? 'bg-blue-200' : 'bg-gray-200',
107
- hasAvatarMargin && 'ml-9',
108
- bubbleProps?.className
109
- )}
110
- >
111
- {(title || senderName) && (
112
- <span
113
- {...titleProps}
114
- className={cn(
115
- 'font-semibold',
116
- title && 'mb-1 py-1.5 text-xs text-muted-foreground italic border-b',
117
- titleProps?.className
118
- )}
119
- >
120
- {title || senderName}
121
- </span>
122
- )}
123
- {children}
124
- {attachmentElement || (attachmentProps ? <Attachment {...attachmentProps} /> : null)}
125
- {linkifyTextElement || (linkifyTextProps ? <LinkifyText {...linkifyTextProps} /> : null)}
126
- {linkPreviewElement || (linkPreviewProps ? <LinkPreview {...linkPreviewProps} /> : null)}
127
- <div className="absolute bottom-1 right-2 flex items-center gap-1 italic">
128
- <FormattedDate distanceToNow {...formattedDateProps} />
129
- <StatusSent {...statusSentProps} />
130
- </div>
131
- </div>
132
- </div>
133
- );
134
- }
135
-
136
- const ChatMessage = memo(forwardRef<HTMLDivElement, ChatMessageProps>(ChatMessageBase));
137
-
138
- export { ChatMessage, type ChatMessageProps };
@@ -1,149 +0,0 @@
1
- import { forwardRef, memo } from 'react';
2
- import { Users } from 'lucide-react';
3
- import { cn } from './utils';
4
- import { Avatar } from './avatar';
5
- import { StatusSent } from './status-sent';
6
- import { FormattedDate } from './formatted-date';
7
- import { Badge } from './badge';
8
-
9
- function DialogItemBase(
10
- {
11
- index = -1,
12
- isPrivateDialog,
13
- selected,
14
- onSelect = () => {},
15
- name,
16
- photo,
17
- userOnline,
18
- userPresence,
19
- hasGroupIcon,
20
- lastSentStatus,
21
- lastSentDate,
22
- lastSenderName,
23
- lastMessage,
24
- typingStatusText,
25
- draft,
26
- draftLabel = 'Draft',
27
- unreadCount = 0,
28
- language = 'en',
29
- divider = 'bottom',
30
- avatarProps,
31
- contentProps,
32
- headerProps,
33
- headerLeftProps,
34
- groupIconProps,
35
- nameProps,
36
- headerRightProps,
37
- statusSentIconProps,
38
- formattedDateProps,
39
- footerProps,
40
- lastMessageProps,
41
- draftLabelProps,
42
- lastSenderNameProps,
43
- unreadBadgeProps,
44
- dividerProps,
45
- ...props
46
- },
47
- ref
48
- ) {
49
- const avatarSource = photo || avatarProps?.src;
50
- const avatarFallback = name || avatarProps?.name;
51
- const avatarOnline = isPrivateDialog ? userOnline || avatarProps?.online : false;
52
- const avatarPresence = isPrivateDialog ? userPresence || avatarProps?.presence : undefined;
53
- const showTopDivider = divider === 'top' || (divider === 'both' && index === 0);
54
- const showBottomDivider = divider === 'bottom' || divider === 'both';
55
-
56
- return (
57
- <div
58
- ref={ref}
59
- {...props}
60
- onClick={onSelect}
61
- className={cn(
62
- 'flex items-start gap-2 px-2 flex-1 cursor-pointer',
63
- 'transition-colors duration-200 ease-linear',
64
- `${selected ? 'border-l-[0.25em] pl-1 border-l-ring bg-ring/20' : 'hover:bg-ring/5'}`,
65
- props?.className
66
- )}
67
- >
68
- <Avatar
69
- src={avatarSource}
70
- name={avatarFallback}
71
- online={avatarOnline}
72
- presence={avatarPresence}
73
- {...avatarProps}
74
- className={cn(`size-13 my-2 ${photo ? 'bg-ring/10' : 'bg-ring/30'}`, avatarProps?.className)}
75
- />
76
-
77
- <div
78
- {...contentProps}
79
- className={cn('relative self-stretch flex-1 flex-col text-foreground', contentProps?.className)}
80
- >
81
- <div {...headerProps} className={cn('flex items-center justify-between gap-1', headerProps?.className)}>
82
- <div {...headerLeftProps} className={cn('flex items-center gap-1', headerLeftProps?.className)}>
83
- {!isPrivateDialog && hasGroupIcon && (
84
- <Users
85
- {...groupIconProps}
86
- className={cn('text-muted-foreground size-4 min-w-4', groupIconProps?.className)}
87
- />
88
- )}
89
- <span {...nameProps} className={cn('font-medium text-left line-clamp-1', nameProps?.className)}>
90
- {name}
91
- </span>
92
- </div>
93
- <div {...headerRightProps} className={cn('flex items-center gap-1', headerRightProps?.className)}>
94
- <StatusSent status={lastSentStatus} {...statusSentIconProps} />
95
- <FormattedDate date={lastSentDate} language={language} {...formattedDateProps} />
96
- </div>
97
- </div>
98
- <div {...footerProps} className={cn('flex items-start justify-between gap-1', footerProps?.className)}>
99
- <span
100
- {...lastMessageProps}
101
- className={cn('text-sm text-left text-muted-foreground line-clamp-2', lastMessageProps?.className)}
102
- >
103
- {typingStatusText ||
104
- (draft ? (
105
- <span>
106
- <span
107
- {...draftLabelProps}
108
- className={cn('text-red-500 font-medium', draftLabelProps?.className)}
109
- >{`${draftLabel}: `}</span>
110
- {draft}
111
- </span>
112
- ) : (
113
- <span>
114
- {lastSenderName && (
115
- <span
116
- {...lastSenderNameProps}
117
- className={cn('font-semibold', lastSenderNameProps?.className)}
118
- >{`${lastSenderName}: `}</span>
119
- )}
120
- {lastMessage}
121
- </span>
122
- ))}
123
- </span>
124
- {unreadCount > 0 && (
125
- <Badge
126
- {...unreadBadgeProps}
127
- className={cn(
128
- 'bg-ring text-background text-xs rounded-full p-1 h-6 min-w-6 mt-0.5',
129
- unreadBadgeProps?.className
130
- )}
131
- >
132
- {unreadCount}
133
- </Badge>
134
- )}
135
- </div>
136
- {showTopDivider && (
137
- <div {...dividerProps} className={cn('absolute -top-px left-0 right-0 border-t', dividerProps?.className)} />
138
- )}
139
- {showBottomDivider && <div className="absolute bottom-0 left-0 right-0 border-b" />}
140
- </div>
141
- </div>
142
- );
143
- }
144
-
145
- const DialogItem = memo(forwardRef(DialogItemBase));
146
-
147
- DialogItem.displayName = 'DialogItem';
148
-
149
- export { DialogItem };
@@ -1,188 +0,0 @@
1
- import type React from 'react';
2
- import { forwardRef, memo } from 'react';
3
- import { Users, type LucideProps } from 'lucide-react';
4
- import { cn } from './utils';
5
- import { Avatar, type AvatarProps } from './avatar';
6
- import { type PresenceStatus } from './presence';
7
- import { StatusSent, type StatusSentProps } from './status-sent';
8
- import { FormattedDate, type FormattedDateProps } from './formatted-date';
9
- import { Badge, type BadgeProps } from './badge';
10
-
11
- interface DialogItemProps extends React.ComponentProps<'div'> {
12
- index?: number;
13
- isPrivateDialog?: boolean;
14
- selected?: boolean;
15
- onSelect?: () => void;
16
- name: string;
17
- photo?: string;
18
- userOnline?: boolean;
19
- userPresence?: PresenceStatus;
20
- hasGroupIcon?: boolean;
21
- lastSentStatus?: StatusSentProps['status'];
22
- lastSentDate?: FormattedDateProps['date'];
23
- lastSenderName?: string;
24
- lastMessage?: string;
25
- typingStatusText?: string;
26
- draft?: string;
27
- draftLabel?: string;
28
- unreadCount?: number;
29
- language?: string;
30
- divider?: 'top' | 'bottom' | 'both' | 'none';
31
- avatarProps?: AvatarProps;
32
- contentProps?: React.ComponentProps<'div'>;
33
- headerProps?: React.ComponentProps<'div'>;
34
- headerLeftProps?: React.ComponentProps<'div'>;
35
- groupIconProps?: LucideProps;
36
- nameProps?: React.ComponentProps<'span'>;
37
- headerRightProps?: React.ComponentProps<'div'>;
38
- statusSentIconProps?: StatusSentProps;
39
- formattedDateProps?: FormattedDateProps;
40
- footerProps?: React.ComponentProps<'div'>;
41
- lastMessageProps?: React.ComponentProps<'span'>;
42
- draftLabelProps?: React.ComponentProps<'span'>;
43
- lastSenderNameProps?: React.ComponentProps<'span'>;
44
- unreadBadgeProps?: BadgeProps;
45
- dividerProps?: React.ComponentProps<'div'>;
46
- }
47
-
48
- function DialogItemBase(
49
- {
50
- index = -1,
51
- isPrivateDialog,
52
- selected,
53
- onSelect = () => {},
54
- name,
55
- photo,
56
- userOnline,
57
- userPresence,
58
- hasGroupIcon,
59
- lastSentStatus,
60
- lastSentDate,
61
- lastSenderName,
62
- lastMessage,
63
- typingStatusText,
64
- draft,
65
- draftLabel = 'Draft',
66
- unreadCount = 0,
67
- language = 'en',
68
- divider = 'bottom',
69
- avatarProps,
70
- contentProps,
71
- headerProps,
72
- headerLeftProps,
73
- groupIconProps,
74
- nameProps,
75
- headerRightProps,
76
- statusSentIconProps,
77
- formattedDateProps,
78
- footerProps,
79
- lastMessageProps,
80
- draftLabelProps,
81
- lastSenderNameProps,
82
- unreadBadgeProps,
83
- dividerProps,
84
- ...props
85
- }: DialogItemProps,
86
- ref: React.ForwardedRef<HTMLDivElement>
87
- ) {
88
- const avatarSource = photo || avatarProps?.src;
89
- const avatarFallback = name || avatarProps?.name;
90
- const avatarOnline = isPrivateDialog ? userOnline || avatarProps?.online : false;
91
- const avatarPresence = isPrivateDialog ? userPresence || avatarProps?.presence : undefined;
92
- const showTopDivider = divider === 'top' || (divider === 'both' && index === 0);
93
- const showBottomDivider = divider === 'bottom' || divider === 'both';
94
-
95
- return (
96
- <div
97
- ref={ref}
98
- {...props}
99
- onClick={onSelect}
100
- className={cn(
101
- 'flex items-start gap-2 px-2 flex-1 cursor-pointer',
102
- 'transition-colors duration-200 ease-linear',
103
- `${selected ? 'border-l-[0.25em] pl-1 border-l-ring bg-ring/20' : 'hover:bg-ring/5'}`,
104
- props?.className
105
- )}
106
- >
107
- <Avatar
108
- src={avatarSource}
109
- name={avatarFallback}
110
- online={avatarOnline}
111
- presence={avatarPresence}
112
- {...avatarProps}
113
- className={cn(`size-13 my-2 ${photo ? 'bg-ring/10' : 'bg-ring/30'}`, avatarProps?.className)}
114
- />
115
-
116
- <div
117
- {...contentProps}
118
- className={cn('relative self-stretch flex-1 flex-col text-foreground', contentProps?.className)}
119
- >
120
- <div {...headerProps} className={cn('flex items-center justify-between gap-1', headerProps?.className)}>
121
- <div {...headerLeftProps} className={cn('flex items-center gap-1', headerLeftProps?.className)}>
122
- {!isPrivateDialog && hasGroupIcon && (
123
- <Users
124
- {...groupIconProps}
125
- className={cn('text-muted-foreground size-4 min-w-4', groupIconProps?.className)}
126
- />
127
- )}
128
- <span {...nameProps} className={cn('font-medium text-left line-clamp-1', nameProps?.className)}>
129
- {name}
130
- </span>
131
- </div>
132
- <div {...headerRightProps} className={cn('flex items-center gap-1', headerRightProps?.className)}>
133
- <StatusSent status={lastSentStatus} {...statusSentIconProps} />
134
- <FormattedDate date={lastSentDate} language={language} {...formattedDateProps} />
135
- </div>
136
- </div>
137
- <div {...footerProps} className={cn('flex items-start justify-between gap-1', footerProps?.className)}>
138
- <span
139
- {...lastMessageProps}
140
- className={cn('text-sm text-left text-muted-foreground line-clamp-2', lastMessageProps?.className)}
141
- >
142
- {typingStatusText ||
143
- (draft ? (
144
- <span>
145
- <span
146
- {...draftLabelProps}
147
- className={cn('text-red-500 font-medium', draftLabelProps?.className)}
148
- >{`${draftLabel}: `}</span>
149
- {draft}
150
- </span>
151
- ) : (
152
- <span>
153
- {lastSenderName && (
154
- <span
155
- {...lastSenderNameProps}
156
- className={cn('font-semibold', lastSenderNameProps?.className)}
157
- >{`${lastSenderName}: `}</span>
158
- )}
159
- {lastMessage}
160
- </span>
161
- ))}
162
- </span>
163
- {unreadCount > 0 && (
164
- <Badge
165
- {...unreadBadgeProps}
166
- className={cn(
167
- 'bg-ring text-background text-xs rounded-full p-1 h-6 min-w-6 mt-0.5',
168
- unreadBadgeProps?.className
169
- )}
170
- >
171
- {unreadCount}
172
- </Badge>
173
- )}
174
- </div>
175
- {showTopDivider && (
176
- <div {...dividerProps} className={cn('absolute -top-px left-0 right-0 border-t', dividerProps?.className)} />
177
- )}
178
- {showBottomDivider && <div className="absolute bottom-0 left-0 right-0 border-b" />}
179
- </div>
180
- </div>
181
- );
182
- }
183
-
184
- const DialogItem = memo(forwardRef<HTMLDivElement, DialogItemProps>(DialogItemBase));
185
-
186
- DialogItem.displayName = 'DialogItem';
187
-
188
- export { DialogItem };
@@ -1,200 +0,0 @@
1
- import { forwardRef, useState } from 'react';
2
- import { FilePlusCorner, Paperclip } from 'lucide-react';
3
- import { Label } from './label';
4
- import { cn } from './utils';
5
-
6
- const areValidFiles = (files, accept) => {
7
- if (files.length === 0) {
8
- return false;
9
- }
10
-
11
- const acceptList =
12
- typeof accept === 'string' ? accept.split(/\s*,\s*|\s+/).map((type) => type.toLowerCase().trim()) : null;
13
-
14
- if (acceptList === null || acceptList.length === 0) {
15
- return false;
16
- }
17
-
18
- return files.every((file) => {
19
- const { type, size } = file;
20
-
21
- if (typeof type !== 'string' || typeof size !== 'number' || size <= 0 || type.length === 0) {
22
- return false;
23
- }
24
-
25
- const fileMimeType = type.toLowerCase();
26
- const [fileType, fileSubtype] = fileMimeType.split('/');
27
-
28
- return acceptList.some(
29
- (acceptType) =>
30
- acceptType.startsWith('*') ||
31
- acceptType === fileMimeType ||
32
- (fileType && acceptType.endsWith('/*') && acceptType.startsWith(fileType)) ||
33
- (acceptType.startsWith('.') && acceptType.slice(1) === fileSubtype)
34
- );
35
- });
36
- };
37
- const handleFiles = (fileList, accept, multiple, onSelectFile, onInvalidFile) => {
38
- const files = Array.from(fileList || []).filter(Boolean);
39
-
40
- if (files.length === 0) {
41
- return;
42
- }
43
-
44
- const validated = areValidFiles(files, accept);
45
-
46
- if (validated) {
47
- onSelectFile(multiple ? files : files[0] ? [files[0]] : []);
48
- } else {
49
- onInvalidFile();
50
- }
51
- };
52
-
53
- function FilePickerInputBase(
54
- {
55
- onSelectFile = () => {},
56
- onInvalidFile = () => {},
57
- multiple = false,
58
- accept = '*/*',
59
- iconElement,
60
- labelProps,
61
- iconProps,
62
- ...props
63
- },
64
- ref
65
- ) {
66
- const handleChange = (event) => {
67
- handleFiles(event.currentTarget.files, accept, multiple, onSelectFile, onInvalidFile);
68
- event.currentTarget.value = '';
69
- };
70
-
71
- return (
72
- <Label
73
- {...labelProps}
74
- htmlFor="file-uploader"
75
- className={cn(
76
- 'group p-2 rounded-full hover:bg-ring/10 transition-all duration-200 ease-in-out cursor-pointer',
77
- labelProps?.className
78
- )}
79
- >
80
- {iconElement || (
81
- <Paperclip
82
- {...iconProps}
83
- className={cn(
84
- 'text-foreground group-hover:scale-110 transition-all duration-200 ease-in-out',
85
- iconProps?.className
86
- )}
87
- />
88
- )}
89
- <input
90
- ref={ref}
91
- id="file-uploader"
92
- type="file"
93
- multiple={multiple}
94
- accept={accept}
95
- onChange={handleChange}
96
- {...props}
97
- className={cn('hidden', props?.className)}
98
- />
99
- </Label>
100
- );
101
- }
102
-
103
- const FilePickerInput = forwardRef(FilePickerInputBase);
104
-
105
- FilePickerInput.displayName = 'FilePickerInput';
106
-
107
- function FilePickerDropzoneBase(
108
- {
109
- onSelectFile = () => {},
110
- onInvalidFile = () => {},
111
- multiple = false,
112
- accept = '*/*',
113
- children,
114
- iconElement,
115
- placeholder,
116
- dropZoneProps,
117
- placeholderContainerProps,
118
- iconProps,
119
- placeholderProps,
120
- ...props
121
- },
122
- ref
123
- ) {
124
- const [isDragging, setIsDragging] = useState(false);
125
- const handleDragEvent = (event) => {
126
- event.preventDefault();
127
- event.stopPropagation();
128
- };
129
- const handleDrop = (event) => {
130
- handleDragEvent(event);
131
- setIsDragging(false);
132
- handleFiles(event.dataTransfer.files, accept, multiple, onSelectFile, onInvalidFile);
133
- event.dataTransfer.clearData();
134
- };
135
- const handleDragEnter = (event) => {
136
- handleDragEvent(event);
137
-
138
- if (event.dataTransfer.items && event.dataTransfer.items.length > 0) {
139
- setIsDragging(true);
140
- }
141
- };
142
- const handleDragLeave = (event) => {
143
- if (!event.currentTarget.contains(event.relatedTarget)) {
144
- setIsDragging(false);
145
- }
146
- };
147
-
148
- return (
149
- <div
150
- ref={ref}
151
- onDragEnter={handleDragEnter}
152
- onDragLeave={handleDragLeave}
153
- onDragOver={handleDragEvent}
154
- {...props}
155
- className={cn('size-full relative', props?.className)}
156
- >
157
- {children}
158
- <div
159
- onDrop={handleDrop}
160
- {...dropZoneProps}
161
- className={cn(
162
- 'group absolute top-0 left-0 size-full',
163
- 'flex items-center justify-center',
164
- 'transition-all duration-300 ease-out',
165
- 'border-2 border-dashed border-ring bg-ring/25 rounded-md',
166
- isDragging
167
- ? 'opacity-100 pointer-events-auto visible scale-100'
168
- : 'opacity-0 pointer-events-none invisible scale-98',
169
- dropZoneProps?.className
170
- )}
171
- >
172
- <div
173
- {...placeholderContainerProps}
174
- className={cn(
175
- 'bg-muted flex flex-row items-center gap-2 rounded-full py-2 px-4 transition-all',
176
- isDragging ? 'scale-100' : 'scale-90',
177
- placeholderContainerProps?.className
178
- )}
179
- >
180
- {iconElement || (
181
- <FilePlusCorner
182
- absoluteStrokeWidth
183
- {...iconProps}
184
- className={cn('text-ring size-6', iconProps?.className)}
185
- />
186
- )}
187
- <span {...placeholderProps} className={cn('text-ring text-md font-bold', placeholderProps?.className)}>
188
- {placeholder || 'Drop files here'}
189
- </span>
190
- </div>
191
- </div>
192
- </div>
193
- );
194
- }
195
-
196
- const FilePickerDropzone = forwardRef(FilePickerDropzoneBase);
197
-
198
- FilePickerDropzone.displayName = 'FilePickerDropzone';
199
-
200
- export { FilePickerInput, FilePickerDropzone, FilePickerInput as Input, FilePickerDropzone as Dropzone };