@connectycube/react-ui-kit 0.0.18 → 0.0.19

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 (39) hide show
  1. package/configs/dependencies.json +6 -0
  2. package/configs/imports.json +3 -1
  3. package/dist/index.cjs +14 -5
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.js +14 -6
  6. package/dist/index.js.map +1 -1
  7. package/dist/types/components/attachment.d.ts +45 -0
  8. package/dist/types/components/attachment.d.ts.map +1 -0
  9. package/dist/types/components/avatar.d.ts +1 -1
  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/dialog-item.d.ts +4 -4
  14. package/dist/types/components/dialog-item.d.ts.map +1 -1
  15. package/dist/types/components/stream-view.d.ts.map +1 -1
  16. package/dist/types/components/switch.d.ts +6 -0
  17. package/dist/types/components/switch.d.ts.map +1 -0
  18. package/dist/types/components/utils.d.ts +1 -0
  19. package/dist/types/components/utils.d.ts.map +1 -1
  20. package/dist/types/index.d.ts +21 -4
  21. package/dist/types/index.d.ts.map +1 -1
  22. package/gen/components/attachment.jsx +214 -0
  23. package/gen/components/dialog-item.jsx +7 -7
  24. package/gen/components/dismiss-layer.jsx +1 -1
  25. package/gen/components/file-picker.jsx +2 -2
  26. package/gen/components/stream-view.jsx +1 -5
  27. package/gen/components/switch.jsx +25 -0
  28. package/gen/components/utils.js +4 -0
  29. package/gen/index.js +46 -0
  30. package/package.json +2 -1
  31. package/src/components/attachment.tsx +270 -0
  32. package/src/components/avatar.tsx +1 -1
  33. package/src/components/dialog-item.tsx +9 -9
  34. package/src/components/dismiss-layer.tsx +1 -1
  35. package/src/components/file-picker.tsx +2 -2
  36. package/src/components/stream-view.tsx +1 -5
  37. package/src/components/switch.tsx +27 -0
  38. package/src/components/utils.ts +4 -0
  39. package/src/index.ts +72 -4
@@ -0,0 +1,270 @@
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
+ uploading?: boolean;
12
+ onReady?: (skipOnce?: boolean) => void;
13
+ linkProps?: AttachmentLinkProps;
14
+ containerProps?: React.ComponentProps<'div'>;
15
+ }
16
+
17
+ interface AttachmentLinkProps
18
+ extends React.ComponentProps<'a'>,
19
+ Omit<AttachmentProps, 'containerProps' & 'mimeType' & 'onReady'> {
20
+ children?: React.ReactNode;
21
+ }
22
+
23
+ interface AttachmentImageProps
24
+ extends React.ComponentProps<'img'>,
25
+ Omit<AttachmentProps, 'containerProps' & 'mimeType'> {}
26
+
27
+ interface AttachmentAudioProps
28
+ extends React.ComponentProps<'audio'>,
29
+ Omit<AttachmentProps, 'linkProps' & 'mimeType' & 'onReady'> {}
30
+
31
+ interface AttachmentVideoProps extends React.ComponentProps<'video'>, Omit<AttachmentProps, 'linkProps' & 'mimeType'> {
32
+ maxSize?: number;
33
+ }
34
+
35
+ interface AttachmentFileProps extends LucideProps, Omit<AttachmentProps, 'containerProps' & 'mimeType'> {
36
+ name?: string | undefined;
37
+ iconElement?: React.ReactNode;
38
+ }
39
+
40
+ interface AttachmentFailedProps extends LucideProps, Omit<AttachmentProps, 'linkProps' & 'mimeType'> {
41
+ name?: string | undefined;
42
+ iconElement?: React.ReactNode;
43
+ }
44
+
45
+ function AttachmentLinkBase(
46
+ { url, uploading = false, children, ...props }: AttachmentLinkProps,
47
+ ref: React.ForwardedRef<HTMLAnchorElement>
48
+ ) {
49
+ return (
50
+ <a
51
+ ref={ref}
52
+ target="_blank"
53
+ rel="noopener noreferrer"
54
+ {...props}
55
+ href={url}
56
+ 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',
58
+ props?.className
59
+ )}
60
+ >
61
+ {children}
62
+ <Spinner loading={uploading} layout="overlay" />
63
+ </a>
64
+ );
65
+ }
66
+
67
+ const AttachmentLink = forwardRef<HTMLAnchorElement, AttachmentLinkProps>(AttachmentLinkBase);
68
+
69
+ AttachmentLink.displayName = 'AttachmentLink';
70
+
71
+ function AttachmentAudioBase(
72
+ { uid, url, uploading = false, containerProps, ...props }: AttachmentAudioProps,
73
+ ref: React.ForwardedRef<HTMLAudioElement>
74
+ ) {
75
+ const audioId = `attachment_audio_${uid || getRandomString()}`;
76
+
77
+ return (
78
+ <div
79
+ {...containerProps}
80
+ className={cn('relative min-h-12 min-w-12 w-full rounded-md overflow-hidden', containerProps?.className)}
81
+ >
82
+ <audio ref={ref} src={url} id={audioId} controls {...props} />
83
+ <Spinner loading={uploading} layout="overlay" />
84
+ </div>
85
+ );
86
+ }
87
+
88
+ const AttachmentAudio = forwardRef<HTMLAudioElement, AttachmentAudioProps>(AttachmentAudioBase);
89
+
90
+ function AttachmentVideoBase(
91
+ { uid, url, maxSize = 360, uploading = false, onReady = () => {}, containerProps, ...props }: AttachmentVideoProps,
92
+ ref: React.ForwardedRef<HTMLVideoElement>
93
+ ) {
94
+ const videoId = `attachment_video_${uid || getRandomString()}`;
95
+ const videoMaxSize = `${maxSize}px`;
96
+ const playerRef = useRef<HTMLVideoElement>(null);
97
+ const [style, setStyle] = useState<React.CSSProperties>({
98
+ maxHeight: videoMaxSize,
99
+ maxWidth: videoMaxSize,
100
+ });
101
+ const handleCanPlay = (event: React.SyntheticEvent<HTMLVideoElement, Event>) => {
102
+ const player = playerRef.current;
103
+
104
+ if (player) {
105
+ const { videoWidth, videoHeight } = player;
106
+ const ratio = videoWidth / videoHeight || 1;
107
+ const height = ratio < 1 ? videoMaxSize : `${Math.round(maxSize / ratio)}px`;
108
+ const width = ratio < 1 ? `${Math.round(maxSize * ratio)}px` : videoMaxSize;
109
+
110
+ setStyle({ height, width, maxHeight: height, maxWidth: width });
111
+ props.onCanPlay?.(event);
112
+ onReady();
113
+ }
114
+ };
115
+
116
+ useImperativeHandle(ref, () => playerRef.current!, []);
117
+
118
+ return (
119
+ <div
120
+ {...containerProps}
121
+ className={cn('relative min-h-20 w-full rounded-md overflow-hidden', containerProps?.className)}
122
+ >
123
+ <video
124
+ id={videoId}
125
+ ref={playerRef}
126
+ src={url}
127
+ controls
128
+ preload="metadata"
129
+ style={style}
130
+ {...props}
131
+ onCanPlay={handleCanPlay}
132
+ className={cn('size-full', props?.className)}
133
+ />
134
+ <Spinner loading={uploading} layout="overlay" />
135
+ </div>
136
+ );
137
+ }
138
+
139
+ const AttachmentVideo = forwardRef<HTMLVideoElement, AttachmentVideoProps>(AttachmentVideoBase);
140
+
141
+ AttachmentVideo.displayName = 'AttachmentVideo';
142
+
143
+ function AttachmentImageBase(
144
+ { uid, url, uploading = false, onReady = () => {}, linkProps, ...props }: AttachmentImageProps,
145
+ ref: React.ForwardedRef<HTMLImageElement>
146
+ ) {
147
+ const imageId = `attachment_image_${uid || getRandomString()}`;
148
+ const handleLoad = (event: React.SyntheticEvent<HTMLImageElement, Event>) => {
149
+ props.onLoad?.(event);
150
+ onReady();
151
+ };
152
+
153
+ return (
154
+ <AttachmentLink href={url} uploading={uploading} {...linkProps}>
155
+ <img
156
+ ref={ref}
157
+ src={url}
158
+ id={imageId}
159
+ alt="attachment"
160
+ {...props}
161
+ 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',
163
+ props?.className
164
+ )}
165
+ onLoad={handleLoad}
166
+ />
167
+ </AttachmentLink>
168
+ );
169
+ }
170
+
171
+ const AttachmentImage = forwardRef<HTMLImageElement, AttachmentImageProps>(AttachmentImageBase);
172
+
173
+ AttachmentImage.displayName = 'AttachmentImage';
174
+
175
+ function AttachmentFile({ url, name, uploading = false, iconElement, linkProps, ...props }: AttachmentFileProps) {
176
+ const fileId = `attachment_file_${props.id || getRandomString()}`;
177
+
178
+ return (
179
+ <AttachmentLink
180
+ href={url}
181
+ uploading={uploading}
182
+ {...linkProps}
183
+ className={cn('flex-row gap-2 px-2', linkProps?.className)}
184
+ >
185
+ {iconElement || (
186
+ <File
187
+ id={fileId}
188
+ {...props}
189
+ className={cn(
190
+ 'size-6 shrink-0 text-foreground/85 group-hover:text-foreground duration-300 ease-out',
191
+ props?.className
192
+ )}
193
+ />
194
+ )}
195
+ {name && (
196
+ <span className="font-medium line-clamp-1 break-all text-foreground/85 group-hover:text-foreground duration-300 ease-out">
197
+ {name}
198
+ </span>
199
+ )}
200
+ </AttachmentLink>
201
+ );
202
+ }
203
+
204
+ AttachmentFile.displayName = 'AttachmentFile';
205
+
206
+ function AttachmentFailed({
207
+ name = 'Unknown file',
208
+ uploading = false,
209
+ iconElement,
210
+ containerProps,
211
+ ...props
212
+ }: AttachmentFailedProps) {
213
+ const failedId = `attachment_failed_${props.id || getRandomString()}`;
214
+
215
+ return (
216
+ <div
217
+ {...containerProps}
218
+ 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',
220
+ containerProps?.className
221
+ )}
222
+ >
223
+ {iconElement || (
224
+ <FileXCorner id={failedId} {...props} className={cn('size-6 shrink-0 text-red-600', props?.className)} />
225
+ )}
226
+ <span className="font-medium line-clamp-1 break-all text-red-600">{name}</span>
227
+ <Spinner loading={uploading} layout="overlay" />
228
+ </div>
229
+ );
230
+ }
231
+
232
+ AttachmentFailed.displayName = 'AttachmentFailed';
233
+
234
+ function AttachmentBase({ mimeType, ...props }: AttachmentProps) {
235
+ const [type = ''] = mimeType?.split('/') || [];
236
+
237
+ if (!props.url) {
238
+ return <AttachmentFailed {...props} />;
239
+ }
240
+
241
+ switch (type) {
242
+ case 'image':
243
+ return <AttachmentImage {...props} />;
244
+ case 'video':
245
+ return <AttachmentVideo {...props} />;
246
+ case 'audio':
247
+ return <AttachmentAudio {...props} />;
248
+ default:
249
+ return <AttachmentFile name={mimeType} {...props} />;
250
+ }
251
+ }
252
+
253
+ const Attachment = memo(AttachmentBase);
254
+
255
+ export {
256
+ Attachment,
257
+ AttachmentLink,
258
+ AttachmentImage,
259
+ AttachmentAudio,
260
+ AttachmentVideo,
261
+ AttachmentFile,
262
+ AttachmentFailed,
263
+ type AttachmentLinkProps,
264
+ type AttachmentImageProps,
265
+ type AttachmentAudioProps,
266
+ type AttachmentVideoProps,
267
+ type AttachmentFileProps,
268
+ type AttachmentFailedProps,
269
+ type AttachmentProps,
270
+ };
@@ -7,7 +7,7 @@ import { cn } from './utils';
7
7
  interface AvatarProps extends AvatarPrimitive.AvatarProps {
8
8
  src?: string | undefined;
9
9
  name?: string | undefined;
10
- online?: boolean;
10
+ online?: boolean | undefined;
11
11
  presence?: PresenceStatus;
12
12
  onlineProps?: React.ComponentProps<'div'>;
13
13
  presenceProps?: PresenceBadgeProps;
@@ -1,11 +1,11 @@
1
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
2
  import { Avatar, type AvatarProps } from './avatar';
6
- import { type PresenceStatus } from './presence';
7
- import { StatusSent, type StatusSentProps } from './status-sent';
3
+ import { Users, type LucideProps } from 'lucide-react';
4
+ import { forwardRef, memo } from 'react';
8
5
  import { FormattedDate, type FormattedDateProps } from './formatted-date';
6
+ import { StatusSent, type StatusSentProps } from './status-sent';
7
+ import { type PresenceStatus } from './presence';
8
+ import { cn } from './utils';
9
9
  import { Badge, type BadgeProps } from './badge';
10
10
 
11
11
  interface DialogItemProps extends React.ComponentProps<'div'> {
@@ -85,9 +85,9 @@ function DialogItemBase(
85
85
  }: DialogItemProps,
86
86
  ref: React.ForwardedRef<HTMLDivElement>
87
87
  ) {
88
- const avatarSource = photo || avatarProps?.src || undefined;
89
- const avatarFallback = name || avatarProps?.name || undefined;
90
- const avatarOnline = isPrivateDialog ? userOnline || avatarProps?.online || false : false;
88
+ const avatarSource = photo || avatarProps?.src;
89
+ const avatarFallback = name || avatarProps?.name;
90
+ const avatarOnline = isPrivateDialog ? userOnline || avatarProps?.online : false;
91
91
  const avatarPresence = isPrivateDialog ? userPresence || avatarProps?.presence : undefined;
92
92
  const showTopDivider = divider === 'top' || (divider === 'both' && index === 0);
93
93
  const showBottomDivider = divider === 'bottom' || divider === 'both';
@@ -185,4 +185,4 @@ const DialogItem = memo(forwardRef<HTMLDivElement, DialogItemProps>(DialogItemBa
185
185
 
186
186
  DialogItem.displayName = 'DialogItem';
187
187
 
188
- export { DialogItem };
188
+ export { DialogItem, type DialogItemProps };
@@ -16,7 +16,7 @@ function DismissLayerBase(
16
16
  ) {
17
17
  const innerRef = useRef<HTMLDivElement>(null);
18
18
 
19
- useImperativeHandle(ref, () => innerRef.current!);
19
+ useImperativeHandle(ref, () => innerRef.current!, []);
20
20
 
21
21
  const handleClickOrTouch = useCallback(
22
22
  (e: React.MouseEvent | React.TouchEvent) => {
@@ -176,14 +176,14 @@ function FilePickerDropzoneBase(
176
176
  onDragLeave={handleDragLeave}
177
177
  onDragOver={handleDragEvent}
178
178
  {...props}
179
- className={cn('size-full relative', props?.className)}
179
+ className={cn('relative size-full min-h-0', props?.className)}
180
180
  >
181
181
  {children}
182
182
  <div
183
183
  onDrop={handleDrop}
184
184
  {...dropZoneProps}
185
185
  className={cn(
186
- 'group absolute top-0 left-0 size-full',
186
+ 'absolute top-0 left-0 size-full',
187
187
  'flex items-center justify-center',
188
188
  'transition-all duration-300 ease-out',
189
189
  'border-2 border-dashed border-ring bg-ring/25 rounded-md',
@@ -1,17 +1,13 @@
1
1
  import type React from 'react';
2
2
  import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
3
3
  import { Maximize, Minimize, PictureInPicture2, type LucideProps } from 'lucide-react';
4
- import { cn } from './utils';
4
+ import { cn, getRandomString } from './utils';
5
5
 
6
6
  interface StreamViewProps extends React.ComponentProps<'video'> {
7
7
  stream?: MediaStream | null;
8
8
  mirror?: boolean;
9
9
  }
10
10
 
11
- function getRandomString(length = 8): string {
12
- return (Date.now() / Math.random()).toString(36).replace('.', '').slice(0, length);
13
- }
14
-
15
11
  function StreamViewBase(
16
12
  { id, stream, mirror, className, muted, ...props }: StreamViewProps,
17
13
  ref: React.ForwardedRef<HTMLVideoElement>
@@ -0,0 +1,27 @@
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+ import * as SwitchPrimitive from '@radix-ui/react-switch';
5
+ import { cn } from './utils';
6
+
7
+ type SwitchProps = React.ComponentProps<typeof SwitchPrimitive.Root>;
8
+
9
+ function Switch(props: SwitchProps) {
10
+ return (
11
+ <SwitchPrimitive.Root
12
+ {...props}
13
+ className={cn(
14
+ 'peer data-[state=checked]:bg-ring data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15em] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring disabled:cursor-not-allowed disabled:opacity-50',
15
+ props?.className
16
+ )}
17
+ >
18
+ <SwitchPrimitive.Thumb
19
+ className={cn(
20
+ 'bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0'
21
+ )}
22
+ />
23
+ </SwitchPrimitive.Root>
24
+ );
25
+ }
26
+
27
+ export { Switch, type SwitchProps };
@@ -5,6 +5,10 @@ export function cn(...inputs: ClassValue[]) {
5
5
  return twMerge(clsx(inputs));
6
6
  }
7
7
 
8
+ export function getRandomString(length = 8): string {
9
+ return (Date.now() / Math.random()).toString(36).replace('.', '').slice(0, length);
10
+ }
11
+
8
12
  export function capitalize(str?: string): string {
9
13
  return typeof str === 'string' && str.length > 0 ? `${str[0]?.toUpperCase()}${str.slice(1)}` : '';
10
14
  }
package/src/index.ts CHANGED
@@ -1,7 +1,75 @@
1
- export { DismissLayer } from './components/dismiss-layer';
1
+ export {
2
+ Attachment,
3
+ AttachmentLink,
4
+ AttachmentImage,
5
+ AttachmentAudio,
6
+ AttachmentVideo,
7
+ AttachmentFile,
8
+ AttachmentFailed,
9
+ type AttachmentProps,
10
+ type AttachmentLinkProps,
11
+ type AttachmentImageProps,
12
+ type AttachmentAudioProps,
13
+ type AttachmentVideoProps,
14
+ type AttachmentFileProps,
15
+ type AttachmentFailedProps,
16
+ } from './components/attachment';
2
17
 
3
- export type { DismissLayerProps } from './components/dismiss-layer';
18
+ export { Avatar, type AvatarProps } from './components/avatar';
4
19
 
5
- export { StreamView, LocalStreamView, RemoteStreamView, FullscreenStreamView } from './components/stream-view';
20
+ export { Badge, type BadgeProps } from './components/badge';
6
21
 
7
- export type { StreamViewProps, FullscreenStreamViewProps, FullscreenStreamViewRef } from './components/stream-view';
22
+ export { Button, type ButtonProps } from './components/button';
23
+
24
+ export { DialogItem, type DialogItemProps } from './components/dialog-item';
25
+
26
+ export { DismissLayer, type DismissLayerProps } from './components/dismiss-layer';
27
+
28
+ export {
29
+ FilePickerInput,
30
+ FilePickerDropzone,
31
+ type FilePickerInputProps,
32
+ type FilePickerDropzoneProps,
33
+ } from './components/file-picker';
34
+
35
+ export { FormattedDate, type FormattedDateProps } from './components/formatted-date';
36
+
37
+ export { Input, type InputProps } from './components/input';
38
+
39
+ export { Label, type LabelProps } from './components/label';
40
+
41
+ export { LinkPreview, type LinkPreviewProps } from './components/link-preview';
42
+
43
+ export { LinkifyText, type LinkifyTextProps } from './components/linkify-text';
44
+
45
+ export { PlaceholderText, type PlaceholderTextProps } from './components/placeholder-text';
46
+
47
+ export {
48
+ Presence,
49
+ PresenceBadge,
50
+ type PresenceStatus,
51
+ type PresenceProps,
52
+ type PresenceBadgeProps,
53
+ } from './components/presence';
54
+
55
+ export { Search, type SearchProps } from './components/search';
56
+
57
+ export { Spinner, type SpinnerProps } from './components/spinner';
58
+
59
+ export { StatusIndicator, type StatusName, type StatusIndicatorProps } from './components/status-indicator';
60
+
61
+ export { StatusSent, type StatusSentProps } from './components/status-sent';
62
+
63
+ export {
64
+ StreamView,
65
+ LocalStreamView,
66
+ RemoteStreamView,
67
+ FullscreenStreamView,
68
+ type StreamViewProps,
69
+ type FullscreenStreamViewProps,
70
+ type FullscreenStreamViewRef,
71
+ } from './components/stream-view';
72
+
73
+ export { Switch, type SwitchProps } from './components/switch';
74
+
75
+ export * from './components/utils';