@connectycube/react-ui-kit 0.0.17 → 0.0.18
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.
- package/configs/dependencies.json +21 -0
- package/configs/imports.json +7 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/types/components/avatar.d.ts +2 -2
- package/dist/types/components/avatar.d.ts.map +1 -1
- package/dist/types/components/badge.d.ts +2 -2
- package/dist/types/components/badge.d.ts.map +1 -1
- package/dist/types/components/dialog-item.d.ts +46 -0
- package/dist/types/components/dialog-item.d.ts.map +1 -0
- package/dist/types/components/file-picker.d.ts +22 -0
- package/dist/types/components/file-picker.d.ts.map +1 -0
- package/dist/types/components/formatted-date.d.ts +8 -0
- package/dist/types/components/formatted-date.d.ts.map +1 -0
- package/dist/types/components/input.d.ts.map +1 -1
- package/dist/types/components/label.d.ts +5 -0
- package/dist/types/components/label.d.ts.map +1 -0
- package/dist/types/components/link-preview.d.ts +21 -0
- package/dist/types/components/link-preview.d.ts.map +1 -0
- package/dist/types/components/linkify-text.d.ts +9 -0
- package/dist/types/components/linkify-text.d.ts.map +1 -0
- package/dist/types/components/spinner.d.ts +3 -1
- package/dist/types/components/spinner.d.ts.map +1 -1
- package/dist/types/components/status-sent.d.ts +7 -0
- package/dist/types/components/status-sent.d.ts.map +1 -0
- package/dist/types/components/stream-view.d.ts.map +1 -1
- package/dist/types/components/utils.d.ts +0 -2
- package/dist/types/components/utils.d.ts.map +1 -1
- package/gen/components/avatar.jsx +13 -3
- package/gen/components/badge.jsx +3 -3
- package/gen/components/button.jsx +2 -2
- package/gen/components/dialog-item.jsx +149 -0
- package/gen/components/file-picker.jsx +200 -0
- package/gen/components/formatted-date.jsx +57 -0
- package/gen/components/label.jsx +22 -0
- package/gen/components/link-preview.jsx +131 -0
- package/gen/components/linkify-text.jsx +31 -0
- package/gen/components/spinner.jsx +29 -5
- package/gen/components/status-sent.jsx +21 -0
- package/gen/components/stream-view.jsx +5 -1
- package/gen/components/utils.js +0 -11
- package/package.json +4 -1
- package/src/components/avatar.tsx +15 -5
- package/src/components/badge.tsx +6 -6
- package/src/components/button.tsx +2 -2
- package/src/components/connectycube-ui/avatar.jsx +54 -0
- package/src/components/connectycube-ui/avatar.tsx +77 -0
- package/src/components/connectycube-ui/badge.jsx +45 -0
- package/src/components/connectycube-ui/badge.tsx +42 -0
- package/src/components/connectycube-ui/dialog-item.jsx +149 -0
- package/src/components/connectycube-ui/dialog-item.tsx +188 -0
- package/src/components/connectycube-ui/file-picker.jsx +200 -0
- package/src/components/connectycube-ui/file-picker.tsx +231 -0
- package/src/components/connectycube-ui/formatted-date.jsx +57 -0
- package/src/components/connectycube-ui/formatted-date.tsx +57 -0
- package/src/components/connectycube-ui/label.jsx +22 -0
- package/src/components/connectycube-ui/label.tsx +23 -0
- package/src/components/connectycube-ui/linkify-text.tsx +40 -0
- package/src/components/connectycube-ui/presence.jsx +81 -0
- package/src/components/connectycube-ui/presence.tsx +96 -0
- package/src/components/connectycube-ui/status-sent.jsx +21 -0
- package/src/components/connectycube-ui/status-sent.tsx +25 -0
- package/src/components/connectycube-ui/utils.js +10 -0
- package/src/components/connectycube-ui/utils.ts +10 -0
- package/src/components/dialog-item.tsx +188 -0
- package/src/components/file-picker.tsx +231 -0
- package/src/components/formatted-date.tsx +57 -0
- package/src/components/input.tsx +1 -1
- package/src/components/label.tsx +23 -0
- package/src/components/link-preview.tsx +149 -0
- package/src/components/linkify-text.tsx +41 -0
- package/src/components/spinner.tsx +31 -5
- package/src/components/status-sent.tsx +25 -0
- package/src/components/stream-view.tsx +5 -1
- package/src/components/utils.ts +0 -11
|
@@ -0,0 +1,200 @@
|
|
|
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 };
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { forwardRef, memo } from 'react';
|
|
2
|
+
import { differenceInCalendarDays, format, formatDistanceToNow, isToday } from 'date-fns';
|
|
3
|
+
import { el, enUS, uk } from 'date-fns/locale';
|
|
4
|
+
import { cn } from './utils';
|
|
5
|
+
|
|
6
|
+
const locales = {
|
|
7
|
+
en: enUS,
|
|
8
|
+
el: el,
|
|
9
|
+
ua: uk,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
function formatDate(date, language = 'en', distanceToNow = false) {
|
|
13
|
+
const locale = locales[language] ?? enUS;
|
|
14
|
+
|
|
15
|
+
if (distanceToNow) {
|
|
16
|
+
return formatDistanceToNow(date, {
|
|
17
|
+
locale,
|
|
18
|
+
addSuffix: true,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const isWithinLast7Days = (date) => {
|
|
23
|
+
const diff = differenceInCalendarDays(new Date(), date);
|
|
24
|
+
|
|
25
|
+
return diff >= 0 && diff <= 6;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
return isToday(date)
|
|
29
|
+
? format(date, 'p', {
|
|
30
|
+
locale,
|
|
31
|
+
})
|
|
32
|
+
: isWithinLast7Days(date)
|
|
33
|
+
? format(date, 'eeee', {
|
|
34
|
+
locale,
|
|
35
|
+
})
|
|
36
|
+
: format(date, 'P', {
|
|
37
|
+
locale,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function FormattedDateBase({ date, language, distanceToNow, ...props }, ref) {
|
|
42
|
+
if (!date) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<span ref={ref} {...props} className={cn('text-xs text-muted-foreground', props?.className)}>
|
|
48
|
+
{formatDate(date, language, distanceToNow)}
|
|
49
|
+
</span>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const FormattedDate = memo(forwardRef(FormattedDateBase));
|
|
54
|
+
|
|
55
|
+
FormattedDate.displayName = 'FormattedDate';
|
|
56
|
+
|
|
57
|
+
export { FormattedDate };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { forwardRef } from 'react';
|
|
2
|
+
import { Root as LabelRoot } from '@radix-ui/react-label';
|
|
3
|
+
import { cn } from './utils';
|
|
4
|
+
|
|
5
|
+
function LabelBase({ ...props }, ref) {
|
|
6
|
+
return (
|
|
7
|
+
<LabelRoot
|
|
8
|
+
ref={ref}
|
|
9
|
+
{...props}
|
|
10
|
+
className={cn(
|
|
11
|
+
'flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50',
|
|
12
|
+
props?.className
|
|
13
|
+
)}
|
|
14
|
+
/>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const Label = forwardRef(LabelBase);
|
|
19
|
+
|
|
20
|
+
Label.displayName = 'Label';
|
|
21
|
+
|
|
22
|
+
export { Label };
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { forwardRef, memo, useCallback, useState } from 'react';
|
|
2
|
+
import { Globe } from 'lucide-react';
|
|
3
|
+
import { cn } from './utils';
|
|
4
|
+
|
|
5
|
+
const decodeHtmlEntities = (text) => {
|
|
6
|
+
if (text.length === 0) {
|
|
7
|
+
return text;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const element = document.createElement('div');
|
|
11
|
+
|
|
12
|
+
element.innerHTML = text;
|
|
13
|
+
|
|
14
|
+
return element.textContent.trim();
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
function LinkPreviewBase(
|
|
18
|
+
{
|
|
19
|
+
thin = false,
|
|
20
|
+
title = '',
|
|
21
|
+
description = '',
|
|
22
|
+
icon,
|
|
23
|
+
iconFallbackElement,
|
|
24
|
+
image,
|
|
25
|
+
onReady = () => {},
|
|
26
|
+
titleContainerProps,
|
|
27
|
+
iconProps,
|
|
28
|
+
iconFallbackProps,
|
|
29
|
+
titleProps,
|
|
30
|
+
descriptionProps,
|
|
31
|
+
imageContainerProps,
|
|
32
|
+
imageProps,
|
|
33
|
+
...props
|
|
34
|
+
},
|
|
35
|
+
ref
|
|
36
|
+
) {
|
|
37
|
+
const [iconSrc, setIconSrc] = useState(icon);
|
|
38
|
+
const [imageSrc, setImageSrc] = useState(image);
|
|
39
|
+
const handleOnLoad = useCallback(
|
|
40
|
+
(event) => {
|
|
41
|
+
imageProps?.onLoad?.(event);
|
|
42
|
+
onReady();
|
|
43
|
+
},
|
|
44
|
+
[onReady, imageProps]
|
|
45
|
+
);
|
|
46
|
+
const handleIconOnError = useCallback(
|
|
47
|
+
(event) => {
|
|
48
|
+
iconProps?.onError?.(event);
|
|
49
|
+
setIconSrc(undefined);
|
|
50
|
+
},
|
|
51
|
+
[iconProps]
|
|
52
|
+
);
|
|
53
|
+
const handleImageOnError = useCallback(
|
|
54
|
+
(event) => {
|
|
55
|
+
imageProps?.onError?.(event);
|
|
56
|
+
setImageSrc(undefined);
|
|
57
|
+
},
|
|
58
|
+
[imageProps]
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<a
|
|
63
|
+
ref={ref}
|
|
64
|
+
target="_blank"
|
|
65
|
+
rel="noopener noreferrer"
|
|
66
|
+
{...props}
|
|
67
|
+
className={cn(
|
|
68
|
+
'transition-color duration-300 ease-out',
|
|
69
|
+
'grid items-start gap-2 p-2 mt-1 overflow-hidden rounded border-l-4 border-l-ring bg-white hover:bg-ring/20',
|
|
70
|
+
thin ? 'grid-cols-3' : 'grid-cols-1',
|
|
71
|
+
props?.className
|
|
72
|
+
)}
|
|
73
|
+
>
|
|
74
|
+
<div
|
|
75
|
+
{...titleContainerProps}
|
|
76
|
+
className={cn('flex items-start gap-2', thin ? 'col-span-3' : 'col-span-1', titleContainerProps?.className)}
|
|
77
|
+
>
|
|
78
|
+
{iconSrc ? (
|
|
79
|
+
<img
|
|
80
|
+
alt="icon"
|
|
81
|
+
src={iconSrc}
|
|
82
|
+
{...iconProps}
|
|
83
|
+
onError={handleIconOnError}
|
|
84
|
+
className={cn('size-5', iconProps?.className)}
|
|
85
|
+
/>
|
|
86
|
+
) : (
|
|
87
|
+
iconFallbackElement || <Globe {...iconFallbackProps} className={cn('size-5', iconFallbackProps?.className)} />
|
|
88
|
+
)}
|
|
89
|
+
<span
|
|
90
|
+
{...titleProps}
|
|
91
|
+
className={cn('text-sm font-bold', thin ? 'line-clamp-1' : 'line-clamp-2', titleProps?.className)}
|
|
92
|
+
>
|
|
93
|
+
{decodeHtmlEntities(title)}
|
|
94
|
+
</span>
|
|
95
|
+
</div>
|
|
96
|
+
{description && (
|
|
97
|
+
<span
|
|
98
|
+
{...descriptionProps}
|
|
99
|
+
className={cn(
|
|
100
|
+
'text-sm line-clamp-5',
|
|
101
|
+
thin ? (imageSrc ? 'col-span-2' : 'col-span-3') : 'col-span-1',
|
|
102
|
+
descriptionProps?.className
|
|
103
|
+
)}
|
|
104
|
+
>
|
|
105
|
+
{decodeHtmlEntities(description)}
|
|
106
|
+
</span>
|
|
107
|
+
)}
|
|
108
|
+
{imageSrc && (
|
|
109
|
+
<div
|
|
110
|
+
{...imageContainerProps}
|
|
111
|
+
className={cn('flex items-center justify-center', imageContainerProps?.className)}
|
|
112
|
+
>
|
|
113
|
+
<img
|
|
114
|
+
alt="banner"
|
|
115
|
+
src={imageSrc}
|
|
116
|
+
{...imageProps}
|
|
117
|
+
onLoad={handleOnLoad}
|
|
118
|
+
onError={handleImageOnError}
|
|
119
|
+
className={cn('rounded max-w-full object-contain', thin ? 'max-h-25' : 'max-h-60', imageProps?.className)}
|
|
120
|
+
/>
|
|
121
|
+
</div>
|
|
122
|
+
)}
|
|
123
|
+
</a>
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const LinkPreview = memo(forwardRef(LinkPreviewBase));
|
|
128
|
+
|
|
129
|
+
LinkPreview.displayName = 'LinkPreview';
|
|
130
|
+
|
|
131
|
+
export { LinkPreview };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { forwardRef, memo, useMemo } from 'react';
|
|
2
|
+
import Linkify from 'linkify-react';
|
|
3
|
+
import { cn } from './utils';
|
|
4
|
+
|
|
5
|
+
const DEFAULT_LINKIFY_OPTIONS = {
|
|
6
|
+
target: '_blank',
|
|
7
|
+
rel: 'noopener noreferrer',
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
function LinkifyTextBase({ text, linkifyProps, ...props }, ref) {
|
|
11
|
+
const options = useMemo(
|
|
12
|
+
() => ({
|
|
13
|
+
...DEFAULT_LINKIFY_OPTIONS,
|
|
14
|
+
...linkifyProps,
|
|
15
|
+
className: cn('text-blue-500 hover:text-blue-600 hover:underline', linkifyProps?.className),
|
|
16
|
+
}),
|
|
17
|
+
[linkifyProps]
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<p ref={ref} {...props} className={cn('wrap-break-word text-base', props?.className)}>
|
|
22
|
+
<Linkify options={options}>{text}</Linkify>
|
|
23
|
+
</p>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const LinkifyText = memo(forwardRef(LinkifyTextBase));
|
|
28
|
+
|
|
29
|
+
LinkifyText.displayName = 'LinkifyText';
|
|
30
|
+
|
|
31
|
+
export { LinkifyText };
|
|
@@ -1,10 +1,34 @@
|
|
|
1
|
-
import { LoaderCircle } from 'lucide-react';
|
|
1
|
+
import { Loader, LoaderCircle } from 'lucide-react';
|
|
2
2
|
import { cn } from './utils';
|
|
3
3
|
|
|
4
|
-
function Spinner({ loading =
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
)
|
|
4
|
+
function Spinner({ loading = false, layout = 'flow', type = 'default', ...props }) {
|
|
5
|
+
const LoaderIcon = type === 'circle' ? LoaderCircle : Loader;
|
|
6
|
+
|
|
7
|
+
if (!loading) {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const spinnerElement = (
|
|
12
|
+
<LoaderIcon
|
|
13
|
+
strokeWidth={2.5}
|
|
14
|
+
{...props}
|
|
15
|
+
className={cn('animate-spin text-ring', layout === 'flow' && 'mx-auto', props?.className)}
|
|
16
|
+
/>
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
switch (layout) {
|
|
20
|
+
case 'absolute':
|
|
21
|
+
return (
|
|
22
|
+
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2">{spinnerElement}</div>
|
|
23
|
+
);
|
|
24
|
+
case 'centered':
|
|
25
|
+
return <div className="flex items-center justify-center size-full">{spinnerElement}</div>;
|
|
26
|
+
case 'overlay':
|
|
27
|
+
return <div className="flex items-center justify-center size-full absolute bg-muted/50">{spinnerElement}</div>;
|
|
28
|
+
case 'flow':
|
|
29
|
+
default:
|
|
30
|
+
return spinnerElement;
|
|
31
|
+
}
|
|
8
32
|
}
|
|
9
33
|
|
|
10
34
|
Spinner.displayName = 'Spinner';
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Ban, Check, CheckCheck, Clock } from 'lucide-react';
|
|
2
|
+
import { cn } from './utils';
|
|
3
|
+
|
|
4
|
+
const StatusSent = ({ status, ...props }) => {
|
|
5
|
+
switch (status) {
|
|
6
|
+
case 'wait':
|
|
7
|
+
return <Clock {...props} className={cn('text-gray-500 size-4', props?.className)} />;
|
|
8
|
+
case 'sent':
|
|
9
|
+
return <Check {...props} className={cn('text-gray-500 size-4', props?.className)} />;
|
|
10
|
+
case 'read':
|
|
11
|
+
return <CheckCheck {...props} className={cn('text-gray-500 size-4', props?.className)} />;
|
|
12
|
+
case 'lost':
|
|
13
|
+
return <Ban {...props} className={cn('text-red-500 size-4', props?.className)} />;
|
|
14
|
+
default:
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
StatusSent.displayName = 'StatusSent';
|
|
20
|
+
|
|
21
|
+
export { StatusSent };
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
|
|
2
2
|
import { Maximize, Minimize, PictureInPicture2 } from 'lucide-react';
|
|
3
|
-
import { cn
|
|
3
|
+
import { cn } from './utils';
|
|
4
|
+
|
|
5
|
+
function getRandomString(length = 8) {
|
|
6
|
+
return (Date.now() / Math.random()).toString(36).replace('.', '').slice(0, length);
|
|
7
|
+
}
|
|
4
8
|
|
|
5
9
|
function StreamViewBase({ id, stream, mirror, className, muted, ...props }, ref) {
|
|
6
10
|
const innerRef = useRef(null);
|
package/gen/components/utils.js
CHANGED
|
@@ -5,17 +5,6 @@ export function cn(...inputs) {
|
|
|
5
5
|
return twMerge(clsx(inputs));
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
-
export function getRandomString(length = 8) {
|
|
9
|
-
return (Date.now() / Math.random()).toString(36).replace('.', '').slice(0, length);
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function getInitialsFromName(name) {
|
|
13
|
-
const words = name?.trim().split(/\s+/).filter(Boolean) ?? [];
|
|
14
|
-
const result = words.length > 1 ? `${words[0]?.[0]}${words[1]?.[0]}` : (words[0]?.slice(0, 2) ?? 'NA');
|
|
15
|
-
|
|
16
|
-
return result.toUpperCase();
|
|
17
|
-
}
|
|
18
|
-
|
|
19
8
|
export function capitalize(str) {
|
|
20
9
|
return typeof str === 'string' && str.length > 0 ? `${str[0]?.toUpperCase()}${str.slice(1)}` : '';
|
|
21
10
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@connectycube/react-ui-kit",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.18",
|
|
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,12 +64,15 @@
|
|
|
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-label": "^2.1.8",
|
|
67
68
|
"@radix-ui/react-slot": "^1.2.4",
|
|
68
69
|
"@radix-ui/react-tooltip": "^1.2.8",
|
|
69
70
|
"class-variance-authority": "^0.7.1",
|
|
70
71
|
"clsx": "^2.1.1",
|
|
72
|
+
"date-fns": "^4.1.0",
|
|
71
73
|
"execa": "^9.6.0",
|
|
72
74
|
"fs-extra": "^11.3.2",
|
|
75
|
+
"linkify-react": "^4.3.2",
|
|
73
76
|
"lucide-react": "^0.555.0",
|
|
74
77
|
"prompts": "^2.4.2",
|
|
75
78
|
"tailwind-merge": "^3.4.0"
|
|
@@ -2,11 +2,11 @@ import type React from 'react';
|
|
|
2
2
|
import { memo, forwardRef } from 'react';
|
|
3
3
|
import * as AvatarPrimitive from '@radix-ui/react-avatar';
|
|
4
4
|
import { PresenceBadge, type PresenceStatus, type PresenceBadgeProps } from './presence';
|
|
5
|
-
import { cn
|
|
5
|
+
import { cn } from './utils';
|
|
6
6
|
|
|
7
7
|
interface AvatarProps extends AvatarPrimitive.AvatarProps {
|
|
8
|
-
src?: string;
|
|
9
|
-
name?: string;
|
|
8
|
+
src?: string | undefined;
|
|
9
|
+
name?: string | undefined;
|
|
10
10
|
online?: boolean;
|
|
11
11
|
presence?: PresenceStatus;
|
|
12
12
|
onlineProps?: React.ComponentProps<'div'>;
|
|
@@ -15,6 +15,13 @@ interface AvatarProps extends AvatarPrimitive.AvatarProps {
|
|
|
15
15
|
fallbackProps?: AvatarPrimitive.AvatarFallbackProps;
|
|
16
16
|
}
|
|
17
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
|
+
|
|
18
25
|
function AvatarBase(
|
|
19
26
|
{
|
|
20
27
|
src,
|
|
@@ -48,13 +55,16 @@ function AvatarBase(
|
|
|
48
55
|
{online && (
|
|
49
56
|
<div
|
|
50
57
|
{...onlineProps}
|
|
51
|
-
className={cn(
|
|
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
|
+
)}
|
|
52
62
|
/>
|
|
53
63
|
)}
|
|
54
64
|
<PresenceBadge
|
|
55
65
|
status={presence}
|
|
56
66
|
{...presenceProps}
|
|
57
|
-
className={cn('absolute
|
|
67
|
+
className={cn('absolute bottom-0 right-0', presenceProps?.className)}
|
|
58
68
|
/>
|
|
59
69
|
</AvatarPrimitive.Root>
|
|
60
70
|
);
|
package/src/components/badge.tsx
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type React from 'react';
|
|
2
2
|
import { forwardRef } from 'react';
|
|
3
|
-
import
|
|
3
|
+
import { Slot } from '@radix-ui/react-slot';
|
|
4
4
|
import { cva, type VariantProps } from 'class-variance-authority';
|
|
5
5
|
import { cn } from './utils';
|
|
6
6
|
|
|
7
|
-
interface BadgeProps extends React.HTMLAttributes<
|
|
7
|
+
interface BadgeProps extends React.HTMLAttributes<HTMLElement>, VariantProps<typeof badgeVariants> {
|
|
8
8
|
asChild?: boolean;
|
|
9
9
|
}
|
|
10
10
|
|
|
@@ -28,14 +28,14 @@ const badgeVariants = cva(
|
|
|
28
28
|
|
|
29
29
|
function BadgeBase(
|
|
30
30
|
{ className, variant, asChild = false, ...props }: BadgeProps,
|
|
31
|
-
ref?: React.ForwardedRef<
|
|
31
|
+
ref?: React.ForwardedRef<HTMLElement>
|
|
32
32
|
) {
|
|
33
|
-
const Comp = asChild ?
|
|
33
|
+
const Comp = asChild ? Slot : 'span';
|
|
34
34
|
|
|
35
|
-
return <Comp ref={ref} className={cn(badgeVariants({ variant }), className)}
|
|
35
|
+
return <Comp ref={ref} {...props} className={cn(badgeVariants({ variant }), className)} />;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
const Badge = forwardRef<
|
|
38
|
+
const Badge = forwardRef<HTMLElement, BadgeProps>(BadgeBase);
|
|
39
39
|
|
|
40
40
|
Badge.displayName = 'Badge';
|
|
41
41
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type React from 'react';
|
|
2
2
|
import { forwardRef } from 'react';
|
|
3
|
-
import
|
|
3
|
+
import { Slot } from '@radix-ui/react-slot';
|
|
4
4
|
import { cva, type VariantProps } from 'class-variance-authority';
|
|
5
5
|
import { cn } from './utils';
|
|
6
6
|
|
|
@@ -40,7 +40,7 @@ function ButtonBase(
|
|
|
40
40
|
{ asChild = false, variant, size, className, ...props }: ButtonProps,
|
|
41
41
|
ref?: React.ForwardedRef<HTMLButtonElement>
|
|
42
42
|
) {
|
|
43
|
-
const Comp = asChild ?
|
|
43
|
+
const Comp = asChild ? Slot : 'button';
|
|
44
44
|
|
|
45
45
|
return (
|
|
46
46
|
<Comp
|