@connectycube/react-ui-kit 0.0.8 → 0.0.11
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/commands/add.js +72 -45
- package/configs/dependencies.json +16 -1
- package/configs/imports.json +8 -0
- package/gen/components/animated-loader.jsx +10 -0
- package/gen/components/avatar.jsx +34 -0
- package/gen/components/dismiss-layer.jsx +57 -0
- package/gen/components/placeholder-text.jsx +22 -0
- package/gen/components/presence.jsx +41 -0
- package/gen/components/stream-view.jsx +166 -28
- package/gen/components/utils.js +20 -0
- package/gen/index.js +3 -1
- package/package.json +6 -5
- package/src/components/animated-loader.tsx +15 -0
- package/src/components/avatar.tsx +45 -0
- package/src/components/connectycube-ui/animated-loader.jsx +10 -0
- package/src/components/connectycube-ui/animated-loader.tsx +15 -0
- package/src/components/connectycube-ui/avatar.jsx +34 -0
- package/src/components/connectycube-ui/avatar.tsx +45 -0
- package/src/components/connectycube-ui/dismiss-layer.jsx +57 -0
- package/src/components/connectycube-ui/dismiss-layer.tsx +74 -0
- package/src/components/connectycube-ui/placeholder-text.jsx +22 -0
- package/src/components/connectycube-ui/placeholder-text.tsx +28 -0
- package/src/components/connectycube-ui/presence.jsx +41 -0
- package/src/components/connectycube-ui/presence.tsx +55 -0
- package/src/components/connectycube-ui/stream-view.jsx +166 -28
- package/src/components/connectycube-ui/stream-view.tsx +231 -62
- package/src/components/connectycube-ui/utils.js +20 -0
- package/src/components/connectycube-ui/utils.ts +18 -0
- package/src/components/dismiss-layer.tsx +74 -0
- package/src/components/placeholder-text.tsx +28 -0
- package/src/components/presence.tsx +55 -0
- package/src/components/stream-view.tsx +231 -62
- package/src/components/utils.ts +18 -0
- package/src/index.ts +6 -2
|
@@ -1,77 +1,246 @@
|
|
|
1
1
|
import type React from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import type { LucideProps } from 'lucide-react';
|
|
3
|
+
import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
|
|
4
|
+
import { Maximize, Minimize, PictureInPicture2 } from 'lucide-react';
|
|
3
5
|
import { cn, getRandomString } from './utils';
|
|
4
6
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
stream?: MediaStream;
|
|
7
|
+
interface StreamViewProps extends React.ComponentProps<'video'> {
|
|
8
|
+
stream?: MediaStream | null;
|
|
8
9
|
mirror?: boolean;
|
|
9
|
-
}
|
|
10
|
+
}
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
id:
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
function StreamViewBase(
|
|
13
|
+
{ id, stream, mirror, className, muted, ...props }: StreamViewProps,
|
|
14
|
+
ref: React.Ref<HTMLVideoElement>
|
|
15
|
+
) {
|
|
16
|
+
const innerRef = useRef<HTMLVideoElement>(null);
|
|
17
|
+
const elementId = useMemo(() => id ?? `stream-${getRandomString()}`, [id]);
|
|
18
|
+
const isMuted = typeof muted === 'boolean' ? muted : false;
|
|
19
|
+
const defaultClassName = 'size-full object-contain';
|
|
20
|
+
const mirrorClassName = mirror ? 'scale-x-[-1]' : '';
|
|
15
21
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
muted={isMuted}
|
|
57
|
-
autoPlay
|
|
58
|
-
playsInline
|
|
59
|
-
{...props}
|
|
60
|
-
/>
|
|
61
|
-
) : null;
|
|
62
|
-
}
|
|
63
|
-
);
|
|
64
|
-
|
|
65
|
-
export const LocalStreamView = forwardRef<StreamViewRef, StreamViewProps>(({ muted, mirror, ...props }, ref) => {
|
|
22
|
+
useImperativeHandle(ref, () => innerRef.current!);
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
if (innerRef.current && stream) {
|
|
26
|
+
innerRef.current.srcObject = stream;
|
|
27
|
+
|
|
28
|
+
const playVideo = () => {
|
|
29
|
+
try {
|
|
30
|
+
innerRef.current?.play();
|
|
31
|
+
} catch (error) {
|
|
32
|
+
console.error('Error playing video:', error);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
innerRef.current.onloadedmetadata = () => {
|
|
37
|
+
queueMicrotask(playVideo);
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
}, [stream]);
|
|
41
|
+
|
|
42
|
+
if (!stream) return null;
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<video
|
|
46
|
+
ref={innerRef}
|
|
47
|
+
id={elementId}
|
|
48
|
+
autoPlay
|
|
49
|
+
playsInline
|
|
50
|
+
muted={isMuted}
|
|
51
|
+
className={cn(defaultClassName, mirrorClassName, className)}
|
|
52
|
+
{...props}
|
|
53
|
+
/>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const StreamView = forwardRef<HTMLVideoElement, StreamViewProps>(StreamViewBase);
|
|
58
|
+
|
|
59
|
+
StreamView.displayName = 'StreamView';
|
|
60
|
+
|
|
61
|
+
function LocalStreamViewBase({ muted, mirror, ...props }: StreamViewProps, ref: React.Ref<HTMLVideoElement>) {
|
|
66
62
|
const isMuted = typeof muted === 'boolean' ? muted : true;
|
|
67
63
|
const isMirror = typeof mirror === 'boolean' ? mirror : true;
|
|
68
64
|
|
|
69
65
|
return <StreamView ref={ref} muted={isMuted} mirror={isMirror} {...props} />;
|
|
70
|
-
}
|
|
66
|
+
}
|
|
71
67
|
|
|
72
|
-
|
|
68
|
+
const LocalStreamView = forwardRef<HTMLVideoElement, StreamViewProps>(LocalStreamViewBase);
|
|
69
|
+
|
|
70
|
+
LocalStreamView.displayName = 'LocalStreamView';
|
|
71
|
+
|
|
72
|
+
function RemoteStreamViewBase({ muted, mirror, ...props }: StreamViewProps, ref: React.Ref<HTMLVideoElement>) {
|
|
73
73
|
const isMuted = typeof muted === 'boolean' ? muted : false;
|
|
74
74
|
const isMirror = typeof mirror === 'boolean' ? mirror : false;
|
|
75
75
|
|
|
76
76
|
return <StreamView ref={ref} muted={isMuted} mirror={isMirror} {...props} />;
|
|
77
|
-
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const RemoteStreamView = forwardRef<HTMLVideoElement, StreamViewProps>(RemoteStreamViewBase);
|
|
80
|
+
|
|
81
|
+
RemoteStreamView.displayName = 'RemoteStreamView';
|
|
82
|
+
|
|
83
|
+
interface FullscreenStreamViewProps extends React.ComponentProps<'div'> {
|
|
84
|
+
element: React.ReactElement<StreamViewProps>;
|
|
85
|
+
pipElement?: React.ReactElement<StreamViewProps>;
|
|
86
|
+
navElement?: React.ReactElement<HTMLElement>;
|
|
87
|
+
hideIconElement?: React.ReactElement;
|
|
88
|
+
showIconElement?: React.ReactElement;
|
|
89
|
+
containerClassName?: string;
|
|
90
|
+
fullscreenButtonClassName?: string;
|
|
91
|
+
fullscreenButtonIconClassName?: string;
|
|
92
|
+
pipContainerClassName?: string;
|
|
93
|
+
pipButtonClassName?: string;
|
|
94
|
+
pipButtonIconClassName?: string;
|
|
95
|
+
containerProps?: React.ComponentProps<'div'>;
|
|
96
|
+
fullscreenButtonProps?: React.ComponentProps<'button'>;
|
|
97
|
+
fullscreenButtonIconProps?: LucideProps;
|
|
98
|
+
pipContainerProps?: React.ComponentProps<'div'>;
|
|
99
|
+
pipButtonProps?: React.ComponentProps<'button'>;
|
|
100
|
+
pipButtonIconProps?: LucideProps;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
interface FullscreenStreamViewRef extends React.ComponentRef<'div'> {
|
|
104
|
+
isFullscreen: boolean;
|
|
105
|
+
isPictureInPicture: boolean;
|
|
106
|
+
toggleFullscreen: () => Promise<void>;
|
|
107
|
+
togglePictureInPicture: () => void;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function FullscreenStreamViewBase(
|
|
111
|
+
{
|
|
112
|
+
element,
|
|
113
|
+
pipElement,
|
|
114
|
+
navElement,
|
|
115
|
+
hideIconElement,
|
|
116
|
+
showIconElement,
|
|
117
|
+
containerClassName,
|
|
118
|
+
fullscreenButtonClassName,
|
|
119
|
+
fullscreenButtonIconClassName,
|
|
120
|
+
pipContainerClassName,
|
|
121
|
+
pipButtonClassName,
|
|
122
|
+
pipButtonIconClassName,
|
|
123
|
+
containerProps,
|
|
124
|
+
fullscreenButtonProps,
|
|
125
|
+
fullscreenButtonIconProps,
|
|
126
|
+
pipContainerProps,
|
|
127
|
+
pipButtonProps,
|
|
128
|
+
pipButtonIconProps,
|
|
129
|
+
}: FullscreenStreamViewProps,
|
|
130
|
+
ref: React.Ref<FullscreenStreamViewRef>
|
|
131
|
+
) {
|
|
132
|
+
const innerRef = useRef<HTMLDivElement>(null);
|
|
133
|
+
const [isFullscreen, setIsFullscreen] = useState<boolean>(false);
|
|
134
|
+
const [isPictureInPicture, setIsPictureInPicture] = useState<boolean>(false);
|
|
135
|
+
const toggleFullscreen = useCallback(async () => {
|
|
136
|
+
const container = innerRef.current;
|
|
137
|
+
|
|
138
|
+
if (!container) return;
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
if (!document.fullscreenElement) {
|
|
142
|
+
await container.requestFullscreen();
|
|
143
|
+
setIsFullscreen(true);
|
|
144
|
+
setIsPictureInPicture(true);
|
|
145
|
+
} else {
|
|
146
|
+
await document.exitFullscreen();
|
|
147
|
+
setIsFullscreen(false);
|
|
148
|
+
setIsPictureInPicture(false);
|
|
149
|
+
}
|
|
150
|
+
} catch (err) {
|
|
151
|
+
console.error('Fullscreen error:', err);
|
|
152
|
+
}
|
|
153
|
+
}, []);
|
|
154
|
+
const togglePictureInPicture = useCallback(() => {
|
|
155
|
+
if (pipElement) {
|
|
156
|
+
setIsPictureInPicture((prevState) => !prevState);
|
|
157
|
+
}
|
|
158
|
+
}, [pipElement]);
|
|
159
|
+
|
|
160
|
+
useImperativeHandle(
|
|
161
|
+
ref,
|
|
162
|
+
() =>
|
|
163
|
+
Object.assign(innerRef.current!, {
|
|
164
|
+
isFullscreen,
|
|
165
|
+
isPictureInPicture,
|
|
166
|
+
toggleFullscreen,
|
|
167
|
+
togglePictureInPicture,
|
|
168
|
+
}),
|
|
169
|
+
[isFullscreen, isPictureInPicture, toggleFullscreen, togglePictureInPicture]
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
useEffect(() => {
|
|
173
|
+
const onFullscreenChange = () => {
|
|
174
|
+
setIsFullscreen(!!document.fullscreenElement);
|
|
175
|
+
setIsPictureInPicture(!!document.fullscreenElement);
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
document.addEventListener('fullscreenchange', onFullscreenChange);
|
|
179
|
+
|
|
180
|
+
return () => document.removeEventListener('fullscreenchange', onFullscreenChange);
|
|
181
|
+
}, []);
|
|
182
|
+
|
|
183
|
+
return (
|
|
184
|
+
<div
|
|
185
|
+
ref={innerRef}
|
|
186
|
+
className={cn('relative flex items-center justify-center size-full', containerClassName)}
|
|
187
|
+
{...containerProps}
|
|
188
|
+
>
|
|
189
|
+
{element}
|
|
190
|
+
<button
|
|
191
|
+
onClick={toggleFullscreen}
|
|
192
|
+
className={cn(
|
|
193
|
+
'absolute top-2 right-2 p-1 rounded-md bg-black/50 text-white hover:bg-black/80 z-10 shadow-xs shadow-white/25',
|
|
194
|
+
fullscreenButtonClassName
|
|
195
|
+
)}
|
|
196
|
+
{...fullscreenButtonProps}
|
|
197
|
+
>
|
|
198
|
+
{isFullscreen
|
|
199
|
+
? hideIconElement || <Minimize className={fullscreenButtonIconClassName} {...fullscreenButtonIconProps} />
|
|
200
|
+
: showIconElement || <Maximize className={fullscreenButtonIconClassName} {...fullscreenButtonIconProps} />}
|
|
201
|
+
</button>
|
|
202
|
+
<div className="absolute size-full p-2 flex flex-col justify-end items-center">
|
|
203
|
+
{isFullscreen && pipElement && (
|
|
204
|
+
<div className="relative size-full flex items-end justify-end">
|
|
205
|
+
{isPictureInPicture && (
|
|
206
|
+
<div
|
|
207
|
+
className={cn(
|
|
208
|
+
'max-w-1/4 max-h-1/4 aspect-4/3 overflow-hidden rounded-md shadow-md shadow-white/25',
|
|
209
|
+
pipContainerClassName
|
|
210
|
+
)}
|
|
211
|
+
{...pipContainerProps}
|
|
212
|
+
>
|
|
213
|
+
{pipElement}
|
|
214
|
+
</div>
|
|
215
|
+
)}
|
|
216
|
+
<button
|
|
217
|
+
onClick={togglePictureInPicture}
|
|
218
|
+
className={cn(
|
|
219
|
+
'absolute bottom-2 right-2 p-1 rounded-md bg-black/50 text-white hover:bg-black/80 z-10 shadow-xs shadow-white/25',
|
|
220
|
+
pipButtonClassName
|
|
221
|
+
)}
|
|
222
|
+
{...pipButtonProps}
|
|
223
|
+
>
|
|
224
|
+
<PictureInPicture2 className={pipButtonIconClassName} {...pipButtonIconProps} />
|
|
225
|
+
</button>
|
|
226
|
+
</div>
|
|
227
|
+
)}
|
|
228
|
+
{isFullscreen && navElement}
|
|
229
|
+
</div>
|
|
230
|
+
</div>
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const FullscreenStreamView = forwardRef<FullscreenStreamViewRef, FullscreenStreamViewProps>(FullscreenStreamViewBase);
|
|
235
|
+
|
|
236
|
+
FullscreenStreamView.displayName = 'FullscreenStreamView';
|
|
237
|
+
|
|
238
|
+
export {
|
|
239
|
+
StreamView,
|
|
240
|
+
LocalStreamView,
|
|
241
|
+
RemoteStreamView,
|
|
242
|
+
FullscreenStreamView,
|
|
243
|
+
type StreamViewProps,
|
|
244
|
+
type FullscreenStreamViewProps,
|
|
245
|
+
type FullscreenStreamViewRef,
|
|
246
|
+
};
|
|
@@ -8,3 +8,23 @@ export function cn(...inputs) {
|
|
|
8
8
|
export function getRandomString(length = 8) {
|
|
9
9
|
return (Date.now() / Math.random()).toString(36).replace('.', '').slice(0, length);
|
|
10
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
|
+
export function capitalize(str) {
|
|
20
|
+
return typeof str === 'string' && str.length > 0 ? `${str[0]?.toUpperCase()}${str.slice(1)}` : '';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export let UserPresence = /*#__PURE__*/ (function (UserPresence) {
|
|
24
|
+
UserPresence['AVAILABLE'] = 'available';
|
|
25
|
+
UserPresence['BUSY'] = 'busy';
|
|
26
|
+
UserPresence['AWAY'] = 'away';
|
|
27
|
+
UserPresence['UNKNOWN'] = 'unknown';
|
|
28
|
+
|
|
29
|
+
return UserPresence;
|
|
30
|
+
})({});
|
|
@@ -8,3 +8,21 @@ export function cn(...inputs: ClassValue[]) {
|
|
|
8
8
|
export function getRandomString(length = 8): string {
|
|
9
9
|
return (Date.now() / Math.random()).toString(36).replace('.', '').slice(0, length);
|
|
10
10
|
}
|
|
11
|
+
|
|
12
|
+
export function getInitialsFromName(name?: string): string {
|
|
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
|
+
export function capitalize(str?: string): string {
|
|
20
|
+
return typeof str === 'string' && str.length > 0 ? `${str[0]?.toUpperCase()}${str.slice(1)}` : '';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export enum UserPresence {
|
|
24
|
+
AVAILABLE = 'available',
|
|
25
|
+
BUSY = 'busy',
|
|
26
|
+
AWAY = 'away',
|
|
27
|
+
UNKNOWN = 'unknown',
|
|
28
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type React from 'react';
|
|
2
|
+
import { useCallback, useEffect, useRef, useImperativeHandle, memo, forwardRef } from 'react';
|
|
3
|
+
import { cn } from './utils';
|
|
4
|
+
|
|
5
|
+
interface DismissLayerProps extends React.ComponentProps<'div'> {
|
|
6
|
+
active: boolean;
|
|
7
|
+
onDismiss: () => void;
|
|
8
|
+
disableClickOutside?: boolean;
|
|
9
|
+
disableEscKeyPress?: boolean;
|
|
10
|
+
disabled?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function DismissLayerBase(
|
|
14
|
+
{
|
|
15
|
+
active,
|
|
16
|
+
onDismiss,
|
|
17
|
+
disableClickOutside = false,
|
|
18
|
+
disableEscKeyPress = false,
|
|
19
|
+
disabled,
|
|
20
|
+
className,
|
|
21
|
+
...props
|
|
22
|
+
}: DismissLayerProps,
|
|
23
|
+
ref: React.Ref<HTMLDivElement>
|
|
24
|
+
) {
|
|
25
|
+
const innerRef = useRef<HTMLDivElement>(null);
|
|
26
|
+
|
|
27
|
+
useImperativeHandle(ref, () => innerRef.current!);
|
|
28
|
+
|
|
29
|
+
const handleClickOrTouch = useCallback(
|
|
30
|
+
(e: React.MouseEvent | React.TouchEvent) => {
|
|
31
|
+
if (!disableClickOutside && active && e.target === innerRef.current) {
|
|
32
|
+
onDismiss();
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
[disableClickOutside, active, onDismiss]
|
|
36
|
+
);
|
|
37
|
+
const handleKeyEvent = useCallback(
|
|
38
|
+
(ev: KeyboardEvent) => {
|
|
39
|
+
if (!disableEscKeyPress && active && ev.key === 'Escape') {
|
|
40
|
+
onDismiss();
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
[disableEscKeyPress, active, onDismiss]
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
if (!disableEscKeyPress && active) {
|
|
48
|
+
document.addEventListener('keydown', handleKeyEvent);
|
|
49
|
+
|
|
50
|
+
return () => document.removeEventListener('keydown', handleKeyEvent);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return;
|
|
54
|
+
}, [disableEscKeyPress, active, handleKeyEvent]);
|
|
55
|
+
|
|
56
|
+
if (disabled || !active) return null;
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<div
|
|
60
|
+
ref={innerRef}
|
|
61
|
+
onClick={handleClickOrTouch}
|
|
62
|
+
onTouchStart={handleClickOrTouch}
|
|
63
|
+
className={cn('fixed top-0 left-0 z-40 size-full bg-black/20', className)}
|
|
64
|
+
aria-hidden
|
|
65
|
+
{...props}
|
|
66
|
+
/>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const DismissLayer = memo(forwardRef<HTMLDivElement, DismissLayerProps>(DismissLayerBase));
|
|
71
|
+
|
|
72
|
+
DismissLayer.displayName = 'DismissLayer';
|
|
73
|
+
|
|
74
|
+
export { DismissLayer, type DismissLayerProps };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type React from 'react';
|
|
2
|
+
import { forwardRef, memo } from 'react';
|
|
3
|
+
import { cn } from './utils';
|
|
4
|
+
|
|
5
|
+
interface PlaceholderTextProps extends React.ComponentProps<'div'> {
|
|
6
|
+
title?: string;
|
|
7
|
+
titles?: string[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function PlaceholderTextBase({ title, titles = [], className }: PlaceholderTextProps, ref: React.Ref<HTMLDivElement>) {
|
|
11
|
+
const rows = typeof title === 'string' ? [title, ...titles] : titles;
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<div ref={ref} className={cn('absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2', className)}>
|
|
15
|
+
{rows.map((row, index) => (
|
|
16
|
+
<div key={`placeholder-text-${index}`} className="text-center">
|
|
17
|
+
{row}
|
|
18
|
+
</div>
|
|
19
|
+
))}
|
|
20
|
+
</div>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const PlaceholderText = memo(forwardRef(PlaceholderTextBase));
|
|
25
|
+
|
|
26
|
+
PlaceholderText.displayName = 'PlaceholderText';
|
|
27
|
+
|
|
28
|
+
export { PlaceholderText, type PlaceholderTextProps };
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type React from 'react';
|
|
2
|
+
import type { LucideProps } from 'lucide-react';
|
|
3
|
+
import { memo } from 'react';
|
|
4
|
+
import { CircleCheck, CircleMinus, CircleQuestionMark, Clock } from 'lucide-react';
|
|
5
|
+
import { capitalize, cn, UserPresence } from './utils';
|
|
6
|
+
|
|
7
|
+
type PresenceStatus = UserPresence | 'available' | 'busy' | 'away' | 'unknown' | undefined;
|
|
8
|
+
|
|
9
|
+
interface PresenceBadgeProps extends LucideProps {
|
|
10
|
+
status: PresenceStatus;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface PresenceProps extends React.ComponentProps<'div'> {
|
|
14
|
+
badge?: boolean;
|
|
15
|
+
status: PresenceStatus;
|
|
16
|
+
label: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function PresenceBadgeBase({ status, className, ...props }: PresenceBadgeProps) {
|
|
20
|
+
switch (status) {
|
|
21
|
+
case UserPresence.AVAILABLE || 'available':
|
|
22
|
+
return <CircleCheck className={cn('rounded-full text-white bg-green-600 size-4.5', className)} {...props} />;
|
|
23
|
+
case UserPresence.BUSY || 'busy':
|
|
24
|
+
return <CircleMinus className={cn('rounded-full text-white bg-red-600 size-4.5', className)} {...props} />;
|
|
25
|
+
case UserPresence.AWAY || 'away':
|
|
26
|
+
return <Clock className={cn('rounded-full text-white bg-yellow-500 size-4.5', className)} {...props} />;
|
|
27
|
+
case UserPresence.UNKNOWN || 'unknown':
|
|
28
|
+
return (
|
|
29
|
+
<CircleQuestionMark className={cn('rounded-full text-white bg-gray-500 size-4.5', className)} {...props} />
|
|
30
|
+
);
|
|
31
|
+
default:
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const PresenceBadge = memo(PresenceBadgeBase);
|
|
37
|
+
|
|
38
|
+
PresenceBadge.displayName = 'PresenceBadge';
|
|
39
|
+
|
|
40
|
+
function PresenceBase({ badge = true, status, label, className, ...props }: PresenceProps) {
|
|
41
|
+
const presence = capitalize(label || status);
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<div className={cn('flex items-center gap-2', className)} {...props}>
|
|
45
|
+
{badge && <PresenceBadge status={status} />}
|
|
46
|
+
<span>{presence}</span>
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const Presence = memo(PresenceBase);
|
|
52
|
+
|
|
53
|
+
Presence.displayName = 'Presence';
|
|
54
|
+
|
|
55
|
+
export { Presence, PresenceBadge, type PresenceStatus, type PresenceProps, type PresenceBadgeProps };
|