@connectycube/react-ui-kit 0.0.16 → 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 +32 -3
- package/configs/imports.json +13 -2
- package/dist/index.cjs +4 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +4 -4
- package/dist/index.js.map +1 -1
- package/dist/types/components/alert-dialog.d.ts.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 +11 -0
- package/dist/types/components/badge.d.ts.map +1 -0
- package/dist/types/components/button.d.ts +3 -5
- package/dist/types/components/button.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 +5 -0
- package/dist/types/components/input.d.ts.map +1 -0
- 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/search.d.ts +15 -0
- package/dist/types/components/search.d.ts.map +1 -0
- package/dist/types/components/spinner.d.ts +12 -0
- package/dist/types/components/spinner.d.ts.map +1 -0
- 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/alert-dialog.jsx +3 -1
- package/gen/components/avatar.jsx +13 -3
- package/gen/components/badge.jsx +45 -0
- package/gen/components/button.jsx +6 -9
- 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/input.jsx +23 -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/search.jsx +75 -0
- package/gen/components/spinner.jsx +36 -0
- 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 +5 -2
- package/src/components/alert-dialog.tsx +3 -1
- package/src/components/avatar.tsx +16 -6
- package/src/components/badge.tsx +42 -0
- package/src/components/button.tsx +12 -14
- 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 +26 -0
- 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/placeholder-text.tsx +1 -1
- package/src/components/search.tsx +86 -0
- package/src/components/spinner.tsx +42 -0
- package/src/components/status-sent.tsx +25 -0
- package/src/components/stream-view.tsx +9 -5
- package/src/components/utils.ts +0 -11
- package/dist/tsconfig.tsbuildinfo +0 -1
- package/dist/types/components/animated-loader.d.ts +0 -10
- package/dist/types/components/animated-loader.d.ts.map +0 -1
- package/gen/components/animated-loader.jsx +0 -12
- package/src/components/animated-loader.tsx +0 -16
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { forwardRef, memo } from 'react';
|
|
2
|
+
import { differenceInCalendarDays, format, formatDistanceToNow, isToday } from 'date-fns';
|
|
3
|
+
import { el, enUS, uk, type Locale } from 'date-fns/locale';
|
|
4
|
+
import { cn } from './utils';
|
|
5
|
+
|
|
6
|
+
interface FormattedDateProps extends React.ComponentProps<'span'> {
|
|
7
|
+
date?: Date | string | number | null;
|
|
8
|
+
language?: string;
|
|
9
|
+
distanceToNow?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const locales: Record<string, Locale> = {
|
|
13
|
+
en: enUS,
|
|
14
|
+
el: el,
|
|
15
|
+
ua: uk,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
function formatDate(date: string | number | Date, language: string = 'en', distanceToNow = false): string {
|
|
19
|
+
const locale = locales[language] ?? enUS;
|
|
20
|
+
|
|
21
|
+
if (distanceToNow) {
|
|
22
|
+
return formatDistanceToNow(date, { locale, addSuffix: true });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const isWithinLast7Days = (date: Date | string | number) => {
|
|
26
|
+
const diff = differenceInCalendarDays(new Date(), date);
|
|
27
|
+
|
|
28
|
+
return diff >= 0 && diff <= 6;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
return isToday(date)
|
|
32
|
+
? format(date, 'p', { locale })
|
|
33
|
+
: isWithinLast7Days(date)
|
|
34
|
+
? format(date, 'eeee', { locale })
|
|
35
|
+
: format(date, 'P', { locale });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function FormattedDateBase(
|
|
39
|
+
{ date, language, distanceToNow, ...props }: FormattedDateProps,
|
|
40
|
+
ref: React.ForwardedRef<HTMLSpanElement>
|
|
41
|
+
) {
|
|
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<HTMLSpanElement, FormattedDateProps>(FormattedDateBase));
|
|
54
|
+
|
|
55
|
+
FormattedDate.displayName = 'FormattedDate';
|
|
56
|
+
|
|
57
|
+
export { FormattedDate, type FormattedDateProps };
|
|
@@ -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,23 @@
|
|
|
1
|
+
import type React from 'react';
|
|
2
|
+
import { forwardRef } from 'react';
|
|
3
|
+
import { Root as LabelRoot, type LabelProps } from '@radix-ui/react-label';
|
|
4
|
+
import { cn } from './utils';
|
|
5
|
+
|
|
6
|
+
function LabelBase({ ...props }: LabelProps, ref: React.ForwardedRef<HTMLLabelElement>) {
|
|
7
|
+
return (
|
|
8
|
+
<LabelRoot
|
|
9
|
+
ref={ref}
|
|
10
|
+
{...props}
|
|
11
|
+
className={cn(
|
|
12
|
+
'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',
|
|
13
|
+
props?.className
|
|
14
|
+
)}
|
|
15
|
+
/>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const Label = forwardRef(LabelBase);
|
|
20
|
+
|
|
21
|
+
Label.displayName = 'Label';
|
|
22
|
+
|
|
23
|
+
export { Label, type LabelProps };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type React from 'react';
|
|
2
|
+
import type { Opts } from 'linkifyjs';
|
|
3
|
+
import { forwardRef, memo, useMemo } from 'react';
|
|
4
|
+
import Linkify from 'linkify-react';
|
|
5
|
+
import { cn } from './utils';
|
|
6
|
+
|
|
7
|
+
const DEFAULT_LINKIFY_OPTIONS: Opts = {
|
|
8
|
+
target: '_blank',
|
|
9
|
+
rel: 'noopener noreferrer',
|
|
10
|
+
className: 'text-blue-500 hover:underline hover:text-blue-600 transition-color duration-300',
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
type LinkifyTextProps = React.ComponentProps<'p'> & {
|
|
14
|
+
text: string;
|
|
15
|
+
linkifyProps?: Opts;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
function LinkifyTextBase(
|
|
19
|
+
{ text, linkifyProps, ...props }: LinkifyTextProps,
|
|
20
|
+
ref: React.ForwardedRef<HTMLParagraphElement>
|
|
21
|
+
) {
|
|
22
|
+
const options = useMemo(() => {
|
|
23
|
+
return {
|
|
24
|
+
...DEFAULT_LINKIFY_OPTIONS,
|
|
25
|
+
...linkifyProps,
|
|
26
|
+
};
|
|
27
|
+
}, [linkifyProps]);
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<p ref={ref} {...props} className={cn('wrap-break-word text-base', props?.className)}>
|
|
31
|
+
<Linkify options={options}>{text}</Linkify>
|
|
32
|
+
</p>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const LinkifyText = memo(forwardRef<HTMLParagraphElement, LinkifyTextProps>(LinkifyTextBase));
|
|
37
|
+
|
|
38
|
+
LinkifyText.displayName = 'LinkifyText';
|
|
39
|
+
|
|
40
|
+
export { LinkifyText, type LinkifyTextProps };
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { capitalize, cn } from './utils';
|
|
2
|
+
|
|
3
|
+
function PresenceBadge({ status, ...props }) {
|
|
4
|
+
switch (status) {
|
|
5
|
+
case 'available':
|
|
6
|
+
return (
|
|
7
|
+
<svg
|
|
8
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
9
|
+
width="16"
|
|
10
|
+
height="16"
|
|
11
|
+
fill="currentColor"
|
|
12
|
+
viewBox="0 0 16 16"
|
|
13
|
+
{...props}
|
|
14
|
+
className={cn('rounded-full size-4.5 text-green-600 bg-white', props?.className)}
|
|
15
|
+
>
|
|
16
|
+
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0m-3.97-3.03a.75.75 0 0 0-1.08.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-.01-1.05z" />
|
|
17
|
+
</svg>
|
|
18
|
+
);
|
|
19
|
+
case 'busy':
|
|
20
|
+
return (
|
|
21
|
+
<svg
|
|
22
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
23
|
+
width="16"
|
|
24
|
+
height="16"
|
|
25
|
+
fill="currentColor"
|
|
26
|
+
viewBox="0 0 16 16"
|
|
27
|
+
{...props}
|
|
28
|
+
className={cn('rounded-full size-4.5 text-red-600 bg-white', props?.className)}
|
|
29
|
+
>
|
|
30
|
+
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0M4.5 7.5a.5.5 0 0 0 0 1h7a.5.5 0 0 0 0-1z" />
|
|
31
|
+
</svg>
|
|
32
|
+
);
|
|
33
|
+
case 'away':
|
|
34
|
+
return (
|
|
35
|
+
<svg
|
|
36
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
37
|
+
width="16"
|
|
38
|
+
height="16"
|
|
39
|
+
fill="currentColor"
|
|
40
|
+
viewBox="0 0 16 16"
|
|
41
|
+
{...props}
|
|
42
|
+
className={cn('rounded-full size-4.5 text-yellow-500 bg-white', props?.className)}
|
|
43
|
+
>
|
|
44
|
+
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0M8 3.5a.5.5 0 0 0-1 0V9a.5.5 0 0 0 .252.434l3.5 2a.5.5 0 0 0 .496-.868L8 8.71z" />
|
|
45
|
+
</svg>
|
|
46
|
+
);
|
|
47
|
+
case 'unknown':
|
|
48
|
+
return (
|
|
49
|
+
<svg
|
|
50
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
51
|
+
width="16"
|
|
52
|
+
height="16"
|
|
53
|
+
fill="currentColor"
|
|
54
|
+
viewBox="0 0 16 16"
|
|
55
|
+
{...props}
|
|
56
|
+
className={cn('rounded-full size-4.5 text-gray-500 bg-white', props?.className)}
|
|
57
|
+
>
|
|
58
|
+
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0M5.496 6.033h.825c.138 0 .248-.113.266-.25.09-.656.54-1.134 1.342-1.134.686 0 1.314.343 1.314 1.168 0 .635-.374.927-.965 1.371-.673.489-1.206 1.06-1.168 1.987l.003.217a.25.25 0 0 0 .25.246h.811a.25.25 0 0 0 .25-.25v-.105c0-.718.273-.927 1.01-1.486.609-.463 1.244-.977 1.244-2.056 0-1.511-1.276-2.241-2.673-2.241-1.267 0-2.655.59-2.75 2.286a.237.237 0 0 0 .241.247m2.325 6.443c.61 0 1.029-.394 1.029-.927 0-.552-.42-.94-1.029-.94-.584 0-1.009.388-1.009.94 0 .533.425.927 1.01.927z" />
|
|
59
|
+
</svg>
|
|
60
|
+
);
|
|
61
|
+
default:
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
PresenceBadge.displayName = 'PresenceBadge';
|
|
67
|
+
|
|
68
|
+
function Presence({ badge = true, status, label, badgeProps, labelProps, ...props }) {
|
|
69
|
+
const presence = capitalize(label || status);
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<div {...props} className={cn('flex items-center gap-2', props?.className)}>
|
|
73
|
+
{badge && <PresenceBadge status={status} {...badgeProps} />}
|
|
74
|
+
<span {...labelProps}>{presence}</span>
|
|
75
|
+
</div>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
Presence.displayName = 'Presence';
|
|
80
|
+
|
|
81
|
+
export { Presence, PresenceBadge };
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type React from 'react';
|
|
2
|
+
import { capitalize, cn } from './utils';
|
|
3
|
+
|
|
4
|
+
type PresenceStatus = 'available' | 'busy' | 'away' | 'unknown' | undefined;
|
|
5
|
+
|
|
6
|
+
interface PresenceProps extends React.ComponentProps<'div'> {
|
|
7
|
+
badge?: boolean;
|
|
8
|
+
status: PresenceStatus;
|
|
9
|
+
label: string;
|
|
10
|
+
badgeProps?: PresenceBadgeProps;
|
|
11
|
+
labelProps?: React.ComponentProps<'span'>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface PresenceBadgeProps extends React.ComponentProps<'svg'> {
|
|
15
|
+
status?: PresenceStatus;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function PresenceBadge({ status, ...props }: PresenceBadgeProps) {
|
|
19
|
+
switch (status) {
|
|
20
|
+
case 'available':
|
|
21
|
+
return (
|
|
22
|
+
<svg
|
|
23
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
24
|
+
width="16"
|
|
25
|
+
height="16"
|
|
26
|
+
fill="currentColor"
|
|
27
|
+
viewBox="0 0 16 16"
|
|
28
|
+
{...props}
|
|
29
|
+
className={cn('rounded-full size-4.5 text-green-600 bg-white', props?.className)}
|
|
30
|
+
>
|
|
31
|
+
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0m-3.97-3.03a.75.75 0 0 0-1.08.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-.01-1.05z" />
|
|
32
|
+
</svg>
|
|
33
|
+
);
|
|
34
|
+
case 'busy':
|
|
35
|
+
return (
|
|
36
|
+
<svg
|
|
37
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
38
|
+
width="16"
|
|
39
|
+
height="16"
|
|
40
|
+
fill="currentColor"
|
|
41
|
+
viewBox="0 0 16 16"
|
|
42
|
+
{...props}
|
|
43
|
+
className={cn('rounded-full size-4.5 text-red-600 bg-white', props?.className)}
|
|
44
|
+
>
|
|
45
|
+
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0M4.5 7.5a.5.5 0 0 0 0 1h7a.5.5 0 0 0 0-1z" />
|
|
46
|
+
</svg>
|
|
47
|
+
);
|
|
48
|
+
case 'away':
|
|
49
|
+
return (
|
|
50
|
+
<svg
|
|
51
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
52
|
+
width="16"
|
|
53
|
+
height="16"
|
|
54
|
+
fill="currentColor"
|
|
55
|
+
viewBox="0 0 16 16"
|
|
56
|
+
{...props}
|
|
57
|
+
className={cn('rounded-full size-4.5 text-yellow-500 bg-white', props?.className)}
|
|
58
|
+
>
|
|
59
|
+
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0M8 3.5a.5.5 0 0 0-1 0V9a.5.5 0 0 0 .252.434l3.5 2a.5.5 0 0 0 .496-.868L8 8.71z" />
|
|
60
|
+
</svg>
|
|
61
|
+
);
|
|
62
|
+
case 'unknown':
|
|
63
|
+
return (
|
|
64
|
+
<svg
|
|
65
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
66
|
+
width="16"
|
|
67
|
+
height="16"
|
|
68
|
+
fill="currentColor"
|
|
69
|
+
viewBox="0 0 16 16"
|
|
70
|
+
{...props}
|
|
71
|
+
className={cn('rounded-full size-4.5 text-gray-500 bg-white', props?.className)}
|
|
72
|
+
>
|
|
73
|
+
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0M5.496 6.033h.825c.138 0 .248-.113.266-.25.09-.656.54-1.134 1.342-1.134.686 0 1.314.343 1.314 1.168 0 .635-.374.927-.965 1.371-.673.489-1.206 1.06-1.168 1.987l.003.217a.25.25 0 0 0 .25.246h.811a.25.25 0 0 0 .25-.25v-.105c0-.718.273-.927 1.01-1.486.609-.463 1.244-.977 1.244-2.056 0-1.511-1.276-2.241-2.673-2.241-1.267 0-2.655.59-2.75 2.286a.237.237 0 0 0 .241.247m2.325 6.443c.61 0 1.029-.394 1.029-.927 0-.552-.42-.94-1.029-.94-.584 0-1.009.388-1.009.94 0 .533.425.927 1.01.927z" />
|
|
74
|
+
</svg>
|
|
75
|
+
);
|
|
76
|
+
default:
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
PresenceBadge.displayName = 'PresenceBadge';
|
|
82
|
+
|
|
83
|
+
function Presence({ badge = true, status, label, badgeProps, labelProps, ...props }: PresenceProps) {
|
|
84
|
+
const presence = capitalize(label || status);
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<div {...props} className={cn('flex items-center gap-2', props?.className)}>
|
|
88
|
+
{badge && <PresenceBadge status={status} {...badgeProps} />}
|
|
89
|
+
<span {...labelProps}>{presence}</span>
|
|
90
|
+
</div>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
Presence.displayName = 'Presence';
|
|
95
|
+
|
|
96
|
+
export { Presence, PresenceBadge, type PresenceStatus, type PresenceProps, type PresenceBadgeProps };
|
|
@@ -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 };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Ban, Check, CheckCheck, Clock, type LucideProps } from 'lucide-react';
|
|
2
|
+
import { cn } from './utils';
|
|
3
|
+
|
|
4
|
+
interface StatusSentProps extends LucideProps {
|
|
5
|
+
status?: 'wait' | 'sent' | 'read' | 'lost' | null;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const StatusSent: React.FC<StatusSentProps> = ({ status, ...props }) => {
|
|
9
|
+
switch (status) {
|
|
10
|
+
case 'wait':
|
|
11
|
+
return <Clock {...props} className={cn('text-gray-500 size-4', props?.className)} />;
|
|
12
|
+
case 'sent':
|
|
13
|
+
return <Check {...props} className={cn('text-gray-500 size-4', props?.className)} />;
|
|
14
|
+
case 'read':
|
|
15
|
+
return <CheckCheck {...props} className={cn('text-gray-500 size-4', props?.className)} />;
|
|
16
|
+
case 'lost':
|
|
17
|
+
return <Ban {...props} className={cn('text-red-500 size-4', props?.className)} />;
|
|
18
|
+
default:
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
StatusSent.displayName = 'StatusSent';
|
|
24
|
+
|
|
25
|
+
export { StatusSent, type StatusSentProps };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { clsx } from 'clsx';
|
|
2
|
+
import { twMerge } from 'tailwind-merge';
|
|
3
|
+
|
|
4
|
+
export function cn(...inputs) {
|
|
5
|
+
return twMerge(clsx(inputs));
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function capitalize(str) {
|
|
9
|
+
return typeof str === 'string' && str.length > 0 ? `${str[0]?.toUpperCase()}${str.slice(1)}` : '';
|
|
10
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { clsx, type ClassValue } from 'clsx';
|
|
2
|
+
import { twMerge } from 'tailwind-merge';
|
|
3
|
+
|
|
4
|
+
export function cn(...inputs: ClassValue[]) {
|
|
5
|
+
return twMerge(clsx(inputs));
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function capitalize(str?: string): string {
|
|
9
|
+
return typeof str === 'string' && str.length > 0 ? `${str[0]?.toUpperCase()}${str.slice(1)}` : '';
|
|
10
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
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 || undefined;
|
|
89
|
+
const avatarFallback = name || avatarProps?.name || undefined;
|
|
90
|
+
const avatarOnline = isPrivateDialog ? userOnline || avatarProps?.online || false : 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 };
|