@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.
Files changed (34) hide show
  1. package/commands/add.js +72 -45
  2. package/configs/dependencies.json +16 -1
  3. package/configs/imports.json +8 -0
  4. package/gen/components/animated-loader.jsx +10 -0
  5. package/gen/components/avatar.jsx +34 -0
  6. package/gen/components/dismiss-layer.jsx +57 -0
  7. package/gen/components/placeholder-text.jsx +22 -0
  8. package/gen/components/presence.jsx +41 -0
  9. package/gen/components/stream-view.jsx +166 -28
  10. package/gen/components/utils.js +20 -0
  11. package/gen/index.js +3 -1
  12. package/package.json +6 -5
  13. package/src/components/animated-loader.tsx +15 -0
  14. package/src/components/avatar.tsx +45 -0
  15. package/src/components/connectycube-ui/animated-loader.jsx +10 -0
  16. package/src/components/connectycube-ui/animated-loader.tsx +15 -0
  17. package/src/components/connectycube-ui/avatar.jsx +34 -0
  18. package/src/components/connectycube-ui/avatar.tsx +45 -0
  19. package/src/components/connectycube-ui/dismiss-layer.jsx +57 -0
  20. package/src/components/connectycube-ui/dismiss-layer.tsx +74 -0
  21. package/src/components/connectycube-ui/placeholder-text.jsx +22 -0
  22. package/src/components/connectycube-ui/placeholder-text.tsx +28 -0
  23. package/src/components/connectycube-ui/presence.jsx +41 -0
  24. package/src/components/connectycube-ui/presence.tsx +55 -0
  25. package/src/components/connectycube-ui/stream-view.jsx +166 -28
  26. package/src/components/connectycube-ui/stream-view.tsx +231 -62
  27. package/src/components/connectycube-ui/utils.js +20 -0
  28. package/src/components/connectycube-ui/utils.ts +18 -0
  29. package/src/components/dismiss-layer.tsx +74 -0
  30. package/src/components/placeholder-text.tsx +28 -0
  31. package/src/components/presence.tsx +55 -0
  32. package/src/components/stream-view.tsx +231 -62
  33. package/src/components/utils.ts +18 -0
  34. package/src/index.ts +6 -2
package/commands/add.js CHANGED
@@ -7,58 +7,92 @@ import { execa } from "execa";
7
7
  const cwd = process.cwd();
8
8
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
9
9
 
10
- async function confirmInstallDependencies(component, deps) {
10
+ async function copyImports(component, srcDir, destDir, isTypeScript) {
11
+ const imports = path.join(__dirname, "..", "configs", "imports.json");
12
+
13
+ if (!(await fs.pathExists(imports))) return;
14
+
15
+ try {
16
+ const config = await fs.readJSON(imports);
17
+ const items = config ? config[component] || [] : [];
18
+ const promises = items?.map(async (item) => {
19
+ const ext = item === 'utils' ? isTypeScript ? ".ts" : ".js" : isTypeScript ? ".tsx" : ".jsx";
20
+ const filename = `${item}${ext}`;
21
+ const input = path.join(srcDir, filename);
22
+ const output = path.join(destDir, filename);
23
+
24
+ if (!(await fs.pathExists(output))) {
25
+ await fs.copyFile(input, output);
26
+ console.log(`- ${filename} (${output})`);
27
+ } else {
28
+ console.log(`- ${filename} (already exists)`);
29
+ }
30
+ });
31
+
32
+ await Promise.all(promises);
33
+
34
+ return items;
35
+ } catch (err) {
36
+ console.warn(`⚠️ Failed to copy imports for '${component}':`, err.message);
37
+ }
38
+ }
39
+
40
+ async function confirmInstallDependencies(components, deps) {
11
41
  const response = await prompts({
12
42
  type: "confirm",
13
43
  name: "confirm",
14
- message: `📦 Install dependencies for "${component}"?\n🔗 ${deps.join("\n🔗 ")}\n`,
44
+ message: `📦 Install dependencies for <${components.join(', ')}>?\n🔗 ${deps.join("\n🔗 ")}\n`,
15
45
  initial: true,
16
46
  });
17
47
 
18
48
  return response.confirm;
19
49
  }
20
50
 
21
- export async function installDependencies(component) {
22
- const input = path.join(__dirname, "..", "configs", "dependencies.json");
51
+ export async function installDependencies(components) {
52
+ const dependencies = path.join(__dirname, "..", "configs", "dependencies.json");
23
53
 
24
- if (!(await fs.pathExists(input))) return;
54
+ if (!(await fs.pathExists(dependencies))) return;
25
55
 
26
56
  try {
27
- const allDeps = await fs.readJSON(input);
28
- const cmpDeps = allDeps && allDeps[component];
57
+ const config = await fs.readJSON(dependencies);
58
+ const deps = Array.from(
59
+ components.reduce((set, component) => {
60
+ const cmpDeps = config ? config[component] || {} : {};
29
61
 
30
- if (cmpDeps && typeof cmpDeps === "object") {
31
- const deps = Object.entries(cmpDeps).map(([pkg, ver]) =>
32
- ver ? `${pkg}@${ver}` : pkg
33
- );
62
+ for (const [pkg, version] of Object.entries(cmpDeps)) {
63
+ set.add(`${pkg}@${version}`);
64
+ }
34
65
 
35
- if (deps.length) {
36
- const confirm = await confirmInstallDependencies(component, deps);
66
+ return set;
67
+ }, new Set())
68
+ );
37
69
 
38
- if (!confirm) {
39
- console.log("⏩ Skipped dependency installation.");
70
+ if (deps.length) {
71
+ const confirm = await confirmInstallDependencies(components, deps);
40
72
 
41
- return;
42
- }
73
+ if (!confirm) {
74
+ console.log("⏩ Skipped dependency installation.");
43
75
 
44
- const pm = fs.existsSync(path.join(cwd, "pnpm-lock.yaml"))
45
- ? "pnpm"
46
- : fs.existsSync(path.join(cwd, "yarn.lock"))
47
- ? "yarn"
48
- : "npm";
76
+ return;
77
+ }
49
78
 
50
- console.log(`ℹ️ Installing dependencies using ${pm}...`);
79
+ const pm = fs.existsSync(path.join(cwd, "pnpm-lock.yaml"))
80
+ ? "pnpm"
81
+ : fs.existsSync(path.join(cwd, "yarn.lock"))
82
+ ? "yarn"
83
+ : "npm";
51
84
 
52
- const cmd = pm === "yarn" || pm === "pnpm" ? "add" : "install";
53
- const flags = pm === "npm" ? ["--save"] : [];
54
- const opts = { cwd, stdio: "inherit" };
85
+ console.log(`ℹ️ Installing dependencies using ${pm}...`);
55
86
 
56
- await execa(pm, [cmd, ...flags, ...deps], opts);
57
- console.log(" Dependencies installed.");
58
- }
87
+ const cmd = pm === "yarn" || pm === "pnpm" ? "add" : "install";
88
+ const flags = pm === "npm" ? ["--save"] : [];
89
+ const opts = { cwd, stdio: "inherit" };
90
+
91
+ await execa(pm, [cmd, ...flags, ...deps], opts);
92
+ console.log("✅ Dependencies installed.");
59
93
  }
60
94
  } catch (err) {
61
- console.warn(`⚠️ Failed to read '${input}':`, err.message);
95
+ console.warn(`⚠️ Failed to read '${dependencies}':`, err.message);
62
96
  }
63
97
  }
64
98
 
@@ -91,31 +125,24 @@ export async function add(component) {
91
125
  }
92
126
 
93
127
  const filename = `${component}${isTypeScript ? ".tsx" : ".jsx"}`;
94
- const utilsFilename = `utils${isTypeScript ? ".ts" : ".js"}`;
95
- const sourceDir = path.join(__dirname, "..", isTypeScript ? "src" : "gen", "components");
96
- const input = path.join(sourceDir, filename);
128
+ const srcDir = path.join(__dirname, "..", isTypeScript ? "src" : "gen", "components");
129
+ const input = path.join(srcDir, filename);
97
130
 
98
131
  if (!(await fs.pathExists(input))) {
99
132
  console.warn(`⚠️ Component "${component}" does not exist.`);
100
133
  process.exit(1);
101
134
  }
102
135
 
103
- const dest = path.join(cwd, "src", "components", "connectycube-ui");
104
- const output = path.join(dest, filename);
105
- const utilsInput = path.join(sourceDir, utilsFilename);
106
- const utilsOutput = path.join(dest, utilsFilename);
136
+ const destDir = path.join(cwd, "src", "components", "connectycube-ui");
137
+ const output = path.join(destDir, filename);
107
138
 
108
- await fs.ensureDir(dest);
139
+ await fs.ensureDir(destDir);
109
140
  await fs.copyFile(input, output);
110
141
  console.log("✅ Added:");
111
142
  console.log(`- ${filename} (${output})`);
112
143
 
113
- if (!(await fs.pathExists(utilsOutput))) {
114
- await fs.copyFile(utilsInput, utilsOutput);
115
- console.log(`- ${utilsFilename} (${utilsOutput})`);
116
- } else {
117
- console.log(`- ${utilsFilename} (already exists)`);
118
- }
144
+ const imports = await copyImports(component, srcDir, destDir, isTypeScript);
145
+ const components = [component, ...imports];
119
146
 
120
- await installDependencies(component);
147
+ await installDependencies(components);
121
148
  }
@@ -1,5 +1,20 @@
1
1
  {
2
+ "animated-loader": {
3
+ "lucide-react": "latest"
4
+ },
5
+ "avatar": {
6
+ "@radix-ui/react-avatar": "latest"
7
+ },
8
+ "dismiss-layer": {},
9
+ "placeholder-text": {},
10
+ "presence": {
11
+ "lucide-react": "latest"
12
+ },
2
13
  "stream-view": {
3
- "webrtc-adapter": ""
14
+ "lucide-react": "latest"
15
+ },
16
+ "utils": {
17
+ "clsx": "latest",
18
+ "tailwind-merge": "latest"
4
19
  }
5
20
  }
@@ -0,0 +1,8 @@
1
+ {
2
+ "animated-loader": ["utils"],
3
+ "avatar": ["utils", "presence"],
4
+ "dismiss-layer": ["utils"],
5
+ "placeholder-text": ["utils"],
6
+ "presence": ["utils"],
7
+ "stream-view": ["utils"]
8
+ }
@@ -0,0 +1,10 @@
1
+ import { LoaderCircle } from 'lucide-react';
2
+ import { cn } from './utils';
3
+
4
+ function AnimatedLoader({ loading = true, className }) {
5
+ return loading ? <LoaderCircle className={cn('animate-spin mx-auto', className)} /> : null;
6
+ }
7
+
8
+ AnimatedLoader.displayName = 'AnimatedLoader';
9
+
10
+ export { AnimatedLoader };
@@ -0,0 +1,34 @@
1
+ import { forwardRef, memo } from 'react';
2
+ import * as AvatarPrimitive from '@radix-ui/react-avatar';
3
+ import { PresenceBadge } from './presence';
4
+ import { cn, getInitialsFromName } from './utils';
5
+
6
+ function AvatarBase(
7
+ { src, name = 'NA', online, presence, className, onlineClassName, presenceClassName, ...props },
8
+ ref
9
+ ) {
10
+ const initials = getInitialsFromName(name);
11
+
12
+ return (
13
+ <AvatarPrimitive.Root
14
+ ref={ref}
15
+ className={cn('relative flex size-8 shrink-0 overflow-hidden rounded-full', className)}
16
+ {...props}
17
+ >
18
+ <AvatarPrimitive.Image src={src} className={cn('aspect-square size-full object-cover')} />
19
+ <AvatarPrimitive.Fallback className={cn('bg-muted flex size-full items-center justify-center')}>
20
+ {initials}
21
+ </AvatarPrimitive.Fallback>
22
+ {online && (
23
+ <div className={cn('rounded-full border-2 bg-green-600 border-green-200 size-3.5', onlineClassName)} />
24
+ )}
25
+ <PresenceBadge status={presence} className={cn('absolute -bottom-0.5 -right-1', presenceClassName)} />
26
+ </AvatarPrimitive.Root>
27
+ );
28
+ }
29
+
30
+ const Avatar = memo(forwardRef(AvatarBase));
31
+
32
+ Avatar.displayName = 'Avatar';
33
+
34
+ export { Avatar };
@@ -0,0 +1,57 @@
1
+ import { useCallback, useEffect, useRef, useImperativeHandle, memo, forwardRef } from 'react';
2
+ import { cn } from './utils';
3
+
4
+ function DismissLayerBase(
5
+ { active, onDismiss, disableClickOutside = false, disableEscKeyPress = false, disabled, className, ...props },
6
+ ref
7
+ ) {
8
+ const innerRef = useRef(null);
9
+
10
+ useImperativeHandle(ref, () => innerRef.current);
11
+
12
+ const handleClickOrTouch = useCallback(
13
+ (e) => {
14
+ if (!disableClickOutside && active && e.target === innerRef.current) {
15
+ onDismiss();
16
+ }
17
+ },
18
+ [disableClickOutside, active, onDismiss]
19
+ );
20
+ const handleKeyEvent = useCallback(
21
+ (ev) => {
22
+ if (!disableEscKeyPress && active && ev.key === 'Escape') {
23
+ onDismiss();
24
+ }
25
+ },
26
+ [disableEscKeyPress, active, onDismiss]
27
+ );
28
+
29
+ useEffect(() => {
30
+ if (!disableEscKeyPress && active) {
31
+ document.addEventListener('keydown', handleKeyEvent);
32
+
33
+ return () => document.removeEventListener('keydown', handleKeyEvent);
34
+ }
35
+
36
+ return;
37
+ }, [disableEscKeyPress, active, handleKeyEvent]);
38
+
39
+ if (disabled || !active) return null;
40
+
41
+ return (
42
+ <div
43
+ ref={innerRef}
44
+ onClick={handleClickOrTouch}
45
+ onTouchStart={handleClickOrTouch}
46
+ className={cn('fixed top-0 left-0 z-40 size-full bg-black/20', className)}
47
+ aria-hidden
48
+ {...props}
49
+ />
50
+ );
51
+ }
52
+
53
+ const DismissLayer = memo(forwardRef(DismissLayerBase));
54
+
55
+ DismissLayer.displayName = 'DismissLayer';
56
+
57
+ export { DismissLayer };
@@ -0,0 +1,22 @@
1
+ import { forwardRef, memo } from 'react';
2
+ import { cn } from './utils';
3
+
4
+ function PlaceholderTextBase({ title, titles = [], className }, ref) {
5
+ const rows = typeof title === 'string' ? [title, ...titles] : titles;
6
+
7
+ return (
8
+ <div ref={ref} className={cn('absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2', className)}>
9
+ {rows.map((row, index) => (
10
+ <div key={`placeholder-text-${index}`} className="text-center">
11
+ {row}
12
+ </div>
13
+ ))}
14
+ </div>
15
+ );
16
+ }
17
+
18
+ const PlaceholderText = memo(forwardRef(PlaceholderTextBase));
19
+
20
+ PlaceholderText.displayName = 'PlaceholderText';
21
+
22
+ export { PlaceholderText };
@@ -0,0 +1,41 @@
1
+ import { memo } from 'react';
2
+ import { CircleCheck, CircleMinus, CircleQuestionMark, Clock } from 'lucide-react';
3
+ import { capitalize, cn, UserPresence } from './utils';
4
+
5
+ function PresenceBadgeBase({ status, className, ...props }) {
6
+ switch (status) {
7
+ case UserPresence.AVAILABLE || 'available':
8
+ return <CircleCheck className={cn('rounded-full text-white bg-green-600 size-4.5', className)} {...props} />;
9
+ case UserPresence.BUSY || 'busy':
10
+ return <CircleMinus className={cn('rounded-full text-white bg-red-600 size-4.5', className)} {...props} />;
11
+ case UserPresence.AWAY || 'away':
12
+ return <Clock className={cn('rounded-full text-white bg-yellow-500 size-4.5', className)} {...props} />;
13
+ case UserPresence.UNKNOWN || 'unknown':
14
+ return (
15
+ <CircleQuestionMark className={cn('rounded-full text-white bg-gray-500 size-4.5', className)} {...props} />
16
+ );
17
+ default:
18
+ return null;
19
+ }
20
+ }
21
+
22
+ const PresenceBadge = memo(PresenceBadgeBase);
23
+
24
+ PresenceBadge.displayName = 'PresenceBadge';
25
+
26
+ function PresenceBase({ badge = true, status, label, className, ...props }) {
27
+ const presence = capitalize(label || status);
28
+
29
+ return (
30
+ <div className={cn('flex items-center gap-2', className)} {...props}>
31
+ {badge && <PresenceBadge status={status} />}
32
+ <span>{presence}</span>
33
+ </div>
34
+ );
35
+ }
36
+
37
+ const Presence = memo(PresenceBase);
38
+
39
+ Presence.displayName = 'Presence';
40
+
41
+ export { Presence, PresenceBadge };
@@ -1,63 +1,201 @@
1
- import { forwardRef, useEffect, useImperativeHandle, useMemo, useRef } from 'react';
1
+ import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
2
+ import { Maximize, Minimize, PictureInPicture2 } from 'lucide-react';
2
3
  import { cn, getRandomString } from './utils';
3
4
 
4
- export const StreamView = forwardRef(({ id, stream, mirror, className, muted, ...props }, ref) => {
5
- const videoRef = useRef(null);
5
+ function StreamViewBase({ id, stream, mirror, className, muted, ...props }, ref) {
6
+ const innerRef = useRef(null);
6
7
  const elementId = useMemo(() => id ?? `stream-${getRandomString()}`, [id]);
7
- const isMuted = typeof muted === 'boolean' ? muted : true;
8
+ const isMuted = typeof muted === 'boolean' ? muted : false;
8
9
  const defaultClassName = 'size-full object-contain';
9
10
  const mirrorClassName = mirror ? 'scale-x-[-1]' : '';
10
11
 
11
- useImperativeHandle(
12
- ref,
13
- () => ({
14
- id: elementId,
15
- element: videoRef.current,
16
- }),
17
- [elementId]
18
- );
12
+ useImperativeHandle(ref, () => innerRef.current);
19
13
 
20
14
  useEffect(() => {
21
- if (videoRef.current && stream) {
22
- videoRef.current.srcObject = stream;
15
+ if (innerRef.current && stream) {
16
+ innerRef.current.srcObject = stream;
23
17
 
24
18
  const playVideo = () => {
25
19
  try {
26
- videoRef.current?.play();
20
+ innerRef.current?.play();
27
21
  } catch (error) {
28
- console.error('<StreamView/> play():', error);
22
+ console.error('Error playing video:', error);
29
23
  }
30
24
  };
31
25
 
32
- videoRef.current.onloadedmetadata = () => {
33
- setTimeout(playVideo, 100);
26
+ innerRef.current.onloadedmetadata = () => {
27
+ queueMicrotask(playVideo);
34
28
  };
35
29
  }
36
30
  }, [stream]);
37
31
 
38
- return stream ? (
32
+ if (!stream) return null;
33
+
34
+ return (
39
35
  <video
40
- ref={videoRef}
36
+ ref={innerRef}
41
37
  id={elementId}
42
- className={cn(defaultClassName, mirrorClassName, className)}
43
- muted={isMuted}
44
38
  autoPlay
45
39
  playsInline
40
+ muted={isMuted}
41
+ className={cn(defaultClassName, mirrorClassName, className)}
46
42
  {...props}
47
43
  />
48
- ) : null;
49
- });
44
+ );
45
+ }
46
+
47
+ const StreamView = forwardRef(StreamViewBase);
50
48
 
51
- export const LocalStreamView = forwardRef(({ muted, mirror, ...props }, ref) => {
49
+ StreamView.displayName = 'StreamView';
50
+
51
+ function LocalStreamViewBase({ muted, mirror, ...props }, ref) {
52
52
  const isMuted = typeof muted === 'boolean' ? muted : true;
53
53
  const isMirror = typeof mirror === 'boolean' ? mirror : true;
54
54
 
55
55
  return <StreamView ref={ref} muted={isMuted} mirror={isMirror} {...props} />;
56
- });
56
+ }
57
+
58
+ const LocalStreamView = forwardRef(LocalStreamViewBase);
57
59
 
58
- export const RemoteStreamView = forwardRef(({ muted, mirror, ...props }, ref) => {
60
+ LocalStreamView.displayName = 'LocalStreamView';
61
+
62
+ function RemoteStreamViewBase({ muted, mirror, ...props }, ref) {
59
63
  const isMuted = typeof muted === 'boolean' ? muted : false;
60
64
  const isMirror = typeof mirror === 'boolean' ? mirror : false;
61
65
 
62
66
  return <StreamView ref={ref} muted={isMuted} mirror={isMirror} {...props} />;
63
- });
67
+ }
68
+
69
+ const RemoteStreamView = forwardRef(RemoteStreamViewBase);
70
+
71
+ RemoteStreamView.displayName = 'RemoteStreamView';
72
+
73
+ function FullscreenStreamViewBase(
74
+ {
75
+ element,
76
+ pipElement,
77
+ navElement,
78
+ hideIconElement,
79
+ showIconElement,
80
+ containerClassName,
81
+ fullscreenButtonClassName,
82
+ fullscreenButtonIconClassName,
83
+ pipContainerClassName,
84
+ pipButtonClassName,
85
+ pipButtonIconClassName,
86
+ containerProps,
87
+ fullscreenButtonProps,
88
+ fullscreenButtonIconProps,
89
+ pipContainerProps,
90
+ pipButtonProps,
91
+ pipButtonIconProps,
92
+ },
93
+ ref
94
+ ) {
95
+ const innerRef = useRef(null);
96
+ const [isFullscreen, setIsFullscreen] = useState(false);
97
+ const [isPictureInPicture, setIsPictureInPicture] = useState(false);
98
+ const toggleFullscreen = useCallback(async () => {
99
+ const container = innerRef.current;
100
+
101
+ if (!container) return;
102
+
103
+ try {
104
+ if (!document.fullscreenElement) {
105
+ await container.requestFullscreen();
106
+ setIsFullscreen(true);
107
+ setIsPictureInPicture(true);
108
+ } else {
109
+ await document.exitFullscreen();
110
+ setIsFullscreen(false);
111
+ setIsPictureInPicture(false);
112
+ }
113
+ } catch (err) {
114
+ console.error('Fullscreen error:', err);
115
+ }
116
+ }, []);
117
+ const togglePictureInPicture = useCallback(() => {
118
+ if (pipElement) {
119
+ setIsPictureInPicture((prevState) => !prevState);
120
+ }
121
+ }, [pipElement]);
122
+
123
+ useImperativeHandle(
124
+ ref,
125
+ () =>
126
+ Object.assign(innerRef.current, {
127
+ isFullscreen,
128
+ isPictureInPicture,
129
+ toggleFullscreen,
130
+ togglePictureInPicture,
131
+ }),
132
+ [isFullscreen, isPictureInPicture, toggleFullscreen, togglePictureInPicture]
133
+ );
134
+
135
+ useEffect(() => {
136
+ const onFullscreenChange = () => {
137
+ setIsFullscreen(!!document.fullscreenElement);
138
+ setIsPictureInPicture(!!document.fullscreenElement);
139
+ };
140
+
141
+ document.addEventListener('fullscreenchange', onFullscreenChange);
142
+
143
+ return () => document.removeEventListener('fullscreenchange', onFullscreenChange);
144
+ }, []);
145
+
146
+ return (
147
+ <div
148
+ ref={innerRef}
149
+ className={cn('relative flex items-center justify-center size-full', containerClassName)}
150
+ {...containerProps}
151
+ >
152
+ {element}
153
+ <button
154
+ onClick={toggleFullscreen}
155
+ className={cn(
156
+ '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',
157
+ fullscreenButtonClassName
158
+ )}
159
+ {...fullscreenButtonProps}
160
+ >
161
+ {isFullscreen
162
+ ? hideIconElement || <Minimize className={fullscreenButtonIconClassName} {...fullscreenButtonIconProps} />
163
+ : showIconElement || <Maximize className={fullscreenButtonIconClassName} {...fullscreenButtonIconProps} />}
164
+ </button>
165
+ <div className="absolute size-full p-2 flex flex-col justify-end items-center">
166
+ {isFullscreen && pipElement && (
167
+ <div className="relative size-full flex items-end justify-end">
168
+ {isPictureInPicture && (
169
+ <div
170
+ className={cn(
171
+ 'max-w-1/4 max-h-1/4 aspect-4/3 overflow-hidden rounded-md shadow-md shadow-white/25',
172
+ pipContainerClassName
173
+ )}
174
+ {...pipContainerProps}
175
+ >
176
+ {pipElement}
177
+ </div>
178
+ )}
179
+ <button
180
+ onClick={togglePictureInPicture}
181
+ className={cn(
182
+ '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',
183
+ pipButtonClassName
184
+ )}
185
+ {...pipButtonProps}
186
+ >
187
+ <PictureInPicture2 className={pipButtonIconClassName} {...pipButtonIconProps} />
188
+ </button>
189
+ </div>
190
+ )}
191
+ {isFullscreen && navElement}
192
+ </div>
193
+ </div>
194
+ );
195
+ }
196
+
197
+ const FullscreenStreamView = forwardRef(FullscreenStreamViewBase);
198
+
199
+ FullscreenStreamView.displayName = 'FullscreenStreamView';
200
+
201
+ export { StreamView, LocalStreamView, RemoteStreamView, FullscreenStreamView };
@@ -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
+ })({});
package/gen/index.js CHANGED
@@ -1 +1,3 @@
1
- export { StreamView, LocalStreamView, RemoteStreamView } from './components/stream-view';
1
+ export { DismissLayer } from './components/dismiss-layer';
2
+
3
+ export { StreamView, LocalStreamView, RemoteStreamView, FullscreenStreamView } from './components/stream-view';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@connectycube/react-ui-kit",
3
- "version": "0.0.8",
3
+ "version": "0.0.11",
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": {
@@ -60,16 +60,17 @@
60
60
  "jsx"
61
61
  ],
62
62
  "dependencies": {
63
+ "@radix-ui/react-avatar": "^1.1.11",
64
+ "clsx": "^2.1.1",
63
65
  "execa": "^9.6.0",
64
66
  "fs-extra": "^11.3.2",
67
+ "lucide-react": "^0.554.0",
65
68
  "prompts": "^2.4.2",
66
- "webrtc-adapter": "^9.0.3"
69
+ "tailwind-merge": "^3.4.0"
67
70
  },
68
71
  "peerDependencies": {
69
- "clsx": "*",
70
72
  "react": ">=18",
71
- "react-dom": ">=18",
72
- "tailwind-merge": "*"
73
+ "react-dom": ">=18"
73
74
  },
74
75
  "devDependencies": {
75
76
  "@babel/core": "^7.28.4",
@@ -0,0 +1,15 @@
1
+ import type { LucideProps } from 'lucide-react';
2
+ import { LoaderCircle } from 'lucide-react';
3
+ import { cn } from './utils';
4
+
5
+ interface AnimatedLoaderProps extends LucideProps {
6
+ loading?: boolean;
7
+ }
8
+
9
+ function AnimatedLoader({ loading = true, className }: AnimatedLoaderProps) {
10
+ return loading ? <LoaderCircle className={cn('animate-spin mx-auto', className)} /> : null;
11
+ }
12
+
13
+ AnimatedLoader.displayName = 'AnimatedLoader';
14
+
15
+ export { AnimatedLoader, type AnimatedLoaderProps };