@conference-kit/ui-react 0.0.1
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/package.json +30 -0
- package/src/ChatPanel.tsx +86 -0
- package/src/ConnectionBadge.tsx +45 -0
- package/src/ControlBar.tsx +88 -0
- package/src/ParticipantGrid.tsx +18 -0
- package/src/ParticipantTile.tsx +50 -0
- package/src/RosterList.tsx +34 -0
- package/src/icons.tsx +89 -0
- package/src/index.ts +8 -0
- package/src/types.ts +31 -0
- package/tsconfig.json +9 -0
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@conference-kit/ui-react",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"import": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsc -p tsconfig.json"
|
|
15
|
+
},
|
|
16
|
+
"peerDependencies": {
|
|
17
|
+
"react": ">=18.2.0",
|
|
18
|
+
"react-dom": ">=18.2.0",
|
|
19
|
+
"tailwindcss": ">=3.4.0"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@conference-kit/react": "^0.0.1",
|
|
23
|
+
"@conference-kit/core": "^0.0.1"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@types/react": "^18.3.27",
|
|
27
|
+
"@types/react-dom": "^18.3.7",
|
|
28
|
+
"typescript": "^5.9.3"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { ChangeEvent, FC, FormEvent } from "react";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { ChatIcon } from "./icons";
|
|
4
|
+
|
|
5
|
+
export type ChatMessage = {
|
|
6
|
+
id: string;
|
|
7
|
+
direction: "in" | "out";
|
|
8
|
+
text: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type ChatPanelProps = {
|
|
12
|
+
messages: ChatMessage[];
|
|
13
|
+
ready: boolean;
|
|
14
|
+
onSend: (text: string) => void;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const ChatPanel: FC<ChatPanelProps> = ({
|
|
18
|
+
messages,
|
|
19
|
+
ready,
|
|
20
|
+
onSend,
|
|
21
|
+
}: ChatPanelProps) => {
|
|
22
|
+
const [text, setText] = useState("ping");
|
|
23
|
+
|
|
24
|
+
const submit = (e: FormEvent) => {
|
|
25
|
+
e.preventDefault();
|
|
26
|
+
if (!text.trim()) return;
|
|
27
|
+
onSend(text.trim());
|
|
28
|
+
setText("");
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<div className="bg-slate-900 border border-slate-800 rounded-xl p-4 grid gap-3">
|
|
33
|
+
<div className="flex items-center justify-between">
|
|
34
|
+
<div className="flex items-center gap-2 text-slate-100 font-semibold">
|
|
35
|
+
<ChatIcon className="h-5 w-5" /> Data Channel
|
|
36
|
+
</div>
|
|
37
|
+
<span
|
|
38
|
+
className={`text-xs px-2 py-1 rounded-full ${
|
|
39
|
+
ready
|
|
40
|
+
? "bg-emerald-100 text-emerald-800"
|
|
41
|
+
: "bg-slate-200 text-slate-700"
|
|
42
|
+
}`}
|
|
43
|
+
>
|
|
44
|
+
{ready ? "Open" : "Closed"}
|
|
45
|
+
</span>
|
|
46
|
+
</div>
|
|
47
|
+
<form className="flex gap-2" onSubmit={submit}>
|
|
48
|
+
<input
|
|
49
|
+
className="flex-1 rounded-lg border border-slate-700 bg-slate-950 px-3 py-2 text-slate-100"
|
|
50
|
+
value={text}
|
|
51
|
+
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
|
52
|
+
setText(e.target.value)
|
|
53
|
+
}
|
|
54
|
+
placeholder="Message"
|
|
55
|
+
/>
|
|
56
|
+
<button
|
|
57
|
+
type="submit"
|
|
58
|
+
disabled={!ready}
|
|
59
|
+
className="px-3 py-2 rounded-lg bg-emerald-600 text-slate-50 font-semibold disabled:opacity-50"
|
|
60
|
+
>
|
|
61
|
+
Send
|
|
62
|
+
</button>
|
|
63
|
+
</form>
|
|
64
|
+
<div className="max-h-64 overflow-auto grid gap-2">
|
|
65
|
+
{messages.length === 0 && (
|
|
66
|
+
<div className="text-slate-400 text-sm">No messages yet</div>
|
|
67
|
+
)}
|
|
68
|
+
{messages.map((msg) => (
|
|
69
|
+
<div
|
|
70
|
+
key={msg.id}
|
|
71
|
+
className={`flex items-center justify-between rounded-lg px-3 py-2 border ${
|
|
72
|
+
msg.direction === "in"
|
|
73
|
+
? "bg-slate-800 border-slate-700"
|
|
74
|
+
: "bg-emerald-50 border-emerald-200"
|
|
75
|
+
}`}
|
|
76
|
+
>
|
|
77
|
+
<span className="text-sm text-slate-100">{msg.text}</span>
|
|
78
|
+
<span className="text-xs text-slate-500">
|
|
79
|
+
{msg.direction === "in" ? "In" : "Out"}
|
|
80
|
+
</span>
|
|
81
|
+
</div>
|
|
82
|
+
))}
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
86
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { FC } from "react";
|
|
2
|
+
import { SignalIcon, WifiOffIcon } from "./icons";
|
|
3
|
+
import type { ConnectionStatus } from "./types";
|
|
4
|
+
|
|
5
|
+
export const ConnectionBadge: FC<{ status: ConnectionStatus }> = ({
|
|
6
|
+
status,
|
|
7
|
+
}) => {
|
|
8
|
+
const signalTone =
|
|
9
|
+
status.signaling === "open"
|
|
10
|
+
? "bg-emerald-100 text-emerald-800"
|
|
11
|
+
: "bg-amber-100 text-amber-800";
|
|
12
|
+
const mediaTone =
|
|
13
|
+
status.media === "ready"
|
|
14
|
+
? "bg-emerald-100 text-emerald-800"
|
|
15
|
+
: status.media === "requesting"
|
|
16
|
+
? "bg-amber-100 text-amber-800"
|
|
17
|
+
: "bg-slate-200 text-slate-700";
|
|
18
|
+
const dataTone =
|
|
19
|
+
status.data === "ready"
|
|
20
|
+
? "bg-emerald-100 text-emerald-800"
|
|
21
|
+
: "bg-slate-200 text-slate-700";
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<div className="flex flex-wrap gap-2 text-sm">
|
|
25
|
+
<span
|
|
26
|
+
className={`inline-flex items-center gap-1 rounded-full px-2.5 py-1 ${signalTone}`}
|
|
27
|
+
>
|
|
28
|
+
<SignalIcon className="h-4 w-4" />
|
|
29
|
+
Signal {status.signaling ?? "idle"}
|
|
30
|
+
</span>
|
|
31
|
+
<span
|
|
32
|
+
className={`inline-flex items-center gap-1 rounded-full px-2.5 py-1 ${mediaTone}`}
|
|
33
|
+
>
|
|
34
|
+
<WifiOffIcon className="h-4 w-4" />
|
|
35
|
+
Media {status.media ?? "off"}
|
|
36
|
+
</span>
|
|
37
|
+
<span
|
|
38
|
+
className={`inline-flex items-center gap-1 rounded-full px-2.5 py-1 ${dataTone}`}
|
|
39
|
+
>
|
|
40
|
+
<SignalIcon className="h-4 w-4" />
|
|
41
|
+
Data {status.data ?? "idle"}
|
|
42
|
+
</span>
|
|
43
|
+
</div>
|
|
44
|
+
);
|
|
45
|
+
};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { FC, ReactNode } from "react";
|
|
2
|
+
import { ChatIcon, MicIcon, PhoneIcon, ScreenIcon, VideoIcon } from "./icons";
|
|
3
|
+
|
|
4
|
+
export type ControlBarProps = {
|
|
5
|
+
joined?: boolean;
|
|
6
|
+
mediaReady?: boolean;
|
|
7
|
+
audioMuted?: boolean;
|
|
8
|
+
videoMuted?: boolean;
|
|
9
|
+
screenSharing?: boolean;
|
|
10
|
+
onJoin?: () => void;
|
|
11
|
+
onLeave?: () => void;
|
|
12
|
+
onToggleAudio?: () => void;
|
|
13
|
+
onToggleVideo?: () => void;
|
|
14
|
+
onToggleScreen?: () => void;
|
|
15
|
+
onReset?: () => void;
|
|
16
|
+
extraRight?: ReactNode;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const buttonBase =
|
|
20
|
+
"inline-flex items-center gap-2 rounded-lg px-3 py-2 text-sm font-semibold border transition";
|
|
21
|
+
|
|
22
|
+
export const ControlBar: FC<ControlBarProps> = ({
|
|
23
|
+
joined,
|
|
24
|
+
mediaReady,
|
|
25
|
+
audioMuted,
|
|
26
|
+
videoMuted,
|
|
27
|
+
screenSharing,
|
|
28
|
+
onJoin,
|
|
29
|
+
onLeave,
|
|
30
|
+
onToggleAudio,
|
|
31
|
+
onToggleVideo,
|
|
32
|
+
onToggleScreen,
|
|
33
|
+
onReset,
|
|
34
|
+
extraRight,
|
|
35
|
+
}: ControlBarProps) => {
|
|
36
|
+
return (
|
|
37
|
+
<div className="flex flex-wrap items-center justify-between gap-3 bg-slate-900 border border-slate-800 rounded-xl px-4 py-3">
|
|
38
|
+
<div className="flex flex-wrap gap-2">
|
|
39
|
+
<button
|
|
40
|
+
className={`${buttonBase} border-emerald-500 text-emerald-100 bg-emerald-600/80 hover:bg-emerald-600`}
|
|
41
|
+
disabled={joined}
|
|
42
|
+
onClick={onJoin}
|
|
43
|
+
>
|
|
44
|
+
<VideoIcon className="h-4 w-4" /> Join
|
|
45
|
+
</button>
|
|
46
|
+
<button
|
|
47
|
+
className={`${buttonBase} border-slate-700 text-slate-100 bg-slate-800 hover:bg-slate-700`}
|
|
48
|
+
disabled={!joined}
|
|
49
|
+
onClick={onLeave}
|
|
50
|
+
>
|
|
51
|
+
<PhoneIcon className="h-4 w-4" /> Leave
|
|
52
|
+
</button>
|
|
53
|
+
<button
|
|
54
|
+
className={`${buttonBase} border-slate-700 text-slate-100 bg-slate-800 hover:bg-slate-700`}
|
|
55
|
+
disabled={!joined || !mediaReady}
|
|
56
|
+
onClick={onToggleAudio}
|
|
57
|
+
>
|
|
58
|
+
<MicIcon className="h-4 w-4" /> {audioMuted ? "Unmute" : "Mute"}
|
|
59
|
+
</button>
|
|
60
|
+
<button
|
|
61
|
+
className={`${buttonBase} border-slate-700 text-slate-100 bg-slate-800 hover:bg-slate-700`}
|
|
62
|
+
disabled={!joined || !mediaReady}
|
|
63
|
+
onClick={onToggleVideo}
|
|
64
|
+
>
|
|
65
|
+
<VideoIcon className="h-4 w-4" />{" "}
|
|
66
|
+
{videoMuted ? "Start video" : "Stop video"}
|
|
67
|
+
</button>
|
|
68
|
+
<button
|
|
69
|
+
className={`${buttonBase} border-slate-700 text-slate-100 bg-slate-800 hover:bg-slate-700`}
|
|
70
|
+
disabled={!joined}
|
|
71
|
+
onClick={onToggleScreen}
|
|
72
|
+
>
|
|
73
|
+
<ScreenIcon className="h-4 w-4" />{" "}
|
|
74
|
+
{screenSharing ? "Stop share" : "Share screen"}
|
|
75
|
+
</button>
|
|
76
|
+
<button
|
|
77
|
+
className={`${buttonBase} border-slate-700 text-slate-300 hover:bg-slate-800`}
|
|
78
|
+
onClick={onReset}
|
|
79
|
+
>
|
|
80
|
+
<ChatIcon className="h-4 w-4" /> Reset
|
|
81
|
+
</button>
|
|
82
|
+
</div>
|
|
83
|
+
{extraRight ? (
|
|
84
|
+
<div className="flex items-center gap-2">{extraRight}</div>
|
|
85
|
+
) : null}
|
|
86
|
+
</div>
|
|
87
|
+
);
|
|
88
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { FC } from "react";
|
|
2
|
+
import type { ParticipantView } from "./types";
|
|
3
|
+
import { ParticipantTile } from "./ParticipantTile";
|
|
4
|
+
|
|
5
|
+
export const ParticipantGrid: FC<{ participants: ParticipantView[] }> = ({
|
|
6
|
+
participants,
|
|
7
|
+
}) => {
|
|
8
|
+
return (
|
|
9
|
+
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
|
10
|
+
{participants.map((p) => (
|
|
11
|
+
<ParticipantTile key={p.id} participant={p} />
|
|
12
|
+
))}
|
|
13
|
+
{participants.length === 0 && (
|
|
14
|
+
<div className="text-slate-400 text-sm">Waiting for participants…</div>
|
|
15
|
+
)}
|
|
16
|
+
</div>
|
|
17
|
+
);
|
|
18
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { FC } from "react";
|
|
2
|
+
import type { ParticipantView } from "./types";
|
|
3
|
+
|
|
4
|
+
export const ParticipantTile: FC<{ participant: ParticipantView }> = ({
|
|
5
|
+
participant,
|
|
6
|
+
}) => {
|
|
7
|
+
return (
|
|
8
|
+
<div className="bg-slate-900 border border-slate-800 rounded-xl p-3 grid gap-2">
|
|
9
|
+
<div className="flex items-center justify-between gap-2 text-sm text-slate-200">
|
|
10
|
+
<div className="font-semibold truncate">
|
|
11
|
+
{participant.label ?? participant.id}
|
|
12
|
+
</div>
|
|
13
|
+
<span className="text-xs text-slate-400">
|
|
14
|
+
{participant.connectionState ?? "new"}
|
|
15
|
+
</span>
|
|
16
|
+
</div>
|
|
17
|
+
<video
|
|
18
|
+
className="w-full aspect-video rounded-lg bg-slate-950"
|
|
19
|
+
muted={participant.isLocal}
|
|
20
|
+
autoPlay
|
|
21
|
+
playsInline
|
|
22
|
+
ref={(el) => {
|
|
23
|
+
if (!el) return;
|
|
24
|
+
if (participant.stream && el.srcObject !== participant.stream) {
|
|
25
|
+
el.srcObject = participant.stream;
|
|
26
|
+
}
|
|
27
|
+
if (!participant.stream && el.srcObject) {
|
|
28
|
+
el.srcObject = null;
|
|
29
|
+
}
|
|
30
|
+
}}
|
|
31
|
+
/>
|
|
32
|
+
<div className="flex items-center gap-3 text-xs text-slate-400">
|
|
33
|
+
<span
|
|
34
|
+
className={
|
|
35
|
+
participant.mutedAudio ? "text-amber-400" : "text-emerald-300"
|
|
36
|
+
}
|
|
37
|
+
>
|
|
38
|
+
{participant.mutedAudio ? "Audio muted" : "Audio on"}
|
|
39
|
+
</span>
|
|
40
|
+
<span
|
|
41
|
+
className={
|
|
42
|
+
participant.mutedVideo ? "text-amber-400" : "text-emerald-300"
|
|
43
|
+
}
|
|
44
|
+
>
|
|
45
|
+
{participant.mutedVideo ? "Video muted" : "Video on"}
|
|
46
|
+
</span>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
);
|
|
50
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { FC } from "react";
|
|
2
|
+
import type { RosterEntry } from "./types";
|
|
3
|
+
|
|
4
|
+
export const RosterList: FC<{ roster: RosterEntry[]; selfId?: string }> = ({
|
|
5
|
+
roster,
|
|
6
|
+
selfId,
|
|
7
|
+
}) => {
|
|
8
|
+
return (
|
|
9
|
+
<div className="grid gap-2">
|
|
10
|
+
{roster.length === 0 && (
|
|
11
|
+
<div className="text-slate-400 text-sm">No peers joined yet</div>
|
|
12
|
+
)}
|
|
13
|
+
{roster.map((entry) => {
|
|
14
|
+
const tone =
|
|
15
|
+
entry.id === selfId
|
|
16
|
+
? "bg-emerald-100 text-emerald-900"
|
|
17
|
+
: "bg-slate-800 text-slate-200";
|
|
18
|
+
return (
|
|
19
|
+
<div
|
|
20
|
+
key={entry.id}
|
|
21
|
+
className={`flex items-center justify-between rounded-lg border border-slate-800 px-3 py-2 ${tone}`}
|
|
22
|
+
>
|
|
23
|
+
<span className="font-semibold truncate">
|
|
24
|
+
{entry.label ?? entry.id}
|
|
25
|
+
</span>
|
|
26
|
+
<span className="text-xs opacity-80">
|
|
27
|
+
{entry.id === selfId ? "You" : entry.status ?? "Peer"}
|
|
28
|
+
</span>
|
|
29
|
+
</div>
|
|
30
|
+
);
|
|
31
|
+
})}
|
|
32
|
+
</div>
|
|
33
|
+
);
|
|
34
|
+
};
|
package/src/icons.tsx
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { SVGProps } from "react";
|
|
2
|
+
|
|
3
|
+
export const VideoIcon = (props: SVGProps<SVGSVGElement>) => (
|
|
4
|
+
<svg
|
|
5
|
+
viewBox="0 0 24 24"
|
|
6
|
+
fill="none"
|
|
7
|
+
stroke="currentColor"
|
|
8
|
+
strokeWidth="1.6"
|
|
9
|
+
{...props}
|
|
10
|
+
>
|
|
11
|
+
<path d="M3 7.5A2.5 2.5 0 0 1 5.5 5h7A2.5 2.5 0 0 1 15 7.5v9A2.5 2.5 0 0 1 12.5 19h-7A2.5 2.5 0 0 1 3 16.5v-9Z" />
|
|
12
|
+
<path d="M15 10.5 20.5 7v10l-5.5-3V10.5Z" />
|
|
13
|
+
</svg>
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
export const MicIcon = (props: SVGProps<SVGSVGElement>) => (
|
|
17
|
+
<svg
|
|
18
|
+
viewBox="0 0 24 24"
|
|
19
|
+
fill="none"
|
|
20
|
+
stroke="currentColor"
|
|
21
|
+
strokeWidth="1.6"
|
|
22
|
+
{...props}
|
|
23
|
+
>
|
|
24
|
+
<rect x="7" y="4" width="10" height="12" rx="3" />
|
|
25
|
+
<path d="M5 11v1a7 7 0 0 0 14 0v-1" />
|
|
26
|
+
<path d="M12 19v2" />
|
|
27
|
+
</svg>
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
export const PhoneIcon = (props: SVGProps<SVGSVGElement>) => (
|
|
31
|
+
<svg
|
|
32
|
+
viewBox="0 0 24 24"
|
|
33
|
+
fill="none"
|
|
34
|
+
stroke="currentColor"
|
|
35
|
+
strokeWidth="1.6"
|
|
36
|
+
{...props}
|
|
37
|
+
>
|
|
38
|
+
<path d="M5 4h4l1.5 4-2 1a10 10 0 0 0 5.5 5.5l1-2L19 15v4a2 2 0 0 1-2 2 14 14 0 0 1-14-14 2 2 0 0 1 2-2Z" />
|
|
39
|
+
</svg>
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
export const ScreenIcon = (props: SVGProps<SVGSVGElement>) => (
|
|
43
|
+
<svg
|
|
44
|
+
viewBox="0 0 24 24"
|
|
45
|
+
fill="none"
|
|
46
|
+
stroke="currentColor"
|
|
47
|
+
strokeWidth="1.6"
|
|
48
|
+
{...props}
|
|
49
|
+
>
|
|
50
|
+
<rect x="3" y="5" width="18" height="12" rx="2" />
|
|
51
|
+
<path d="M8 19h8" />
|
|
52
|
+
</svg>
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
export const ChatIcon = (props: SVGProps<SVGSVGElement>) => (
|
|
56
|
+
<svg
|
|
57
|
+
viewBox="0 0 24 24"
|
|
58
|
+
fill="none"
|
|
59
|
+
stroke="currentColor"
|
|
60
|
+
strokeWidth="1.6"
|
|
61
|
+
{...props}
|
|
62
|
+
>
|
|
63
|
+
<path d="M5 5h14a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2H9l-4 3v-5H5a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2Z" />
|
|
64
|
+
</svg>
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
export const SignalIcon = (props: SVGProps<SVGSVGElement>) => (
|
|
68
|
+
<svg
|
|
69
|
+
viewBox="0 0 24 24"
|
|
70
|
+
fill="none"
|
|
71
|
+
stroke="currentColor"
|
|
72
|
+
strokeWidth="1.6"
|
|
73
|
+
{...props}
|
|
74
|
+
>
|
|
75
|
+
<path d="M4 18h2v-4H4v4Zm5 0h2V8H9v10Zm5 0h2V4h-2v14Zm5 0h2v-7h-2v7Z" />
|
|
76
|
+
</svg>
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
export const WifiOffIcon = (props: SVGProps<SVGSVGElement>) => (
|
|
80
|
+
<svg
|
|
81
|
+
viewBox="0 0 24 24"
|
|
82
|
+
fill="none"
|
|
83
|
+
stroke="currentColor"
|
|
84
|
+
strokeWidth="1.6"
|
|
85
|
+
{...props}
|
|
86
|
+
>
|
|
87
|
+
<path d="m3 3 18 18M7.5 7.1A13 13 0 0 1 12 6c1.9 0 3.7.4 5.4 1.2M5 10.2A10 10 0 0 1 12 8c1.6 0 3.2.3 4.6 1M7 13.3A7 7 0 0 1 12 12c1.4 0 2.6.3 3.8.9M9.5 16.4A3.5 3.5 0 0 1 12 16c.5 0 1 .1 1.5.3" />
|
|
88
|
+
</svg>
|
|
89
|
+
);
|
package/src/index.ts
ADDED
package/src/types.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export type ParticipantView = {
|
|
2
|
+
id: string;
|
|
3
|
+
label?: string;
|
|
4
|
+
stream?: MediaStream | null;
|
|
5
|
+
connectionState?: RTCPeerConnectionState;
|
|
6
|
+
iceState?: RTCIceConnectionState;
|
|
7
|
+
mutedAudio?: boolean;
|
|
8
|
+
mutedVideo?: boolean;
|
|
9
|
+
isLocal?: boolean;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type RosterEntry = {
|
|
13
|
+
id: string;
|
|
14
|
+
label?: string;
|
|
15
|
+
status?: "self" | "peer" | "connecting";
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type ControlHandlers = {
|
|
19
|
+
onJoin?: () => void;
|
|
20
|
+
onLeave?: () => void;
|
|
21
|
+
onMuteAudio?: (mute: boolean) => void;
|
|
22
|
+
onMuteVideo?: (mute: boolean) => void;
|
|
23
|
+
onScreenShare?: (enable: boolean) => void;
|
|
24
|
+
onReset?: () => void;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type ConnectionStatus = {
|
|
28
|
+
signaling?: "idle" | "connecting" | "open" | "closed";
|
|
29
|
+
media?: "off" | "requesting" | "ready";
|
|
30
|
+
data?: "idle" | "ready";
|
|
31
|
+
};
|