Skip to content

Commit 628c94d

Browse files
update /ui
1 parent 260f8bf commit 628c94d

File tree

8 files changed

+506
-293
lines changed

8 files changed

+506
-293
lines changed

app/ui/(landing-page)/page.tsx

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
'use client';
2+
3+
import { AgentControlBar } from '@/components/livekit/agent-control-bar/agent-control-bar';
4+
import { Button } from '@/components/livekit/button';
5+
import { ChatEntry } from '@/components/livekit/chat-entry';
6+
7+
export default function Page() {
8+
return (
9+
<>
10+
<header className="grid h-96 place-content-center space-y-6 text-center">
11+
<h1 className="text-5xl">
12+
<span className="font-bold tracking-tighter">LiveKit</span>{' '}
13+
<span className="font-light tracking-tighter">UI</span>
14+
</h1>
15+
<p className="text-lg text-pretty">
16+
A set of Open Source UI components for
17+
<br />
18+
building beautiful voice experiences.
19+
</p>
20+
<div className="flex justify-center gap-4">
21+
<Button variant="primary" asChild>
22+
<a href="/ui/components">View components</a>
23+
</Button>
24+
<Button variant="ghost" asChild>
25+
<a href="https://docs.livekit.io/agents/start/frontend/">Read our docs</a>
26+
</Button>
27+
</div>
28+
</header>
29+
30+
<main className="mx-auto max-w-3xl space-y-8">
31+
<div className="border-border bg-background h-96 rounded-3xl border p-8">
32+
<div className="flex h-full flex-col gap-4">
33+
<div className="flex-1 grow">
34+
<ChatEntry
35+
locale="en-US"
36+
name="User"
37+
message="Hello, how are you?"
38+
messageOrigin="local"
39+
timestamp={1761096559966}
40+
/>
41+
<ChatEntry
42+
locale="en-US"
43+
name="Agent"
44+
message="I am good, how about you?"
45+
messageOrigin="remote"
46+
timestamp={1761096569216}
47+
/>
48+
</div>
49+
<AgentControlBar
50+
className="w-full"
51+
controls={{
52+
leave: true,
53+
chat: true,
54+
camera: true,
55+
microphone: true,
56+
screenShare: true,
57+
}}
58+
/>
59+
</div>
60+
</div>
61+
</main>
62+
</>
63+
);
64+
}

app/ui/README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
THIS IS NOT PART OF THE MAIN APPLICATION CODE.
2+
3+
This folder contains code for testing and previewing LiveKit's UI component library in isolation.
4+
5+
## Getting started
6+
7+
To run the development server, run the following command:
8+
9+
```bash
10+
npm run dev
11+
```
12+
13+
Then, navigate to `http://localhost:3000/ui` to see the components.

app/ui/_components.tsx

Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
'use client';
2+
3+
import { type VariantProps } from 'class-variance-authority';
4+
import { Track } from 'livekit-client';
5+
import { MicrophoneIcon } from '@phosphor-icons/react/dist/ssr';
6+
import { AgentControlBar } from '@/components/livekit/agent-control-bar/agent-control-bar';
7+
import { TrackDeviceSelect } from '@/components/livekit/agent-control-bar/track-device-select';
8+
import { TrackSelector } from '@/components/livekit/agent-control-bar/track-selector';
9+
import { TrackToggle } from '@/components/livekit/agent-control-bar/track-toggle';
10+
import { Alert, AlertDescription, AlertTitle, alertVariants } from '@/components/livekit/alert';
11+
import { AlertToast } from '@/components/livekit/alert-toast';
12+
import { Button, buttonVariants } from '@/components/livekit/button';
13+
import { ChatEntry } from '@/components/livekit/chat-entry';
14+
import {
15+
Select,
16+
SelectContent,
17+
SelectItem,
18+
SelectTrigger,
19+
SelectValue,
20+
} from '@/components/livekit/select';
21+
import { ShimmerText } from '@/components/livekit/shimmer-text';
22+
import { Toggle, toggleVariants } from '@/components/livekit/toggle';
23+
import { cn } from '@/lib/utils';
24+
25+
type toggleVariantsType = VariantProps<typeof toggleVariants>['variant'];
26+
type toggleVariantsSizeType = VariantProps<typeof toggleVariants>['size'];
27+
type buttonVariantsType = VariantProps<typeof buttonVariants>['variant'];
28+
type buttonVariantsSizeType = VariantProps<typeof buttonVariants>['size'];
29+
type alertVariantsType = VariantProps<typeof alertVariants>['variant'];
30+
31+
interface ContainerProps {
32+
componentName: string;
33+
children: React.ReactNode;
34+
className?: string;
35+
}
36+
37+
function Container({ componentName, children, className }: ContainerProps) {
38+
return (
39+
<div className={className}>
40+
<div className="bg-background border-input space-y-4 rounded-3xl border p-8 drop-shadow-lg/5">
41+
{children}
42+
</div>
43+
</div>
44+
);
45+
}
46+
47+
function StoryTitle({ children }: { children: React.ReactNode }) {
48+
return <h4 className="text-muted-foreground mb-2 font-mono text-xs uppercase">{children}</h4>;
49+
}
50+
51+
export const COMPONENTS = {
52+
// Button
53+
Button: () => (
54+
<Container componentName="Button">
55+
<table className="w-full">
56+
<thead className="font-mono text-xs font-normal uppercase [&_th]:w-1/5 [&_th]:p-2 [&_th]:text-center [&_th]:font-normal">
57+
<tr>
58+
<th></th>
59+
<th>Small</th>
60+
<th>Default</th>
61+
<th>Large</th>
62+
<th>Icon</th>
63+
</tr>
64+
</thead>
65+
<tbody className="[&_td]:p-2 [&_td:not(:first-child)]:text-center">
66+
{['default', 'primary', 'secondary', 'outline', 'ghost', 'link', 'destructive'].map(
67+
(variant) => (
68+
<tr key={variant}>
69+
<td className="text-right font-mono text-xs font-normal uppercase">{variant}</td>
70+
{['sm', 'default', 'lg', 'icon'].map((size) => (
71+
<td key={size}>
72+
<Button
73+
variant={variant as buttonVariantsType}
74+
size={size as buttonVariantsSizeType}
75+
>
76+
{size === 'icon' ? <MicrophoneIcon size={16} weight="bold" /> : 'Button'}
77+
</Button>
78+
</td>
79+
))}
80+
</tr>
81+
)
82+
)}
83+
</tbody>
84+
</table>
85+
</Container>
86+
),
87+
88+
// Toggle
89+
Toggle: () => (
90+
<Container componentName="Toggle">
91+
<table className="w-full">
92+
<thead className="font-mono text-xs font-normal uppercase [&_th]:w-1/5 [&_th]:p-2 [&_th]:text-center [&_th]:font-normal">
93+
<tr>
94+
<th></th>
95+
<th>Small</th>
96+
<th>Default</th>
97+
<th>Large</th>
98+
<th>Icon</th>
99+
</tr>
100+
</thead>
101+
<tbody className="[&_td]:p-2 [&_td:not(:first-child)]:text-center">
102+
{['default', 'primary', 'secondary', 'outline'].map((variant) => (
103+
<tr key={variant}>
104+
<td className="text-right font-mono text-xs font-normal uppercase">{variant}</td>
105+
{['sm', 'default', 'lg', 'icon'].map((size) => (
106+
<td key={size}>
107+
<Toggle
108+
size={size as toggleVariantsSizeType}
109+
variant={variant as toggleVariantsType}
110+
>
111+
{size === 'icon' ? <MicrophoneIcon size={16} weight="bold" /> : 'Toggle'}
112+
</Toggle>
113+
</td>
114+
))}
115+
</tr>
116+
))}
117+
</tbody>
118+
</table>
119+
</Container>
120+
),
121+
122+
// Alert
123+
Alert: () => (
124+
<Container componentName="Alert">
125+
{['default', 'destructive'].map((variant) => (
126+
<div key={variant}>
127+
<StoryTitle>{variant}</StoryTitle>
128+
<Alert key={variant} variant={variant as alertVariantsType}>
129+
<AlertTitle>Alert {variant} title</AlertTitle>
130+
<AlertDescription>This is a {variant} alert description.</AlertDescription>
131+
</Alert>
132+
</div>
133+
))}
134+
</Container>
135+
),
136+
137+
// Select
138+
Select: () => (
139+
<Container componentName="Select">
140+
<div className="grid w-full grid-cols-2 gap-2">
141+
<div>
142+
<StoryTitle>Size default</StoryTitle>
143+
<Select>
144+
<SelectTrigger>
145+
<SelectValue placeholder="Select a track" />
146+
</SelectTrigger>
147+
<SelectContent>
148+
<SelectItem value="1">Track 1</SelectItem>
149+
<SelectItem value="2">Track 2</SelectItem>
150+
<SelectItem value="3">Track 3</SelectItem>
151+
</SelectContent>
152+
</Select>
153+
</div>
154+
<div>
155+
<StoryTitle>Size sm</StoryTitle>
156+
<Select>
157+
<SelectTrigger size="sm">
158+
<SelectValue placeholder="Select a track" />
159+
</SelectTrigger>
160+
<SelectContent>
161+
<SelectItem value="1">Track 1</SelectItem>
162+
<SelectItem value="2">Track 2</SelectItem>
163+
<SelectItem value="3">Track 3</SelectItem>
164+
</SelectContent>
165+
</Select>
166+
</div>
167+
</div>
168+
</Container>
169+
),
170+
171+
// Agent control bar
172+
AgentControlBar: () => (
173+
<Container componentName="AgentControlBar">
174+
<div className="relative flex items-center justify-center">
175+
<AgentControlBar
176+
className="w-full"
177+
controls={{
178+
leave: true,
179+
chat: true,
180+
camera: true,
181+
microphone: true,
182+
screenShare: true,
183+
}}
184+
/>
185+
</div>
186+
</Container>
187+
),
188+
189+
// Track device select
190+
TrackDeviceSelect: () => (
191+
<Container componentName="TrackDeviceSelect">
192+
<div className="grid grid-cols-2 gap-4">
193+
<div>
194+
<StoryTitle>Size default</StoryTitle>
195+
<TrackDeviceSelect kind="audioinput" />
196+
</div>
197+
<div>
198+
<StoryTitle>Size sm</StoryTitle>
199+
<TrackDeviceSelect size="sm" kind="audioinput" />
200+
</div>
201+
</div>
202+
</Container>
203+
),
204+
205+
// Track toggle
206+
TrackToggle: () => (
207+
<Container componentName="TrackToggle">
208+
<div className="grid grid-cols-2 gap-4">
209+
<div>
210+
<StoryTitle>Track.Source.Microphone</StoryTitle>
211+
<TrackToggle variant="outline" source={Track.Source.Microphone} />
212+
</div>
213+
<div>
214+
<StoryTitle>Track.Source.Camera</StoryTitle>
215+
<TrackToggle variant="outline" source={Track.Source.Camera} />
216+
</div>
217+
</div>
218+
</Container>
219+
),
220+
221+
// Track selector
222+
TrackSelector: () => (
223+
<Container componentName="TrackSelector">
224+
<div className="grid grid-cols-2 gap-4">
225+
<div>
226+
<StoryTitle>Track.Source.Camera</StoryTitle>
227+
<TrackSelector kind="videoinput" source={Track.Source.Camera} />
228+
</div>
229+
<div>
230+
<StoryTitle>Track.Source.Microphone</StoryTitle>
231+
<TrackSelector kind="audioinput" source={Track.Source.Microphone} />
232+
</div>
233+
</div>
234+
</Container>
235+
),
236+
237+
// Chat entry
238+
ChatEntry: () => (
239+
<Container componentName="ChatEntry">
240+
<div className="mx-auto max-w-prose space-y-4">
241+
<ChatEntry
242+
locale="en-US"
243+
timestamp={Date.now() + 1000}
244+
message="Hello, how are you?"
245+
messageOrigin="local"
246+
name="User"
247+
/>
248+
<ChatEntry
249+
locale="en-US"
250+
timestamp={Date.now() + 5000}
251+
message="I am good, how about you?"
252+
messageOrigin="remote"
253+
name="Agent"
254+
/>
255+
</div>
256+
</Container>
257+
),
258+
259+
// Shimmer text
260+
ShimmerText: () => (
261+
<Container componentName="ShimmerText">
262+
<div className="text-center">
263+
<ShimmerText>This is shimmer text</ShimmerText>
264+
</div>
265+
</Container>
266+
),
267+
268+
// Alert toast
269+
AlertToast: () => (
270+
<Container componentName="AlertToast">
271+
<StoryTitle>Alert toast</StoryTitle>
272+
<div className="mx-auto max-w-prose">
273+
<AlertToast
274+
id="alert-toast"
275+
title="Alert toast"
276+
description="This is a alert toast description."
277+
/>
278+
</div>
279+
</Container>
280+
),
281+
};
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
'use client';
2+
3+
import { redirect, useParams } from 'next/navigation';
4+
import { COMPONENTS } from '../../_components';
5+
6+
export default function Page() {
7+
const { slug = [] } = useParams();
8+
const [componentName] = slug;
9+
const component = COMPONENTS[componentName as keyof typeof COMPONENTS];
10+
11+
if (!component) {
12+
return redirect('/ui');
13+
}
14+
15+
return (
16+
<>
17+
<div className="py-8">
18+
<h1 className="text-foreground mb-8 text-5xl font-bold">{componentName}</h1>
19+
{component()}
20+
</div>
21+
</>
22+
);
23+
}

0 commit comments

Comments
 (0)