@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 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
@@ -0,0 +1,8 @@
1
+ export * from "./types";
2
+ export * from "./ConnectionBadge";
3
+ export * from "./ParticipantTile";
4
+ export * from "./ParticipantGrid";
5
+ export * from "./RosterList";
6
+ export * from "./ControlBar";
7
+ export * from "./ChatPanel";
8
+ export * from "./icons";
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
+ };
package/tsconfig.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "rootDir": "src",
5
+ "outDir": "dist",
6
+ "jsx": "react-jsx"
7
+ },
8
+ "include": ["src/**/*"]
9
+ }