@connectycube/react-ui-kit 0.0.7 → 0.0.10
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/bin/cli.js +1 -1
- package/commands/add.js +148 -0
- package/{cmd → commands}/help.js +1 -1
- package/configs/dependencies.json +20 -0
- 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 +9 -6
- 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 +201 -0
- package/src/components/connectycube-ui/stream-view.tsx +231 -62
- package/src/components/connectycube-ui/utils.js +30 -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
- package/cmd/add.js +0 -99
- /package/{cmd → commands}/list.js +0 -0
package/bin/cli.js
CHANGED
|
@@ -4,7 +4,7 @@ import path from 'path';
|
|
|
4
4
|
import { fileURLToPath } from 'url';
|
|
5
5
|
|
|
6
6
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
-
const commandsDir = path.resolve(__dirname, '../
|
|
7
|
+
const commandsDir = path.resolve(__dirname, '../commands');
|
|
8
8
|
const args = process.argv.slice(2);
|
|
9
9
|
const cmd = args[0] ? args[0] === '--help' ? 'help' : args[0] : 'help';
|
|
10
10
|
|
package/commands/add.js
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import fs from "fs-extra";
|
|
3
|
+
import prompts from "prompts";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
import { execa } from "execa";
|
|
6
|
+
|
|
7
|
+
const cwd = process.cwd();
|
|
8
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
|
|
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) {
|
|
41
|
+
const response = await prompts({
|
|
42
|
+
type: "confirm",
|
|
43
|
+
name: "confirm",
|
|
44
|
+
message: `📦 Install dependencies for <${components.join(', ')}>?\n🔗 ${deps.join("\n🔗 ")}\n`,
|
|
45
|
+
initial: true,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
return response.confirm;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function installDependencies(components) {
|
|
52
|
+
const dependencies = path.join(__dirname, "..", "configs", "dependencies.json");
|
|
53
|
+
|
|
54
|
+
if (!(await fs.pathExists(dependencies))) return;
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
const config = await fs.readJSON(dependencies);
|
|
58
|
+
const deps = Array.from(
|
|
59
|
+
components.reduce((set, component) => {
|
|
60
|
+
const cmpDeps = config ? config[component] || {} : {};
|
|
61
|
+
|
|
62
|
+
for (const [pkg, version] of Object.entries(cmpDeps)) {
|
|
63
|
+
set.add(`${pkg}@${version}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return set;
|
|
67
|
+
}, new Set())
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
if (deps.length) {
|
|
71
|
+
const confirm = await confirmInstallDependencies(components, deps);
|
|
72
|
+
|
|
73
|
+
if (!confirm) {
|
|
74
|
+
console.log("⏩ Skipped dependency installation.");
|
|
75
|
+
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
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";
|
|
84
|
+
|
|
85
|
+
console.log(`ℹ️ Installing dependencies using ${pm}...`);
|
|
86
|
+
|
|
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.");
|
|
93
|
+
}
|
|
94
|
+
} catch (err) {
|
|
95
|
+
console.warn(`⚠️ Failed to read '${dependencies}':`, err.message);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function promptIsTypeScript() {
|
|
100
|
+
const response = await prompts({
|
|
101
|
+
type: "select",
|
|
102
|
+
name: "isTypeScript",
|
|
103
|
+
message: "What language does your project use?",
|
|
104
|
+
choices: [
|
|
105
|
+
{ title: "👉 TypeScript (🟦 *.{ts,tsx})", value: true },
|
|
106
|
+
{ title: "👉 JavaScript (🟨 *.{js,jsx})", value: false }
|
|
107
|
+
],
|
|
108
|
+
initial: 0
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
return response.isTypeScript;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export async function add(component) {
|
|
115
|
+
if (!component) {
|
|
116
|
+
console.warn(`⚠️ Usage: npx @connectycube/react-ui-kit add [component-name]`);
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const isTypeScript = await promptIsTypeScript();
|
|
121
|
+
|
|
122
|
+
if (isTypeScript === undefined) {
|
|
123
|
+
console.error("️⛔ Aborted.");
|
|
124
|
+
process.exit(0);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const filename = `${component}${isTypeScript ? ".tsx" : ".jsx"}`;
|
|
128
|
+
const srcDir = path.join(__dirname, "..", isTypeScript ? "src" : "gen", "components");
|
|
129
|
+
const input = path.join(srcDir, filename);
|
|
130
|
+
|
|
131
|
+
if (!(await fs.pathExists(input))) {
|
|
132
|
+
console.warn(`⚠️ Component "${component}" does not exist.`);
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const destDir = path.join(cwd, "src", "components", "connectycube-ui");
|
|
137
|
+
const output = path.join(destDir, filename);
|
|
138
|
+
|
|
139
|
+
await fs.ensureDir(destDir);
|
|
140
|
+
await fs.copyFile(input, output);
|
|
141
|
+
console.log("✅ Added:");
|
|
142
|
+
console.log(`- ${filename} (${output})`);
|
|
143
|
+
|
|
144
|
+
const imports = await copyImports(component, srcDir, destDir, isTypeScript);
|
|
145
|
+
const components = [component, ...imports];
|
|
146
|
+
|
|
147
|
+
await installDependencies(components);
|
|
148
|
+
}
|
package/{cmd → commands}/help.js
RENAMED
|
@@ -4,7 +4,7 @@ import { fileURLToPath } from 'url';
|
|
|
4
4
|
|
|
5
5
|
export function help() {
|
|
6
6
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
-
const commands = path.resolve(__dirname, '../
|
|
7
|
+
const commands = path.resolve(__dirname, '../commands');
|
|
8
8
|
const names = fs.readdirSync(commands).filter(f => f.endsWith('.js'));
|
|
9
9
|
|
|
10
10
|
console.info('ℹ️ Usage: npx @connectycube/react-ui-kit@latest <command> [args]');
|
|
@@ -0,0 +1,20 @@
|
|
|
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
|
+
},
|
|
13
|
+
"stream-view": {
|
|
14
|
+
"lucide-react": "latest"
|
|
15
|
+
},
|
|
16
|
+
"utils": {
|
|
17
|
+
"clsx": "latest",
|
|
18
|
+
"tailwind-merge": "latest"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -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 presence={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({ presence, className, ...props }) {
|
|
6
|
+
switch (presence) {
|
|
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, className, presence, label, ...props }) {
|
|
27
|
+
const status = capitalize(label || presence);
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<div className={cn('flex items-center gap-2', className)} {...props}>
|
|
31
|
+
{badge && <PresenceBadge presence={presence} />}
|
|
32
|
+
<span>{status}</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
|
-
|
|
5
|
-
const
|
|
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 :
|
|
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 (
|
|
22
|
-
|
|
15
|
+
if (innerRef.current && stream) {
|
|
16
|
+
innerRef.current.srcObject = stream;
|
|
23
17
|
|
|
24
18
|
const playVideo = () => {
|
|
25
19
|
try {
|
|
26
|
-
|
|
20
|
+
innerRef.current?.play();
|
|
27
21
|
} catch (error) {
|
|
28
|
-
console.error('
|
|
22
|
+
console.error('Error playing video:', error);
|
|
29
23
|
}
|
|
30
24
|
};
|
|
31
25
|
|
|
32
|
-
|
|
33
|
-
|
|
26
|
+
innerRef.current.onloadedmetadata = () => {
|
|
27
|
+
queueMicrotask(playVideo);
|
|
34
28
|
};
|
|
35
29
|
}
|
|
36
30
|
}, [stream]);
|
|
37
31
|
|
|
38
|
-
|
|
32
|
+
if (!stream) return null;
|
|
33
|
+
|
|
34
|
+
return (
|
|
39
35
|
<video
|
|
40
|
-
ref={
|
|
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
|
-
)
|
|
49
|
-
}
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const StreamView = forwardRef(StreamViewBase);
|
|
50
48
|
|
|
51
|
-
|
|
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
|
-
|
|
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 };
|
package/gen/components/utils.js
CHANGED
|
@@ -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
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.10",
|
|
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": {
|
|
@@ -33,8 +33,9 @@
|
|
|
33
33
|
"module": "dist/index.js",
|
|
34
34
|
"types": "dist/types/index.d.ts",
|
|
35
35
|
"files": [
|
|
36
|
+
"configs",
|
|
37
|
+
"commands",
|
|
36
38
|
"bin",
|
|
37
|
-
"cmd",
|
|
38
39
|
"dist",
|
|
39
40
|
"src",
|
|
40
41
|
"gen"
|
|
@@ -59,15 +60,17 @@
|
|
|
59
60
|
"jsx"
|
|
60
61
|
],
|
|
61
62
|
"dependencies": {
|
|
63
|
+
"@radix-ui/react-avatar": "^1.1.11",
|
|
64
|
+
"clsx": "^2.1.1",
|
|
62
65
|
"execa": "^9.6.0",
|
|
63
66
|
"fs-extra": "^11.3.2",
|
|
64
|
-
"
|
|
67
|
+
"lucide-react": "^0.554.0",
|
|
68
|
+
"prompts": "^2.4.2",
|
|
69
|
+
"tailwind-merge": "^3.4.0"
|
|
65
70
|
},
|
|
66
71
|
"peerDependencies": {
|
|
67
|
-
"clsx": "*",
|
|
68
72
|
"react": ">=18",
|
|
69
|
-
"react-dom": ">=18"
|
|
70
|
-
"tailwind-merge": "*"
|
|
73
|
+
"react-dom": ">=18"
|
|
71
74
|
},
|
|
72
75
|
"devDependencies": {
|
|
73
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 };
|