@conference-kit/ui-vue 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 +27 -0
- package/src/ChatPanel.vue +71 -0
- package/src/ConnectionBadge.vue +57 -0
- package/src/ControlBar.vue +73 -0
- package/src/ParticipantGrid.vue +24 -0
- package/src/ParticipantTile.vue +55 -0
- package/src/RosterList.vue +28 -0
- package/src/icons.ts +36 -0
- package/src/index.ts +6 -0
- package/tsconfig.json +9 -0
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@conference-kit/ui-vue",
|
|
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
|
+
"vue": "^3.4.0",
|
|
18
|
+
"tailwindcss": ">=3.4.0"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@conference-kit/vue": "^0.0.1",
|
|
22
|
+
"@conference-kit/core": "^0.0.1"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"typescript": "^5.9.3"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="bg-slate-900 border border-slate-800 rounded-xl p-4 grid gap-3">
|
|
3
|
+
<div class="flex items-center justify-between">
|
|
4
|
+
<div class="flex items-center gap-2 text-slate-100 font-semibold">
|
|
5
|
+
<ChatIcon class="h-5 w-5" /> Data Channel
|
|
6
|
+
</div>
|
|
7
|
+
<span
|
|
8
|
+
:class="[
|
|
9
|
+
'text-xs px-2 py-1 rounded-full',
|
|
10
|
+
ready
|
|
11
|
+
? 'bg-emerald-100 text-emerald-800'
|
|
12
|
+
: 'bg-slate-200 text-slate-700',
|
|
13
|
+
]"
|
|
14
|
+
>
|
|
15
|
+
{{ ready ? "Open" : "Closed" }}
|
|
16
|
+
</span>
|
|
17
|
+
</div>
|
|
18
|
+
<form class="flex gap-2" @submit.prevent="submit">
|
|
19
|
+
<input
|
|
20
|
+
class="flex-1 rounded-lg border border-slate-700 bg-slate-950 px-3 py-2 text-slate-100"
|
|
21
|
+
v-model="text"
|
|
22
|
+
placeholder="Message"
|
|
23
|
+
/>
|
|
24
|
+
<button
|
|
25
|
+
type="submit"
|
|
26
|
+
:disabled="!ready"
|
|
27
|
+
class="px-3 py-2 rounded-lg bg-emerald-600 text-slate-50 font-semibold disabled:opacity-50"
|
|
28
|
+
>
|
|
29
|
+
Send
|
|
30
|
+
</button>
|
|
31
|
+
</form>
|
|
32
|
+
<div class="max-h-64 overflow-auto grid gap-2">
|
|
33
|
+
<div v-if="!messages.length" class="text-slate-400 text-sm">
|
|
34
|
+
No messages yet
|
|
35
|
+
</div>
|
|
36
|
+
<div
|
|
37
|
+
v-for="msg in messages"
|
|
38
|
+
:key="msg.id"
|
|
39
|
+
:class="[
|
|
40
|
+
'flex items-center justify-between rounded-lg px-3 py-2 border',
|
|
41
|
+
msg.direction === 'in'
|
|
42
|
+
? 'bg-slate-800 border-slate-700'
|
|
43
|
+
: 'bg-emerald-50 border-emerald-200',
|
|
44
|
+
]"
|
|
45
|
+
>
|
|
46
|
+
<span class="text-sm text-slate-100">{{ msg.text }}</span>
|
|
47
|
+
<span class="text-xs text-slate-500">{{
|
|
48
|
+
msg.direction === "in" ? "In" : "Out"
|
|
49
|
+
}}</span>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
</template>
|
|
54
|
+
|
|
55
|
+
<script setup lang="ts">
|
|
56
|
+
import { ref } from "vue";
|
|
57
|
+
import { ChatIcon } from "./icons";
|
|
58
|
+
|
|
59
|
+
type ChatMessage = { id: string; direction: "in" | "out"; text: string };
|
|
60
|
+
|
|
61
|
+
const props = defineProps<{ messages: ChatMessage[]; ready: boolean }>();
|
|
62
|
+
const emit = defineEmits<{ send: [text: string] }>();
|
|
63
|
+
|
|
64
|
+
const text = ref("ping");
|
|
65
|
+
|
|
66
|
+
function submit() {
|
|
67
|
+
if (!text.value.trim()) return;
|
|
68
|
+
emit("send", text.value.trim());
|
|
69
|
+
text.value = "";
|
|
70
|
+
}
|
|
71
|
+
</script>
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="flex flex-wrap gap-2 text-sm">
|
|
3
|
+
<span
|
|
4
|
+
:class="[
|
|
5
|
+
'inline-flex items-center gap-1 rounded-full px-2.5 py-1',
|
|
6
|
+
signalTone,
|
|
7
|
+
]"
|
|
8
|
+
>
|
|
9
|
+
<SignalIcon class="h-4 w-4" /> Signal {{ status.signaling ?? "idle" }}
|
|
10
|
+
</span>
|
|
11
|
+
<span
|
|
12
|
+
:class="[
|
|
13
|
+
'inline-flex items-center gap-1 rounded-full px-2.5 py-1',
|
|
14
|
+
mediaTone,
|
|
15
|
+
]"
|
|
16
|
+
>
|
|
17
|
+
<WifiOffIcon class="h-4 w-4" /> Media {{ status.media ?? "off" }}
|
|
18
|
+
</span>
|
|
19
|
+
<span
|
|
20
|
+
:class="[
|
|
21
|
+
'inline-flex items-center gap-1 rounded-full px-2.5 py-1',
|
|
22
|
+
dataTone,
|
|
23
|
+
]"
|
|
24
|
+
>
|
|
25
|
+
<SignalIcon class="h-4 w-4" /> Data {{ status.data ?? "idle" }}
|
|
26
|
+
</span>
|
|
27
|
+
</div>
|
|
28
|
+
</template>
|
|
29
|
+
|
|
30
|
+
<script setup lang="ts">
|
|
31
|
+
import { computed } from "vue";
|
|
32
|
+
import { SignalIcon, WifiOffIcon } from "./icons";
|
|
33
|
+
|
|
34
|
+
type Status = {
|
|
35
|
+
signaling?: "idle" | "connecting" | "open" | "closed";
|
|
36
|
+
media?: "off" | "requesting" | "ready";
|
|
37
|
+
data?: "idle" | "ready";
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const props = defineProps<{ status: Status }>();
|
|
41
|
+
|
|
42
|
+
const signalTone = computed(() =>
|
|
43
|
+
props.status.signaling === "open"
|
|
44
|
+
? "bg-emerald-100 text-emerald-800"
|
|
45
|
+
: "bg-amber-100 text-amber-800"
|
|
46
|
+
);
|
|
47
|
+
const mediaTone = computed(() => {
|
|
48
|
+
if (props.status.media === "ready") return "bg-emerald-100 text-emerald-800";
|
|
49
|
+
if (props.status.media === "requesting") return "bg-amber-100 text-amber-800";
|
|
50
|
+
return "bg-slate-200 text-slate-700";
|
|
51
|
+
});
|
|
52
|
+
const dataTone = computed(() =>
|
|
53
|
+
props.status.data === "ready"
|
|
54
|
+
? "bg-emerald-100 text-emerald-800"
|
|
55
|
+
: "bg-slate-200 text-slate-700"
|
|
56
|
+
);
|
|
57
|
+
</script>
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
class="flex flex-wrap items-center justify-between gap-3 bg-slate-900 border border-slate-800 rounded-xl px-4 py-3"
|
|
4
|
+
>
|
|
5
|
+
<div class="flex flex-wrap gap-2">
|
|
6
|
+
<button class="btn-join" :disabled="joined" @click="$emit('join')">
|
|
7
|
+
<VideoIcon class="h-4 w-4" /> Join
|
|
8
|
+
</button>
|
|
9
|
+
<button class="btn-ghost" :disabled="!joined" @click="$emit('leave')">
|
|
10
|
+
<PhoneIcon class="h-4 w-4" /> Leave
|
|
11
|
+
</button>
|
|
12
|
+
<button
|
|
13
|
+
class="btn-ghost"
|
|
14
|
+
:disabled="!joined || !mediaReady"
|
|
15
|
+
@click="$emit('toggle-audio')"
|
|
16
|
+
>
|
|
17
|
+
<MicIcon class="h-4 w-4" /> {{ audioMuted ? "Unmute" : "Mute" }}
|
|
18
|
+
</button>
|
|
19
|
+
<button
|
|
20
|
+
class="btn-ghost"
|
|
21
|
+
:disabled="!joined || !mediaReady"
|
|
22
|
+
@click="$emit('toggle-video')"
|
|
23
|
+
>
|
|
24
|
+
<VideoIcon class="h-4 w-4" />
|
|
25
|
+
{{ videoMuted ? "Start video" : "Stop video" }}
|
|
26
|
+
</button>
|
|
27
|
+
<button
|
|
28
|
+
class="btn-ghost"
|
|
29
|
+
:disabled="!joined"
|
|
30
|
+
@click="$emit('toggle-screen')"
|
|
31
|
+
>
|
|
32
|
+
<ScreenIcon class="h-4 w-4" />
|
|
33
|
+
{{ screenSharing ? "Stop share" : "Share screen" }}
|
|
34
|
+
</button>
|
|
35
|
+
<button class="btn-ghost" @click="$emit('reset')">
|
|
36
|
+
<ChatIcon class="h-4 w-4" /> Reset
|
|
37
|
+
</button>
|
|
38
|
+
</div>
|
|
39
|
+
<div class="flex items-center gap-2">
|
|
40
|
+
<slot name="right"></slot>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
</template>
|
|
44
|
+
|
|
45
|
+
<script setup lang="ts">
|
|
46
|
+
import { ChatIcon, MicIcon, PhoneIcon, ScreenIcon, VideoIcon } from "./icons";
|
|
47
|
+
|
|
48
|
+
defineProps<{
|
|
49
|
+
joined?: boolean;
|
|
50
|
+
mediaReady?: boolean;
|
|
51
|
+
audioMuted?: boolean;
|
|
52
|
+
videoMuted?: boolean;
|
|
53
|
+
screenSharing?: boolean;
|
|
54
|
+
}>();
|
|
55
|
+
|
|
56
|
+
defineEmits([
|
|
57
|
+
"join",
|
|
58
|
+
"leave",
|
|
59
|
+
"toggle-audio",
|
|
60
|
+
"toggle-video",
|
|
61
|
+
"toggle-screen",
|
|
62
|
+
"reset",
|
|
63
|
+
]);
|
|
64
|
+
</script>
|
|
65
|
+
|
|
66
|
+
<style scoped>
|
|
67
|
+
.btn-join {
|
|
68
|
+
@apply inline-flex items-center gap-2 rounded-lg px-3 py-2 text-sm font-semibold border border-emerald-500 text-emerald-100 bg-emerald-600/80 hover:bg-emerald-600 disabled:opacity-50;
|
|
69
|
+
}
|
|
70
|
+
.btn-ghost {
|
|
71
|
+
@apply inline-flex items-center gap-2 rounded-lg px-3 py-2 text-sm font-semibold border border-slate-700 text-slate-100 bg-slate-800 hover:bg-slate-700 disabled:opacity-50;
|
|
72
|
+
}
|
|
73
|
+
</style>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
|
3
|
+
<ParticipantTile v-for="p in participants" :key="p.id" :participant="p" />
|
|
4
|
+
<div v-if="!participants.length" class="text-slate-400 text-sm">
|
|
5
|
+
Waiting for participants…
|
|
6
|
+
</div>
|
|
7
|
+
</div>
|
|
8
|
+
</template>
|
|
9
|
+
|
|
10
|
+
<script setup lang="ts">
|
|
11
|
+
import ParticipantTile from "./ParticipantTile.vue";
|
|
12
|
+
|
|
13
|
+
type Participant = {
|
|
14
|
+
id: string;
|
|
15
|
+
label?: string;
|
|
16
|
+
stream?: MediaStream | null;
|
|
17
|
+
connectionState?: RTCPeerConnectionState;
|
|
18
|
+
mutedAudio?: boolean;
|
|
19
|
+
mutedVideo?: boolean;
|
|
20
|
+
isLocal?: boolean;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
defineProps<{ participants: Participant[] }>();
|
|
24
|
+
</script>
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="bg-slate-900 border border-slate-800 rounded-xl p-3 grid gap-2">
|
|
3
|
+
<div class="flex items-center justify-between gap-2 text-sm text-slate-200">
|
|
4
|
+
<div class="font-semibold truncate">
|
|
5
|
+
{{ participant.label ?? participant.id }}
|
|
6
|
+
</div>
|
|
7
|
+
<span class="text-xs text-slate-400">{{
|
|
8
|
+
participant.connectionState ?? "new"
|
|
9
|
+
}}</span>
|
|
10
|
+
</div>
|
|
11
|
+
<video
|
|
12
|
+
class="w-full aspect-video rounded-lg bg-slate-950"
|
|
13
|
+
:muted="participant.isLocal"
|
|
14
|
+
autoplay
|
|
15
|
+
playsinline
|
|
16
|
+
ref="bindVideo"
|
|
17
|
+
></video>
|
|
18
|
+
<div class="flex items-center gap-3 text-xs text-slate-400">
|
|
19
|
+
<span
|
|
20
|
+
:class="participant.mutedAudio ? 'text-amber-400' : 'text-emerald-300'"
|
|
21
|
+
>
|
|
22
|
+
{{ participant.mutedAudio ? "Audio muted" : "Audio on" }}
|
|
23
|
+
</span>
|
|
24
|
+
<span
|
|
25
|
+
:class="participant.mutedVideo ? 'text-amber-400' : 'text-emerald-300'"
|
|
26
|
+
>
|
|
27
|
+
{{ participant.mutedVideo ? "Video muted" : "Video on" }}
|
|
28
|
+
</span>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
</template>
|
|
32
|
+
|
|
33
|
+
<script setup lang="ts">
|
|
34
|
+
type Participant = {
|
|
35
|
+
id: string;
|
|
36
|
+
label?: string;
|
|
37
|
+
stream?: MediaStream | null;
|
|
38
|
+
connectionState?: RTCPeerConnectionState;
|
|
39
|
+
mutedAudio?: boolean;
|
|
40
|
+
mutedVideo?: boolean;
|
|
41
|
+
isLocal?: boolean;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const props = defineProps<{ participant: Participant }>();
|
|
45
|
+
|
|
46
|
+
function bindVideo(el: HTMLVideoElement | null) {
|
|
47
|
+
if (!el) return;
|
|
48
|
+
if (props.participant.stream && el.srcObject !== props.participant.stream) {
|
|
49
|
+
el.srcObject = props.participant.stream;
|
|
50
|
+
}
|
|
51
|
+
if (!props.participant.stream && el.srcObject) {
|
|
52
|
+
el.srcObject = null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
</script>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="grid gap-2">
|
|
3
|
+
<div v-if="!roster.length" class="text-slate-400 text-sm">
|
|
4
|
+
No peers joined yet
|
|
5
|
+
</div>
|
|
6
|
+
<div
|
|
7
|
+
v-for="entry in roster"
|
|
8
|
+
:key="entry.id"
|
|
9
|
+
:class="[
|
|
10
|
+
'flex items-center justify-between rounded-lg border px-3 py-2',
|
|
11
|
+
entry.id === selfId
|
|
12
|
+
? 'bg-emerald-100 border-emerald-200 text-emerald-900'
|
|
13
|
+
: 'bg-slate-800 border-slate-700 text-slate-200',
|
|
14
|
+
]"
|
|
15
|
+
>
|
|
16
|
+
<span class="font-semibold truncate">{{ entry.label ?? entry.id }}</span>
|
|
17
|
+
<span class="text-xs opacity-80">{{
|
|
18
|
+
entry.id === selfId ? "You" : entry.status ?? "Peer"
|
|
19
|
+
}}</span>
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
</template>
|
|
23
|
+
|
|
24
|
+
<script setup lang="ts">
|
|
25
|
+
type RosterEntry = { id: string; label?: string; status?: string };
|
|
26
|
+
|
|
27
|
+
defineProps<{ roster: RosterEntry[]; selfId?: string }>();
|
|
28
|
+
</script>
|
package/src/icons.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { h, type FunctionalComponent, type SVGAttributes } from "vue";
|
|
2
|
+
|
|
3
|
+
function makeIcon(path: string): FunctionalComponent<SVGAttributes> {
|
|
4
|
+
return (props) =>
|
|
5
|
+
h(
|
|
6
|
+
"svg",
|
|
7
|
+
{
|
|
8
|
+
viewBox: "0 0 24 24",
|
|
9
|
+
fill: "none",
|
|
10
|
+
stroke: "currentColor",
|
|
11
|
+
strokeWidth: 1.6,
|
|
12
|
+
...props,
|
|
13
|
+
},
|
|
14
|
+
[h("path", { d: path })]
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const SignalIcon = makeIcon(
|
|
19
|
+
"M4 18h2v-4H4v4Zm5 0h2V8H9v10Zm5 0h2V4h-2v14Zm5 0h2v-7h-2v7Z"
|
|
20
|
+
);
|
|
21
|
+
export const WifiOffIcon = makeIcon(
|
|
22
|
+
"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"
|
|
23
|
+
);
|
|
24
|
+
export const MicIcon = makeIcon(
|
|
25
|
+
"M7 4a5 5 0 1 1 10 0v6a5 5 0 1 1-10 0V4Z M5 11v1a7 7 0 0 0 14 0v-1 M12 19v2"
|
|
26
|
+
);
|
|
27
|
+
export const VideoIcon = makeIcon(
|
|
28
|
+
"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 M15 10.5 20.5 7v10l-5.5-3V10.5Z"
|
|
29
|
+
);
|
|
30
|
+
export const PhoneIcon = makeIcon(
|
|
31
|
+
"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"
|
|
32
|
+
);
|
|
33
|
+
export const ScreenIcon = makeIcon("M3 5h18v12H3z M8 19h8");
|
|
34
|
+
export const ChatIcon = makeIcon(
|
|
35
|
+
"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"
|
|
36
|
+
);
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { default as ConnectionBadge } from "./ConnectionBadge.vue";
|
|
2
|
+
export { default as ParticipantTile } from "./ParticipantTile.vue";
|
|
3
|
+
export { default as ParticipantGrid } from "./ParticipantGrid.vue";
|
|
4
|
+
export { default as RosterList } from "./RosterList.vue";
|
|
5
|
+
export { default as ControlBar } from "./ControlBar.vue";
|
|
6
|
+
export { default as ChatPanel } from "./ChatPanel.vue";
|