aytoasty commited on
Commit
33349d7
·
1 Parent(s): acbd8aa

feat: implement Ethos branding, extract Navbar, and polish Studio UI

Browse files
.context/technical_spec.md CHANGED
@@ -1,5 +1,5 @@
1
  ## core components
2
- - **base model:** mistral voxtral.
3
  - **tracking and evaluation:** weights and biases.
4
  - **platform:** hugging face for model and adapter hosting.
5
 
 
1
  ## core components
2
+ - **base model:** Ethostral (fine-tuned Mistral).
3
  - **tracking and evaluation:** weights and biases.
4
  - **platform:** hugging face for model and adapter hosting.
5
 
demo/src/app/globals.css CHANGED
@@ -48,72 +48,94 @@
48
  }
49
 
50
  :root {
51
- --radius: 0.625rem;
52
- --background: oklch(1 0 0);
53
- --foreground: oklch(0.145 0 0);
 
 
 
 
54
  --card: oklch(1 0 0);
55
- --card-foreground: oklch(0.145 0 0);
 
 
56
  --popover: oklch(1 0 0);
57
- --popover-foreground: oklch(0.145 0 0);
58
- --primary: oklch(0.205 0 0);
59
- --primary-foreground: oklch(0.985 0 0);
60
- --secondary: oklch(0.97 0 0);
61
- --secondary-foreground: oklch(0.205 0 0);
62
- --muted: oklch(0.97 0 0);
63
- --muted-foreground: oklch(0.556 0 0);
64
- --accent: oklch(0.97 0 0);
65
- --accent-foreground: oklch(0.205 0 0);
 
 
 
 
 
 
 
 
 
 
66
  --destructive: oklch(0.577 0.245 27.325);
67
- --border: oklch(0.922 0 0);
68
- --input: oklch(0.922 0 0);
69
- --ring: oklch(0.708 0 0);
70
- --chart-1: oklch(0.646 0.222 41.116);
71
- --chart-2: oklch(0.6 0.118 184.704);
72
- --chart-3: oklch(0.398 0.07 227.392);
73
- --chart-4: oklch(0.828 0.189 84.429);
74
- --chart-5: oklch(0.769 0.188 70.08);
75
- --sidebar: oklch(0.985 0 0);
76
- --sidebar-foreground: oklch(0.145 0 0);
77
- --sidebar-primary: oklch(0.205 0 0);
78
- --sidebar-primary-foreground: oklch(0.985 0 0);
79
- --sidebar-accent: oklch(0.97 0 0);
80
- --sidebar-accent-foreground: oklch(0.205 0 0);
81
- --sidebar-border: oklch(0.922 0 0);
82
- --sidebar-ring: oklch(0.708 0 0);
 
 
 
 
 
 
83
  }
84
 
85
  .dark {
86
- --background: oklch(0.145 0 0);
87
- --foreground: oklch(0.985 0 0);
88
- --card: oklch(0.205 0 0);
89
- --card-foreground: oklch(0.985 0 0);
90
- --popover: oklch(0.205 0 0);
91
- --popover-foreground: oklch(0.985 0 0);
92
- --primary: oklch(0.922 0 0);
93
- --primary-foreground: oklch(0.205 0 0);
94
- --secondary: oklch(0.269 0 0);
95
- --secondary-foreground: oklch(0.985 0 0);
96
- --muted: oklch(0.269 0 0);
97
- --muted-foreground: oklch(0.708 0 0);
98
- --accent: oklch(0.269 0 0);
99
- --accent-foreground: oklch(0.985 0 0);
100
  --destructive: oklch(0.704 0.191 22.216);
101
  --border: oklch(1 0 0 / 10%);
102
  --input: oklch(1 0 0 / 15%);
103
- --ring: oklch(0.556 0 0);
104
- --chart-1: oklch(0.488 0.243 264.376);
105
- --chart-2: oklch(0.696 0.17 162.48);
106
- --chart-3: oklch(0.769 0.188 70.08);
107
- --chart-4: oklch(0.627 0.265 303.9);
108
- --chart-5: oklch(0.645 0.246 16.439);
109
- --sidebar: oklch(0.205 0 0);
110
- --sidebar-foreground: oklch(0.985 0 0);
111
- --sidebar-primary: oklch(0.488 0.243 264.376);
112
- --sidebar-primary-foreground: oklch(0.985 0 0);
113
- --sidebar-accent: oklch(0.269 0 0);
114
- --sidebar-accent-foreground: oklch(0.985 0 0);
115
  --sidebar-border: oklch(1 0 0 / 10%);
116
- --sidebar-ring: oklch(0.556 0 0);
117
  }
118
 
119
  @layer base {
 
48
  }
49
 
50
  :root {
51
+ --radius: 0.875rem;
52
+
53
+ /* Background / Surface */
54
+ --background: oklch(0.985 0 0);
55
+ --foreground: oklch(0.1 0 0);
56
+
57
+ /* Card */
58
  --card: oklch(1 0 0);
59
+ --card-foreground: oklch(0.1 0 0);
60
+
61
+ /* Popover */
62
  --popover: oklch(1 0 0);
63
+ --popover-foreground: oklch(0.1 0 0);
64
+
65
+ /* Primary = Black */
66
+ --primary: oklch(0.1 0 0);
67
+ --primary-foreground: oklch(1 0 0);
68
+
69
+ /* Secondary = Light gray */
70
+ --secondary: oklch(0.94 0 0);
71
+ --secondary-foreground: oklch(0.2 0 0);
72
+
73
+ /* Muted = Subtle surfaces */
74
+ --muted: oklch(0.94 0 0);
75
+ --muted-foreground: oklch(0.55 0 0);
76
+
77
+ /* Accent */
78
+ --accent: oklch(0.92 0 0);
79
+ --accent-foreground: oklch(0.1 0 0);
80
+
81
+ /* Destructive */
82
  --destructive: oklch(0.577 0.245 27.325);
83
+
84
+ /* Border and Input */
85
+ --border: oklch(0.88 0 0);
86
+ --input: oklch(0.88 0 0);
87
+ --ring: oklch(0.6 0 0);
88
+
89
+ /* Charts */
90
+ --chart-1: oklch(0.3 0 0);
91
+ --chart-2: oklch(0.55 0 0);
92
+ --chart-3: oklch(0.45 0.07 227.392);
93
+ --chart-4: oklch(0.7 0.1 84.429);
94
+ --chart-5: oklch(0.5 0.15 16.439);
95
+
96
+ /* Sidebar */
97
+ --sidebar: oklch(0.97 0 0);
98
+ --sidebar-foreground: oklch(0.1 0 0);
99
+ --sidebar-primary: oklch(0.1 0 0);
100
+ --sidebar-primary-foreground: oklch(1 0 0);
101
+ --sidebar-accent: oklch(0.92 0 0);
102
+ --sidebar-accent-foreground: oklch(0.1 0 0);
103
+ --sidebar-border: oklch(0.88 0 0);
104
+ --sidebar-ring: oklch(0.6 0 0);
105
  }
106
 
107
  .dark {
108
+ --background: oklch(0.1 0 0);
109
+ --foreground: oklch(0.97 0 0);
110
+ --card: oklch(0.15 0 0);
111
+ --card-foreground: oklch(0.97 0 0);
112
+ --popover: oklch(0.15 0 0);
113
+ --popover-foreground: oklch(0.97 0 0);
114
+ --primary: oklch(0.97 0 0);
115
+ --primary-foreground: oklch(0.1 0 0);
116
+ --secondary: oklch(0.22 0 0);
117
+ --secondary-foreground: oklch(0.97 0 0);
118
+ --muted: oklch(0.22 0 0);
119
+ --muted-foreground: oklch(0.62 0 0);
120
+ --accent: oklch(0.22 0 0);
121
+ --accent-foreground: oklch(0.97 0 0);
122
  --destructive: oklch(0.704 0.191 22.216);
123
  --border: oklch(1 0 0 / 10%);
124
  --input: oklch(1 0 0 / 15%);
125
+ --ring: oklch(0.5 0 0);
126
+ --chart-1: oklch(0.8 0 0);
127
+ --chart-2: oklch(0.6 0 0);
128
+ --chart-3: oklch(0.7 0.1 227.392);
129
+ --chart-4: oklch(0.6 0.2 303.9);
130
+ --chart-5: oklch(0.6 0.2 16.439);
131
+ --sidebar: oklch(0.15 0 0);
132
+ --sidebar-foreground: oklch(0.97 0 0);
133
+ --sidebar-primary: oklch(0.97 0 0);
134
+ --sidebar-primary-foreground: oklch(0.1 0 0);
135
+ --sidebar-accent: oklch(0.22 0 0);
136
+ --sidebar-accent-foreground: oklch(0.97 0 0);
137
  --sidebar-border: oklch(1 0 0 / 10%);
138
+ --sidebar-ring: oklch(0.5 0 0);
139
  }
140
 
141
  @layer base {
demo/src/app/layout.tsx CHANGED
@@ -10,8 +10,8 @@ const lato = Lato({
10
  });
11
 
12
  export const metadata: Metadata = {
13
- title: "Voxtral Studio | Emotional Speech Recognition",
14
- description: "Advanced emotional speech recognition and transcription studio.",
15
  };
16
 
17
  export default function RootLayout({
@@ -20,9 +20,9 @@ export default function RootLayout({
20
  children: React.ReactNode;
21
  }>) {
22
  return (
23
- <html lang="en" className="dark">
24
  <body
25
- className={`${lato.variable} antialiased selection:bg-blue-500/30 font-sans`}
26
  >
27
  <TooltipProvider>
28
  {children}
 
10
  });
11
 
12
  export const metadata: Metadata = {
13
+ title: "Ethos Studio | Emotional Speech Recognition",
14
+ description: "Advanced emotional speech recognition and transcription studio powered by Ethostral.",
15
  };
16
 
17
  export default function RootLayout({
 
20
  children: React.ReactNode;
21
  }>) {
22
  return (
23
+ <html lang="en">
24
  <body
25
+ className={`${lato.variable} antialiased selection:bg-black/10 font-sans`}
26
  >
27
  <TooltipProvider>
28
  {children}
demo/src/app/page.tsx CHANGED
@@ -1,80 +1,212 @@
1
  "use client"
2
 
3
- import React from "react"
4
  import Link from "next/link"
5
- import { motion } from "framer-motion"
6
- import { Microphone, ArrowRight, ChartLine, Waveform } from "@phosphor-icons/react"
 
 
 
7
  import { Button } from "@/components/ui/button"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
 
9
- export default function LandingPage() {
 
 
 
 
 
 
 
 
 
10
  return (
11
- <div className="min-h-screen bg-[#050505] text-white flex flex-col items-center justify-center p-6 relative overflow-hidden">
12
- {/* Background Glow */}
13
- <div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[600px] h-[600px] bg-blue-500/10 rounded-full blur-[120px] pointer-events-none" />
14
-
15
- <motion.div
16
- initial={{ opacity: 0, y: 20 }}
17
- animate={{ opacity: 1, y: 0 }}
18
- transition={{ duration: 0.8 }}
19
- className="max-w-2xl w-full text-center space-y-8 z-10"
20
- >
 
 
 
 
 
 
 
 
21
  <div className="space-y-4">
22
- <motion.div
23
- initial={{ scale: 0.9 }}
24
- animate={{ scale: 1 }}
25
- className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-white/5 border border-white/10 text-xs font-sans text-white/60 tracking-widest uppercase"
26
- >
27
- <ChartLine size={14} className="text-blue-500" />
28
- V0.1.0_PROTOTYPE
29
- </motion.div>
30
-
31
- <h1 className="text-6xl md:text-8xl font-black tracking-tighter leading-none">
32
- VOXTRAL<span className="text-blue-500">_</span>
33
- </h1>
34
-
35
- <p className="text-lg text-white/40 font-medium max-w-lg mx-auto leading-relaxed">
36
- High-precision emotional speech recognition.
37
- Diarized, transcribed, and analyzed in real-time.
38
- </p>
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  </div>
40
 
41
- <div className="flex flex-col sm:flex-row items-center justify-center gap-4">
42
- <Link href="/studio">
43
- <Button size="lg" className="h-14 px-8 rounded-maia bg-white text-black hover:bg-white/90 text-sm font-bold gap-3 group">
44
- ENTER STUDIO
45
- <ArrowRight size={18} weight="bold" className="group-hover:translate-x-1 transition-transform" />
46
- </Button>
47
- </Link>
48
- <Button variant="outline" size="lg" className="h-14 px-8 rounded-maia border-white/10 bg-white/5 hover:bg-white/10 text-sm font-bold gap-3">
49
- DOCUMENTATION
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
  </Button>
51
  </div>
52
 
53
- <div className="pt-12 grid grid-cols-3 gap-8">
54
- {[
55
- { label: "LATENCY", value: "<500MS" },
56
- { label: "PRECISION", value: "99.2%" },
57
- { label: "ENGINES", value: "VOX_V3" },
58
- ].map(stat => (
59
- <div key={stat.label} className="flex flex-col items-center">
60
- <span className="text-[10px] font-bold text-white/20 tracking-widest uppercase">{stat.label}</span>
61
- <span className="text-sm font-sans text-white/60">{stat.value}</span>
62
- </div>
63
- ))}
 
64
  </div>
65
- </motion.div>
66
-
67
- {/* Subtle Waveform Animation at bottom */}
68
- <div className="absolute bottom-12 left-0 right-0 h-24 flex items-end justify-center gap-1 opacity-20 pointer-events-none">
69
- {[...Array(60)].map((_, i) => (
70
- <motion.div
71
- key={i}
72
- animate={{ height: [`20%`, `${40 + Math.random() * 60}%`, `20%`] }}
73
- transition={{ duration: 1.5 + Math.random(), repeat: Infinity }}
74
- className="w-[2px] bg-white rounded-full"
75
  />
76
- ))}
77
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
  </div>
79
  )
80
  }
 
1
  "use client"
2
 
3
+ import React, { useState } from "react"
4
  import Link from "next/link"
5
+ import { motion, AnimatePresence } from "framer-motion"
6
+ import {
7
+ Microphone, MagnifyingGlass, DotsThreeVertical,
8
+ UploadSimple, Sparkle, Play, Clock,
9
+ } from "@phosphor-icons/react"
10
  import { Button } from "@/components/ui/button"
11
+ import { Input } from "@/components/ui/input"
12
+ import { Separator } from "@/components/ui/separator"
13
+ import { Switch } from "@/components/ui/switch"
14
+ import { Label } from "@/components/ui/label"
15
+ import {
16
+ Dialog, DialogContent, DialogHeader, DialogTitle,
17
+ } from "@/components/ui/dialog"
18
+ import {
19
+ Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
20
+ } from "@/components/ui/select"
21
+ import {
22
+ Table, TableBody, TableCell, TableHead, TableHeader, TableRow,
23
+ } from "@/components/ui/table"
24
+ import {
25
+ DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger,
26
+ } from "@/components/ui/dropdown-menu"
27
+ import { Navbar } from "@/components/navbar"
28
+ import { Skeleton } from "@/components/ui/skeleton"
29
 
30
+ // --- Mock Data ---
31
+ const MOCK_SESSIONS = [
32
+ { id: "1", title: "Team_Standup_2026-02-28.mp4", createdAt: "2 days ago" },
33
+ { id: "2", title: "Customer_Interview_Batch_7.wav", createdAt: "5 days ago" },
34
+ { id: "3", title: "Podcast_Episode_14.mp3", createdAt: "1 week ago" },
35
+ { id: "4", title: "WeChat_20250804025710.mp4", createdAt: "7 months ago" },
36
+ ]
37
+
38
+ // --- Upload Dialog ---
39
+ function UploadDialog({ open, onOpenChange }: { open: boolean; onOpenChange: (v: boolean) => void }) {
40
  return (
41
+ <Dialog open={open} onOpenChange={onOpenChange}>
42
+ <DialogContent className="sm:max-w-[480px]">
43
+ <DialogHeader>
44
+ <DialogTitle>Transcribe files</DialogTitle>
45
+ </DialogHeader>
46
+
47
+ {/* Drop Zone */}
48
+ <div className="border-2 border-dashed border-border rounded-lg px-6 py-10 flex flex-col items-center gap-2 text-center cursor-pointer hover:border-foreground/30 hover:bg-muted/40 transition-colors">
49
+ <div className="w-10 h-10 rounded-full bg-muted flex items-center justify-center mb-1">
50
+ <UploadSimple size={20} className="text-muted-foreground" />
51
+ </div>
52
+ <p className="text-sm font-semibold text-foreground">Click or drag files here to upload</p>
53
+ <p className="text-xs text-muted-foreground">Audio & video files, up to 1000MB</p>
54
+ </div>
55
+
56
+ <Separator />
57
+
58
+ {/* Settings */}
59
  <div className="space-y-4">
60
+ <div className="flex items-center justify-between">
61
+ <Label htmlFor="lang-select" className="text-sm font-medium">Primary language</Label>
62
+ <Select defaultValue="detect">
63
+ <SelectTrigger id="lang-select" className="w-32 h-8 text-sm">
64
+ <SelectValue placeholder="Detect" />
65
+ </SelectTrigger>
66
+ <SelectContent>
67
+ <SelectItem value="detect">Detect</SelectItem>
68
+ <SelectItem value="en">English</SelectItem>
69
+ <SelectItem value="es">Spanish</SelectItem>
70
+ <SelectItem value="fr">French</SelectItem>
71
+ <SelectItem value="zh">Chinese</SelectItem>
72
+ </SelectContent>
73
+ </Select>
74
+ </div>
75
+
76
+ <div className="flex items-center justify-between">
77
+ <Label htmlFor="diarize" className="text-sm font-medium">Speaker diarization</Label>
78
+ <Switch id="diarize" defaultChecked />
79
+ </div>
80
+
81
+ <div className="flex items-center justify-between">
82
+ <Label htmlFor="emotion" className="text-sm font-medium">Emotion analysis</Label>
83
+ <Switch id="emotion" defaultChecked />
84
+ </div>
85
+
86
+ <div className="flex items-center justify-between">
87
+ <Label htmlFor="subtitles" className="text-sm font-medium">Include subtitles</Label>
88
+ <Switch id="subtitles" />
89
+ </div>
90
  </div>
91
 
92
+ <Button className="w-full gap-2 font-bold">
93
+ <UploadSimple size={16} weight="bold" />
94
+ Upload files
95
+ </Button>
96
+ </DialogContent>
97
+ </Dialog>
98
+ )
99
+ }
100
+
101
+ // --- Main Page ---
102
+ export default function HomePage() {
103
+ const [showModal, setShowModal] = useState(false)
104
+ const [search, setSearch] = useState("")
105
+
106
+ const filtered = MOCK_SESSIONS.filter(s =>
107
+ s.title.toLowerCase().includes(search.toLowerCase())
108
+ )
109
+
110
+ return (
111
+ <div className="min-h-screen bg-background text-foreground">
112
+ <Navbar />
113
+
114
+ <main className="max-w-4xl mx-auto px-6 py-10">
115
+ {/* Page Header */}
116
+ <div className="flex items-start justify-between mb-6">
117
+ <div>
118
+ <h1 className="text-2xl font-black tracking-tight">Speech to text</h1>
119
+ <p className="text-sm text-muted-foreground mt-1">
120
+ Transcribe audio and video files with our{" "}
121
+ <span className="underline underline-offset-2 text-foreground font-medium cursor-pointer">industry-leading ASR model.</span>
122
+ </p>
123
+ </div>
124
+ <Button onClick={() => setShowModal(true)} className="gap-2 font-bold shadow-sm">
125
+ <Microphone size={16} weight="bold" />
126
+ Transcribe files
127
  </Button>
128
  </div>
129
 
130
+ {/* Promo Banner */}
131
+ <div className="border border-border rounded-lg p-4 flex items-center gap-4 mb-5 bg-card hover:bg-muted/40 transition-colors cursor-pointer">
132
+ <Skeleton className="w-14 h-14 rounded-lg bg-foreground/10 flex-shrink-0" />
133
+ <div className="flex-1 min-w-0">
134
+ <p className="text-sm font-bold">Try Ethostral Realtime</p>
135
+ <p className="text-sm text-muted-foreground mt-0.5 leading-snug">
136
+ Experience lightning-fast transcription with unmatched emotional accuracy, powered by Ethostral.
137
+ </p>
138
+ </div>
139
+ <Button variant="outline" size="sm" className="flex-shrink-0 font-bold">
140
+ Try the demo
141
+ </Button>
142
  </div>
143
+
144
+ {/* Search */}
145
+ <div className="relative mb-5">
146
+ <MagnifyingGlass size={15} className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground" />
147
+ <Input
148
+ placeholder="Search transcripts..."
149
+ value={search}
150
+ onChange={e => setSearch(e.target.value)}
151
+ className="pl-9 h-9 text-sm bg-card"
 
152
  />
153
+ </div>
154
+
155
+ {/* Table */}
156
+ <Table>
157
+ <TableHeader>
158
+ <TableRow>
159
+ <TableHead className="text-[11px] font-black uppercase tracking-widest text-muted-foreground">Title</TableHead>
160
+ <TableHead className="text-[11px] font-black uppercase tracking-widest text-muted-foreground">Created at</TableHead>
161
+ <TableHead className="w-10" />
162
+ </TableRow>
163
+ </TableHeader>
164
+ <TableBody>
165
+ {filtered.length === 0 ? (
166
+ <TableRow>
167
+ <TableCell colSpan={3} className="text-center text-muted-foreground py-16 text-sm">
168
+ No transcripts found.
169
+ </TableCell>
170
+ </TableRow>
171
+ ) : (
172
+ filtered.map((session, i) => (
173
+ <TableRow key={session.id} className="cursor-pointer group">
174
+ <TableCell>
175
+ <Link href="/studio" className="flex items-center gap-3">
176
+ <div className="w-8 h-8 rounded-md bg-muted border border-border flex items-center justify-center flex-shrink-0">
177
+ <Play size={12} weight="fill" className="text-muted-foreground" />
178
+ </div>
179
+ <span className="text-sm font-semibold truncate max-w-xs">{session.title}</span>
180
+ </Link>
181
+ </TableCell>
182
+ <TableCell>
183
+ <span className="text-sm text-muted-foreground flex items-center gap-1.5">
184
+ <Clock size={13} />
185
+ {session.createdAt}
186
+ </span>
187
+ </TableCell>
188
+ <TableCell>
189
+ <DropdownMenu>
190
+ <DropdownMenuTrigger asChild>
191
+ <Button variant="ghost" size="icon" className="h-7 w-7 opacity-0 group-hover:opacity-100 transition-opacity">
192
+ <DotsThreeVertical size={16} />
193
+ </Button>
194
+ </DropdownMenuTrigger>
195
+ <DropdownMenuContent align="end">
196
+ <DropdownMenuItem>Open</DropdownMenuItem>
197
+ <DropdownMenuItem>Export</DropdownMenuItem>
198
+ <DropdownMenuItem className="text-destructive">Delete</DropdownMenuItem>
199
+ </DropdownMenuContent>
200
+ </DropdownMenu>
201
+ </TableCell>
202
+ </TableRow>
203
+ ))
204
+ )}
205
+ </TableBody>
206
+ </Table>
207
+ </main>
208
+
209
+ <UploadDialog open={showModal} onOpenChange={setShowModal} />
210
  </div>
211
  )
212
  }
demo/src/app/studio/page.tsx CHANGED
@@ -1,350 +1,349 @@
1
  "use client"
2
 
3
- import React, { useState, useEffect } from "react"
 
4
  import { motion, AnimatePresence } from "framer-motion"
5
  import {
6
- Waves,
7
- Microphone,
8
- Export,
9
- Play,
10
- Pause,
11
  SpeakerHigh,
12
- Waveform,
13
- ChartLine,
14
- ChartPieSlice,
15
- ArrowsInLineVertical,
16
- Sliders
17
  } from "@phosphor-icons/react"
18
  import { Button } from "@/components/ui/button"
19
- import { Card } from "@/components/ui/card"
20
- import { ScrollArea } from "@/components/ui/scroll-area"
21
- import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
22
  import { Badge } from "@/components/ui/badge"
 
 
 
23
  import { Slider } from "@/components/ui/slider"
24
- import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
 
 
 
 
 
 
 
25
 
26
  // --- Mock Data ---
 
 
 
 
27
 
28
- const MOCK_TRANSCRIPT = [
29
- { id: 1, speaker: "Speaker 0", time: "00:10", text: "I can't believe we're actually doing this.", emotion: "Surprise", valence: 0.6, arousal: 0.8 },
30
- { id: 2, speaker: "Speaker 1", time: "00:15", text: "It's been a long time coming. Are you nervous?", emotion: "Neutral", valence: 0.1, arousal: 0.2 },
31
- { id: 3, speaker: "Speaker 0", time: "00:22", text: "A little bit. The data is just... it's a lot to process.", emotion: "Anxiety", valence: -0.4, arousal: 0.7 },
32
- { id: 4, speaker: "Speaker 1", time: "00:30", text: "Don't worry, the models are calibrated for this level of noise.", emotion: "Calm", valence: 0.3, arousal: -0.2 },
 
 
 
33
  ]
34
 
35
- // --- Components ---
 
 
 
36
 
37
- const Header = () => (
38
- <header className="h-14 border-b border-white/10 bg-black/40 backdrop-blur-md flex items-center justify-between px-6 sticky top-0 z-50">
39
- <div className="flex items-center gap-4">
40
- <div className="flex items-center gap-2">
41
- <div className="w-3 h-3 rounded-full bg-emerald-500 animate-pulse" />
42
- <span className="text-sm font-medium text-white/80">LIVE_SESSION_01.wav</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  </div>
44
- <div className="h-4 w-[1px] bg-white/10" />
45
- <span className="text-xs font-sans text-white/40">DURATION: 00:32:45</span>
46
- </div>
47
- <div className="flex items-center gap-3">
48
- <Button variant="outline" size="sm" className="bg-white/5 border-white/10 hover:bg-white/10 text-white/80 gap-2">
49
- <Export size={16} weight="bold" />
50
- EXPORT
51
- </Button>
52
- <div className="w-8 h-8 rounded-full bg-white/10 flex items-center justify-center">
53
- <div className="w-4 h-4 rounded-full bg-blue-500" />
54
  </div>
55
- </div>
56
- </header>
57
- )
58
 
59
- const TranscriptPanel = ({ activeSegment, onSelect }: any) => (
60
- <div className="flex flex-col h-full bg-black/20 border-r border-white/5">
61
- <div className="p-4 border-b border-white/5 flex items-center gap-2">
62
- <SpeakerHigh size={18} className="text-white/60" />
63
- <h2 className="text-xs font-bold tracking-widest text-white/60 uppercase">DIARIZED_TRANSCRIPT</h2>
64
- </div>
65
- <ScrollArea className="flex-1 p-4">
66
- <div className="space-y-6">
67
- {MOCK_TRANSCRIPT.map((item) => (
68
- <motion.div
69
- key={item.id}
70
- initial={{ opacity: 0, x: -10 }}
71
- animate={{ opacity: 1, x: 0 }}
72
- className={`group cursor-pointer p-3 rounded-xl transition-all ${activeSegment === item.id ? 'bg-white/10 ring-1 ring-white/20' : 'hover:bg-white/5'}`}
73
- onClick={() => onSelect(item)}
74
- >
75
- <div className="flex items-start gap-3">
76
- <Avatar className="w-8 h-8 rounded-lg">
77
- <AvatarFallback className="bg-white/5 text-[10px] text-white/40">{item.speaker[item.speaker.length - 1]}</AvatarFallback>
78
- </Avatar>
79
- <div className="flex-1 space-y-1">
80
- <div className="flex items-center justify-between">
81
- <span className="text-[10px] font-bold text-white/40 uppercase tracking-tighter">{item.speaker}</span>
82
- <span className="text-[10px] font-sans text-white/20">{item.time}</span>
83
- </div>
84
- <p className="text-sm text-white/80 leading-relaxed font-sans">{item.text}</p>
85
- <div className="pt-2 flex flex-wrap gap-2">
86
- <TooltipProvider>
87
- <Tooltip>
88
- <TooltipTrigger>
89
- <Badge variant="outline" className="bg-blue-500/10 border-blue-500/30 text-blue-400 text-[9px] px-1.5 py-0 uppercase font-sans">
90
- {item.emotion}
91
- </Badge>
92
- </TooltipTrigger>
93
- <TooltipContent className="bg-black/90 border-white/10 text-xs">
94
- Dominant Emotion Tag
95
- </TooltipContent>
96
- </Tooltip>
97
- </TooltipProvider>
98
- </div>
99
- </div>
100
- </div>
101
- </motion.div>
102
- ))}
103
  </div>
104
- </ScrollArea>
105
- </div>
106
- )
107
 
108
- const QuadViewAnalytics = ({ activeSegment }: any) => {
 
109
  return (
110
- <div className="grid grid-cols-2 grid-rows-2 h-full gap-[1px] bg-white/5">
111
- {/* 1. Russell's Circumplex */}
112
- <Card className="rounded-none border-none bg-black/40 p-4 flex flex-col gap-4 relative overflow-hidden group">
113
- <div className="flex items-center justify-between">
114
- <div className="flex items-center gap-2">
115
- <ChartLine size={18} className="text-blue-400" />
116
- <h3 className="text-[10px] font-bold tracking-widest text-white/40 uppercase">RUSSELL_CIRCUMPLEX</h3>
117
- </div>
118
- <span className="text-[10px] font-sans text-white/20">V: {activeSegment?.valence || 0} / A: {activeSegment?.arousal || 0}</span>
119
- </div>
120
- <div className="flex-1 bg-white/5 rounded-lg relative flex items-center justify-center border border-white/5">
121
- <div className="absolute inset-0 flex items-center justify-center opacity-10">
122
- <div className="w-full h-[1px] bg-white" />
123
- <div className="h-full w-[1px] bg-white absolute" />
124
- </div>
125
- <div className="absolute top-2 left-1/2 -translate-x-1/2 text-[8px] text-white/20">AROUSAL +</div>
126
- <div className="absolute bottom-2 left-1/2 -translate-x-1/2 text-[8px] text-white/20">AROUSAL -</div>
127
- <div className="absolute left-2 top-1/2 -translate-y-1/2 text-[8px] text-white/20 rotate-90">VALENCE -</div>
128
- <div className="absolute right-2 top-1/2 -translate-y-1/2 text-[8px] text-white/20 -rotate-90">VALENCE +</div>
129
-
130
- <motion.div
131
- animate={{
132
- x: (activeSegment?.valence || 0) * 100,
133
- y: (activeSegment?.arousal || 0) * -100
134
- }}
135
- className="w-4 h-4 bg-blue-500 rounded-full shadow-[0_0_20px_rgba(59,130,246,0.5)] border-2 border-white relative z-10"
136
- >
137
- <div className="absolute inset-0 animate-ping bg-blue-500 rounded-full opacity-50" />
138
- </motion.div>
139
  </div>
140
- </Card>
141
-
142
- {/* 2. Plutchik's Wheel / Radar */}
143
- <Card className="rounded-none border-none bg-black/40 p-4 flex flex-col gap-4 group">
144
- <div className="flex items-center justify-between">
145
- <div className="flex items-center gap-2">
146
- <ChartPieSlice size={18} className="text-purple-400" />
147
- <h3 className="text-[10px] font-bold tracking-widest text-white/40 uppercase">PLUTCHIK_WHEEL</h3>
148
  </div>
 
149
  </div>
150
- <div className="flex-1 flex items-center justify-center relative">
151
- <div className="w-32 h-32 rounded-full border border-white/10 flex items-center justify-center">
152
- <div className="w-20 h-20 rounded-full border border-white/5 flex items-center justify-center" />
153
- <div className="absolute inset-0 flex items-center justify-center">
154
- {[0, 45, 90, 135, 180, 225, 270, 315].map(deg => (
155
- <div key={deg} className="absolute w-[1px] h-full bg-white/5" style={{ transform: `rotate(${deg}deg)` }} />
156
- ))}
157
- </div>
158
- <motion.div
159
- animate={{ scale: [1, 1.1, 1], rotate: [0, 5, -5, 0] }}
160
- transition={{ duration: 4, repeat: Infinity }}
161
- className="w-24 h-24 bg-purple-500/20 rounded-full border border-purple-500/40 backdrop-blur-sm"
162
- style={{ clipPath: 'polygon(50% 0%, 90% 20%, 100% 60%, 75% 100%, 25% 100%, 0% 60%, 10% 20%)' }}
163
- />
164
- </div>
165
- </div>
166
- </Card>
167
 
168
- {/* 3. Prosodic Meters */}
169
- <Card className="rounded-none border-none bg-black/40 p-4 flex flex-col gap-4 group">
170
- <div className="flex items-center justify-between">
171
- <div className="flex items-center gap-2">
172
- <ArrowsInLineVertical size={18} className="text-emerald-400" />
173
- <h3 className="text-[10px] font-bold tracking-widest text-white/40 uppercase">PROSODIC_ANALYSIS</h3>
174
- </div>
175
- </div>
176
- <div className="flex-1 flex items-end justify-between px-4 pb-2 gap-4">
177
- {[
178
- { label: "PITCH", value: 160, max: 400, unit: "Hz", color: "bg-emerald-500" },
179
- { label: "JITTER", value: 0.12, max: 1, unit: "%", color: "bg-blue-500" },
180
- { label: "RATE", value: 140, max: 300, unit: "wpm", color: "bg-amber-500" },
181
- { label: "SHIM", value: 0.4, max: 1, unit: "dB", color: "bg-purple-500" },
182
- ].map(meter => (
183
- <div key={meter.label} className="flex-1 flex flex-col items-center gap-2 h-full">
184
- <div className="flex-1 w-full bg-white/5 rounded-sm overflow-hidden relative flex flex-col justify-end">
185
- <div className="absolute inset-0 opacity-10 flex flex-col justify-between p-1">
186
- {[...Array(10)].map((_, i) => <div key={i} className="h-[1px] w-full bg-white" />)}
 
 
 
 
 
 
 
 
 
 
 
187
  </div>
188
- <motion.div
189
- initial={{ height: 0 }}
190
- animate={{ height: `${(meter.value / meter.max) * 100}%` }}
191
- className={`w-full ${meter.color} shadow-[0_0_15px_rgba(255,255,255,0.1)]`}
192
- />
193
  </div>
194
- <span className="text-[8px] font-bold text-white/20 uppercase tracking-tighter">{meter.label}</span>
195
- <span className="text-[9px] font-sans text-white/40">{meter.value}{meter.unit}</span>
196
- </div>
197
- ))}
198
- </div>
199
- </Card>
200
 
201
- {/* 4. PAD Space */}
202
- <Card className="rounded-none border-none bg-black/40 p-4 flex flex-col gap-4 group">
203
- <div className="flex items-center justify-between">
204
- <div className="flex items-center gap-2">
205
- <Sliders size={18} className="text-amber-400" />
206
- <h3 className="text-[10px] font-bold tracking-widest text-white/40 uppercase">PAD_DIMENSIONS</h3>
207
- </div>
208
- </div>
209
- <div className="flex-1 space-y-4 px-2">
210
- {[
211
- { label: "Pleasure", value: 45, color: "bg-emerald-500" },
212
- { label: "Arousal", value: 78, color: "bg-blue-500" },
213
- { label: "Dominance", value: 62, color: "bg-purple-500" },
214
- ].map(slider => (
215
- <div key={slider.label} className="space-y-1">
216
- <div className="flex justify-between text-[8px] uppercase tracking-widest text-white/40">
217
- <span>{slider.label}</span>
218
- <span className="font-sans">{slider.value}%</span>
219
- </div>
220
- <div className="h-1 w-full bg-white/5 rounded-full overflow-hidden">
221
- <motion.div
222
- initial={{ width: 0 }}
223
- animate={{ width: `${slider.value}%` }}
224
- className={`h-full ${slider.color}`}
225
- />
 
 
 
 
 
 
 
 
226
  </div>
227
- </div>
228
- ))}
229
  </div>
230
- </Card>
231
  </div>
232
  )
233
  }
234
 
235
- const EmotionalTimeline = () => (
236
- <footer className="h-32 border-t border-white/10 bg-black/60 backdrop-blur-xl flex flex-col">
237
- <div className="h-2 w-full bg-white/5 relative">
238
- <div className="absolute left-1/4 w-[1px] h-full bg-blue-500 shadow-[0_0_10px_rgba(59,130,246,1)] z-10" />
239
- <div className="absolute left-1/4 right-0 h-full bg-blue-500/10" />
240
- </div>
241
- <div className="flex-1 flex px-6 py-4 gap-6 items-center">
242
- <div className="flex items-center gap-4">
243
- <Button size="icon" variant="ghost" className="rounded-full hover:bg-white/10 text-white">
244
- <Waves size={24} weight="bold" />
245
- </Button>
246
- <div className="flex items-center gap-1">
247
- <Button size="icon" variant="ghost" className="h-8 w-8 text-white/60">
248
- <Play size={18} weight="fill" />
 
 
249
  </Button>
250
- <span className="text-[10px] font-sans text-white/40">00:32:04 / 01:20:00</span>
 
 
 
 
 
251
  </div>
252
- </div>
253
- <div className="flex-1 h-12 relative flex items-center justify-between gap-[2px]">
254
- {[...Array(120)].map((_, i) => (
255
- <div
256
- key={i}
257
- className={`w-[2px] rounded-full transition-all duration-300 ${i % 10 === 0 ? 'h-8 bg-white/20' : 'h-4 bg-white/5'}`}
258
- style={{ height: `${20 + Math.random() * 60}%`, opacity: i > 30 ? 0.3 : 1 }}
259
- />
260
- ))}
261
- <div className="absolute inset-x-0 h-[1px] bg-white/5" />
262
- </div>
263
- <div className="flex items-center gap-4">
264
- <div className="flex flex-col items-end">
265
- <span className="text-[8px] font-bold text-white/20 uppercase tracking-widest">SCANNING_RESOLUTION</span>
266
- <span className="text-[10px] font-sans text-white/60">120ms/sample</span>
 
267
  </div>
268
- <Slider defaultValue={[25]} max={100} step={1} className="w-24" />
269
  </div>
270
- </div>
271
- </footer>
272
- )
273
 
274
- const RecorderOverlay = () => {
275
- const [isRecording, setIsRecording] = useState(false)
 
 
 
 
 
 
276
 
277
- return (
278
- <div className="absolute right-8 bottom-40 z-50">
279
- <motion.button
280
- whileHover={{ scale: 1.05 }}
281
- whileTap={{ scale: 0.95 }}
282
- onClick={() => setIsRecording(!isRecording)}
283
- className={`w-16 h-16 rounded-full flex items-center justify-center relative transition-all duration-500 ${isRecording ? 'bg-red-500 shadow-[0_0_50px_rgba(239,68,68,0.5)]' : 'bg-white/10 hover:bg-white/20 backdrop-blur-md'}`}
284
- >
285
- <AnimatePresence mode="wait">
286
- {isRecording ? (
287
- <motion.div
288
- key="stop"
289
- initial={{ opacity: 0, scale: 0 }}
290
- animate={{ opacity: 1, scale: 1 }}
291
- exit={{ opacity: 0, scale: 0 }}
292
- >
293
- <div className="w-6 h-6 bg-white rounded-sm" />
294
- </motion.div>
295
- ) : (
296
- <motion.div
297
- key="mic"
298
- initial={{ opacity: 0, scale: 0 }}
299
- animate={{ opacity: 1, scale: 1 }}
300
- exit={{ opacity: 0, scale: 0 }}
301
- >
302
- <Microphone size={32} weight="bold" className="text-white" />
303
- </motion.div>
304
- )}
305
- </AnimatePresence>
306
 
307
- {isRecording && (
308
- <motion.div
309
- initial={{ scale: 1 }}
310
- animate={{ scale: [1, 1.5, 1], opacity: [0.5, 0, 0.5] }}
311
- transition={{ duration: 2, repeat: Infinity }}
312
- className="absolute inset-0 rounded-full border-2 border-red-500"
313
- />
314
- )}
315
- </motion.button>
316
  </div>
317
  )
318
  }
319
 
 
320
  export default function StudioPage() {
321
- const [activeSegment, setActiveSegment] = useState(MOCK_TRANSCRIPT[0])
 
 
 
322
 
323
  return (
324
- <main className="flex flex-col h-screen bg-[#050505] text-white selection:bg-blue-500/30 overflow-hidden font-sans">
325
- <Header />
 
 
 
 
 
 
 
326
 
327
- <div className="flex-1 flex overflow-hidden">
328
- {/* Left Panel: Transcript */}
329
- <div className="w-[400px]">
330
- <TranscriptPanel
331
- activeSegment={activeSegment.id}
332
- onSelect={setActiveSegment}
333
- />
 
 
 
 
 
 
 
 
 
 
 
334
  </div>
335
 
336
- {/* Right Panel: Analytics */}
337
- <div className="flex-1 flex flex-col relative">
338
- <QuadViewAnalytics activeSegment={activeSegment} />
339
 
340
- <div className="absolute inset-0 pointer-events-none border-[1px] border-white/5 z-20" />
 
 
 
 
341
 
342
- {/* Recorder Hub */}
343
- <RecorderOverlay />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
344
  </div>
345
  </div>
346
 
347
- <EmotionalTimeline />
348
- </main>
 
349
  )
350
  }
 
1
  "use client"
2
 
3
+ import React, { useState } from "react"
4
+ import Link from "next/link"
5
  import { motion, AnimatePresence } from "framer-motion"
6
  import {
7
+ ArrowLeft, ArrowCounterClockwise, ArrowClockwise,
8
+ Export, Play, Pause, Plus, DotsThreeVertical,
9
+ MagnifyingGlass, MinusCircle, PlusCircle, Waveform,
 
 
10
  SpeakerHigh,
 
 
 
 
 
11
  } from "@phosphor-icons/react"
12
  import { Button } from "@/components/ui/button"
 
 
 
13
  import { Badge } from "@/components/ui/badge"
14
+ import { ScrollArea } from "@/components/ui/scroll-area"
15
+ import { Separator } from "@/components/ui/separator"
16
+ import { Avatar, AvatarFallback } from "@/components/ui/avatar"
17
  import { Slider } from "@/components/ui/slider"
18
+ import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"
19
+ import {
20
+ DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger,
21
+ } from "@/components/ui/dropdown-menu"
22
+ import {
23
+ Collapsible, CollapsibleContent, CollapsibleTrigger,
24
+ } from "@/components/ui/collapsible"
25
+ import { Progress } from "@/components/ui/progress"
26
 
27
  // --- Mock Data ---
28
+ const SPEAKERS = [
29
+ { id: "s0", label: "Speaker 0", color: "bg-blue-400" },
30
+ { id: "s1", label: "Speaker 1", color: "bg-pink-400" },
31
+ ]
32
 
33
+ const SEGMENTS = [
34
+ { id: 1, speaker: "s0", startTime: "00.10", endTime: "07.28", text: "[instrumental music plays]", emotion: "Neutral", valence: 0.0, arousal: 0.1 },
35
+ { id: 2, speaker: "s0", startTime: "08.10", endTime: "09.04", text: "Hello, I'm here.", emotion: "Calm", valence: 0.3, arousal: -0.1 },
36
+ { id: 3, speaker: "s1", startTime: "10.62", endTime: "11.00", text: "Oh.", emotion: "Surprise", valence: 0.4, arousal: 0.6 },
37
+ { id: 4, speaker: "s1", startTime: "13.02", endTime: "14.18", text: "Hi.", emotion: "Neutral", valence: 0.1, arousal: 0.0 },
38
+ { id: 5, speaker: "s0", startTime: "14.82", endTime: "16.40", text: "Hi.", emotion: "Happy", valence: 0.7, arousal: 0.4 },
39
+ { id: 6, speaker: "s1", startTime: "17.20", endTime: "22.10", text: "It's been a long time coming. Are you nervous?", emotion: "Curious", valence: 0.2, arousal: 0.5 },
40
+ { id: 7, speaker: "s0", startTime: "22.90", endTime: "28.50", text: "A little bit. The data is just... it's a lot to process.", emotion: "Anxiety", valence: -0.4, arousal: 0.7 },
41
  ]
42
 
43
+ const SPEAKER_MAP: Record<string, { label: string; color: string }> = {
44
+ s0: { label: "Speaker 0", color: "bg-blue-400" },
45
+ s1: { label: "Speaker 1", color: "bg-pink-400" },
46
+ }
47
 
48
+ // Transcript segment row matches the reference's Speaker / Timestamp / Text layout
49
+ function SegmentRow({
50
+ seg,
51
+ active,
52
+ onClick,
53
+ }: {
54
+ seg: typeof SEGMENTS[number]
55
+ active: boolean
56
+ onClick: () => void
57
+ }) {
58
+ const speaker = SPEAKER_MAP[seg.speaker]
59
+ return (
60
+ <div
61
+ onClick={onClick}
62
+ className={`flex gap-0 group transition-colors cursor-pointer border-b border-border last:border-0 ${active ? "bg-accent" : "hover:bg-muted/40"}`}
63
+ >
64
+ {/* Speaker col */}
65
+ <div className="w-36 flex-shrink-0 flex items-start gap-2 px-4 py-4">
66
+ <Avatar className="w-7 h-7 flex-shrink-0 mt-0.5">
67
+ <AvatarFallback className={`${speaker.color} text-white text-[10px] font-bold`}>
68
+ {speaker.label[0]}
69
+ </AvatarFallback>
70
+ </Avatar>
71
+ <span className="text-xs font-bold text-foreground leading-tight mt-1">{speaker.label}</span>
72
  </div>
73
+
74
+ {/* Separator */}
75
+ <Separator orientation="vertical" className="h-auto" />
76
+
77
+ {/* Time + text col */}
78
+ <div className="flex-1 px-5 py-4 space-y-1 min-w-0">
79
+ <p className="text-[11px] text-muted-foreground font-medium">{seg.startTime}</p>
80
+ <p className="text-sm text-foreground leading-relaxed">{seg.text}</p>
81
+ <p className="text-[11px] text-muted-foreground font-medium">{seg.endTime}</p>
 
82
  </div>
 
 
 
83
 
84
+ {/* Emotion badge right */}
85
+ <div className="flex-shrink-0 flex items-start pt-4 pr-4">
86
+ <Badge
87
+ variant="outline"
88
+ className="text-[9px] font-bold uppercase tracking-wider px-1.5 py-0 border-border text-muted-foreground"
89
+ >
90
+ {seg.emotion}
91
+ </Badge>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
92
  </div>
93
+ </div>
94
+ )
95
+ }
96
 
97
+ // Right panel: video preview + properties
98
+ function RightPanel({ activeSegment }: { activeSegment: typeof SEGMENTS[number] | null }) {
99
  return (
100
+ <div className="flex flex-col h-full border-l border-border bg-card">
101
+ {/* Fake video player */}
102
+ <div className="aspect-video w-full bg-slate-950 flex items-center justify-center flex-shrink-0 relative">
103
+ <div className="absolute inset-0 flex flex-col items-center justify-center gap-2 text-white/20">
104
+ <Waveform size={36} />
105
+ <span className="text-[10px] font-bold uppercase tracking-widest">VIDEO PREVIEW</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
  </div>
107
+ {/* Fake video controls bar */}
108
+ <div className="absolute bottom-0 left-0 right-0 h-6 bg-black/60 flex items-center px-2 gap-1">
109
+ <Play size={10} weight="fill" className="text-white/60" />
110
+ <div className="flex-1 h-[2px] bg-white/20 rounded-full mx-1">
111
+ <div className="w-1/4 h-full bg-white/60 rounded-full" />
 
 
 
112
  </div>
113
+ <span className="text-white/40 text-[9px]">0:14.82</span>
114
  </div>
115
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
116
 
117
+ <ScrollArea className="flex-1">
118
+ <div className="p-4 space-y-4">
119
+ {/* Global Properties */}
120
+ <Collapsible defaultOpen>
121
+ <CollapsibleTrigger className="flex items-center gap-2 text-xs font-bold text-muted-foreground uppercase tracking-widest hover:text-foreground transition-colors w-full text-left">
122
+ <span></span> Global Properties
123
+ </CollapsibleTrigger>
124
+ <CollapsibleContent>
125
+ <div className="mt-3 space-y-3 text-sm">
126
+ <div className="flex items-center justify-between">
127
+ <span className="font-semibold text-foreground truncate max-w-[160px] text-xs">WeChat_20250804025710.mp4</span>
128
+ </div>
129
+ <Separator />
130
+ <div className="flex items-center justify-between">
131
+ <span className="text-muted-foreground text-xs">Language</span>
132
+ <span className="font-semibold text-xs">English</span>
133
+ </div>
134
+ <div className="flex items-center justify-between">
135
+ <span className="text-muted-foreground text-xs">Subtitles</span>
136
+ <DropdownMenu>
137
+ <DropdownMenuTrigger asChild>
138
+ <Button variant="ghost" size="icon" className="h-6 w-6">
139
+ <DotsThreeVertical size={14} />
140
+ </Button>
141
+ </DropdownMenuTrigger>
142
+ <DropdownMenuContent>
143
+ <DropdownMenuItem>Export SRT</DropdownMenuItem>
144
+ <DropdownMenuItem>Export VTT</DropdownMenuItem>
145
+ </DropdownMenuContent>
146
+ </DropdownMenu>
147
  </div>
 
 
 
 
 
148
  </div>
149
+ </CollapsibleContent>
150
+ </Collapsible>
 
 
 
 
151
 
152
+ <Separator />
153
+
154
+ {/* Emotional Analysis (active segment) */}
155
+ <Collapsible defaultOpen>
156
+ <CollapsibleTrigger className="flex items-center gap-2 text-xs font-bold text-muted-foreground uppercase tracking-widest hover:text-foreground transition-colors w-full text-left">
157
+ <span></span> Emotional Analysis
158
+ </CollapsibleTrigger>
159
+ <CollapsibleContent>
160
+ <div className="mt-3 space-y-3">
161
+ {activeSegment ? (
162
+ <>
163
+ <div className="flex items-center justify-between text-xs">
164
+ <span className="text-muted-foreground">Emotion</span>
165
+ <Badge variant="outline" className="font-bold text-[10px]">{activeSegment.emotion}</Badge>
166
+ </div>
167
+ <div className="space-y-1">
168
+ <div className="flex items-center justify-between text-[11px] text-muted-foreground">
169
+ <span>Valence</span>
170
+ <span className="font-bold text-foreground">{((activeSegment.valence + 1) / 2 * 100).toFixed(0)}%</span>
171
+ </div>
172
+ <Progress value={(activeSegment.valence + 1) / 2 * 100} className="h-1.5" />
173
+ </div>
174
+ <div className="space-y-1">
175
+ <div className="flex items-center justify-between text-[11px] text-muted-foreground">
176
+ <span>Arousal</span>
177
+ <span className="font-bold text-foreground">{((activeSegment.arousal + 1) / 2 * 100).toFixed(0)}%</span>
178
+ </div>
179
+ <Progress value={(activeSegment.arousal + 1) / 2 * 100} className="h-1.5" />
180
+ </div>
181
+ </>
182
+ ) : (
183
+ <p className="text-xs text-muted-foreground">Select a segment to view analysis.</p>
184
+ )}
185
  </div>
186
+ </CollapsibleContent>
187
+ </Collapsible>
188
  </div>
189
+ </ScrollArea>
190
  </div>
191
  )
192
  }
193
 
194
+ // Bottom timeline bar
195
+ function TimelineBar({ isPlaying, onToggle }: { isPlaying: boolean; onToggle: () => void }) {
196
+ return (
197
+ <div className="border-t border-border bg-card flex-shrink-0">
198
+ {/* Speaker track rows */}
199
+ <div className="border-b border-border">
200
+ <div className="flex items-center px-3 py-1.5 gap-2 group">
201
+ <Button variant="ghost" size="icon" className="h-5 w-5 text-muted-foreground cursor-grab">
202
+ <DotsThreeVertical size={12} />
203
+ </Button>
204
+ <Avatar className="w-5 h-5">
205
+ <AvatarFallback className="bg-blue-400 text-white text-[8px] font-bold">S</AvatarFallback>
206
+ </Avatar>
207
+ <span className="text-xs font-semibold text-foreground w-20">Speaker 0</span>
208
+ <Button variant="ghost" size="icon" className="h-5 w-5 text-muted-foreground ml-auto opacity-0 group-hover:opacity-100">
209
+ <DotsThreeVertical size={12} />
210
  </Button>
211
+ {/* Track visualization */}
212
+ <div className="flex-1 h-5 flex items-center gap-[2px]">
213
+ {[60, 15, 12, 8, 10, 14].map((w, i) => (
214
+ <div key={i} className="h-4 bg-blue-200 rounded-sm" style={{ width: `${w}%` }} />
215
+ ))}
216
+ </div>
217
  </div>
218
+ <div className="flex items-center px-3 py-1.5 gap-2 group">
219
+ <Button variant="ghost" size="icon" className="h-5 w-5 text-muted-foreground cursor-grab">
220
+ <DotsThreeVertical size={12} />
221
+ </Button>
222
+ <Avatar className="w-5 h-5">
223
+ <AvatarFallback className="bg-pink-400 text-white text-[8px] font-bold">S</AvatarFallback>
224
+ </Avatar>
225
+ <span className="text-xs font-semibold text-foreground w-20">Speaker 1</span>
226
+ <Button variant="ghost" size="icon" className="h-5 w-5 text-muted-foreground ml-auto opacity-0 group-hover:opacity-100">
227
+ <DotsThreeVertical size={12} />
228
+ </Button>
229
+ <div className="flex-1 h-5 flex items-center gap-[2px]">
230
+ {[0, 0, 5, 6, 0, 8, 5, 6].map((w, i) => (
231
+ w > 0 ? <div key={i} className="h-4 bg-pink-200 rounded-sm" style={{ width: `${w}%` }} /> : <div key={i} style={{ width: "8%" }} />
232
+ ))}
233
+ </div>
234
  </div>
 
235
  </div>
 
 
 
236
 
237
+ {/* Playback controls */}
238
+ <div className="flex items-center gap-4 px-4 h-10">
239
+ {/* Zoom */}
240
+ <div className="flex items-center gap-1.5 text-muted-foreground">
241
+ <MinusCircle size={14} />
242
+ <Slider defaultValue={[40]} max={100} className="w-16" />
243
+ <PlusCircle size={14} />
244
+ </div>
245
 
246
+ <Separator orientation="vertical" className="h-5" />
247
+
248
+ {/* Play / speed */}
249
+ <div className="flex items-center gap-3">
250
+ <Button
251
+ size="icon"
252
+ className="h-7 w-7 rounded-full"
253
+ onClick={onToggle}
254
+ >
255
+ {isPlaying
256
+ ? <Pause size={13} weight="fill" />
257
+ : <Play size={13} weight="fill" />
258
+ }
259
+ </Button>
260
+ <span className="text-xs font-bold text-muted-foreground">1.0×</span>
261
+ </div>
262
+
263
+ <div className="flex-1" />
 
 
 
 
 
 
 
 
 
 
 
264
 
265
+ {/* Add segment */}
266
+ <Button variant="ghost" size="sm" className="text-xs text-muted-foreground font-semibold gap-1.5 h-7">
267
+ <Plus size={13} />
268
+ Add segment
269
+ </Button>
270
+ </div>
 
 
 
271
  </div>
272
  )
273
  }
274
 
275
+ // --- Page ---
276
  export default function StudioPage() {
277
+ const [activeId, setActiveId] = useState<number>(1)
278
+ const [isPlaying, setIsPlaying] = useState(false)
279
+
280
+ const activeSegment = SEGMENTS.find(s => s.id === activeId) ?? null
281
 
282
  return (
283
+ <div className="flex flex-col h-screen bg-background text-foreground overflow-hidden">
284
+ {/* Top Bar */}
285
+ <header className="h-11 border-b border-border bg-card flex items-center px-4 gap-3 flex-shrink-0 sticky top-0 z-50">
286
+ <Link href="/">
287
+ <Button variant="ghost" size="icon" className="h-7 w-7 text-muted-foreground hover:text-foreground">
288
+ <ArrowLeft size={16} />
289
+ </Button>
290
+ </Link>
291
+
292
 
293
+ {/* Undo / Redo */}
294
+ <div className="flex items-center gap-0.5 ml-1">
295
+ <Tooltip>
296
+ <TooltipTrigger asChild>
297
+ <Button variant="ghost" size="icon" className="h-7 w-7 text-muted-foreground">
298
+ <ArrowCounterClockwise size={15} />
299
+ </Button>
300
+ </TooltipTrigger>
301
+ <TooltipContent>Undo</TooltipContent>
302
+ </Tooltip>
303
+ <Tooltip>
304
+ <TooltipTrigger asChild>
305
+ <Button variant="ghost" size="icon" className="h-7 w-7 text-muted-foreground">
306
+ <ArrowClockwise size={15} />
307
+ </Button>
308
+ </TooltipTrigger>
309
+ <TooltipContent>Redo</TooltipContent>
310
+ </Tooltip>
311
  </div>
312
 
313
+ <div className="flex-1" />
 
 
314
 
315
+ <Button className="gap-1.5 font-bold h-8 text-xs px-3">
316
+ <Export size={14} weight="bold" />
317
+ Export
318
+ </Button>
319
+ </header>
320
 
321
+ {/* Body */}
322
+ <div className="flex flex-1 overflow-hidden">
323
+ {/* Left: Transcript scroll */}
324
+ <div className="flex-1 overflow-hidden flex flex-col min-w-0">
325
+ <ScrollArea className="flex-1">
326
+ <div className="max-w-2xl mx-auto py-4">
327
+ {SEGMENTS.map(seg => (
328
+ <SegmentRow
329
+ key={seg.id}
330
+ seg={seg}
331
+ active={seg.id === activeId}
332
+ onClick={() => setActiveId(seg.id)}
333
+ />
334
+ ))}
335
+ </div>
336
+ </ScrollArea>
337
+ </div>
338
+
339
+ {/* Right: Properties panel ~260px */}
340
+ <div className="w-[280px] flex-shrink-0 overflow-hidden flex flex-col">
341
+ <RightPanel activeSegment={activeSegment} />
342
  </div>
343
  </div>
344
 
345
+ {/* Bottom: Timeline */}
346
+ <TimelineBar isPlaying={isPlaying} onToggle={() => setIsPlaying(p => !p)} />
347
+ </div>
348
  )
349
  }
demo/src/components/navbar.tsx ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import React from "react"
4
+ import { Sparkle, Bell } from "@phosphor-icons/react"
5
+ import { Button } from "@/components/ui/button"
6
+ import { Avatar, AvatarFallback } from "@/components/ui/avatar"
7
+ import { Separator } from "@/components/ui/separator"
8
+ import { Skeleton } from "@/components/ui/skeleton"
9
+
10
+ export function Navbar() {
11
+ return (
12
+ <header className="h-12 border-b border-border bg-card flex items-center justify-between px-5 sticky top-0 z-50">
13
+ <div className="flex items-center gap-2 text-foreground">
14
+ <Skeleton className="h-5 w-5 rounded bg-foreground/20" />
15
+ <span className="text-sm font-bold tracking-tight">Ethos</span>
16
+ </div>
17
+ <div className="flex items-center gap-1">
18
+ <Button variant="ghost" size="sm" className="text-muted-foreground font-medium h-8 px-3">Feedback</Button>
19
+ <Button variant="ghost" size="sm" className="text-muted-foreground font-medium h-8 px-3">Docs</Button>
20
+ <Button variant="ghost" size="sm" className="text-muted-foreground font-medium h-8 px-3 gap-1">
21
+ <Sparkle size={14} />Ask
22
+ </Button>
23
+ <Separator orientation="vertical" className="h-4 mx-2" />
24
+ <Button variant="ghost" size="icon" className="h-8 w-8 text-muted-foreground">
25
+ <Bell size={16} />
26
+ </Button>
27
+ <Avatar className="h-7 w-7 cursor-pointer ml-1">
28
+ <AvatarFallback className="bg-secondary text-foreground text-[11px] font-bold">U</AvatarFallback>
29
+ </Avatar>
30
+ </div>
31
+ </header>
32
+ )
33
+ }
demo/src/components/ui/collapsible.tsx ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import { Collapsible as CollapsiblePrimitive } from "radix-ui"
4
+
5
+ function Collapsible({
6
+ ...props
7
+ }: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
8
+ return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />
9
+ }
10
+
11
+ function CollapsibleTrigger({
12
+ ...props
13
+ }: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {
14
+ return (
15
+ <CollapsiblePrimitive.CollapsibleTrigger
16
+ data-slot="collapsible-trigger"
17
+ {...props}
18
+ />
19
+ )
20
+ }
21
+
22
+ function CollapsibleContent({
23
+ ...props
24
+ }: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) {
25
+ return (
26
+ <CollapsiblePrimitive.CollapsibleContent
27
+ data-slot="collapsible-content"
28
+ {...props}
29
+ />
30
+ )
31
+ }
32
+
33
+ export { Collapsible, CollapsibleTrigger, CollapsibleContent }
demo/src/components/ui/dialog.tsx ADDED
@@ -0,0 +1,165 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { Dialog as DialogPrimitive } from "radix-ui"
5
+
6
+ import { cn } from "@/lib/utils"
7
+ import { Button } from "@/components/ui/button"
8
+ import { XIcon } from "lucide-react"
9
+
10
+ function Dialog({
11
+ ...props
12
+ }: React.ComponentProps<typeof DialogPrimitive.Root>) {
13
+ return <DialogPrimitive.Root data-slot="dialog" {...props} />
14
+ }
15
+
16
+ function DialogTrigger({
17
+ ...props
18
+ }: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
19
+ return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
20
+ }
21
+
22
+ function DialogPortal({
23
+ ...props
24
+ }: React.ComponentProps<typeof DialogPrimitive.Portal>) {
25
+ return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
26
+ }
27
+
28
+ function DialogClose({
29
+ ...props
30
+ }: React.ComponentProps<typeof DialogPrimitive.Close>) {
31
+ return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
32
+ }
33
+
34
+ function DialogOverlay({
35
+ className,
36
+ ...props
37
+ }: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
38
+ return (
39
+ <DialogPrimitive.Overlay
40
+ data-slot="dialog-overlay"
41
+ className={cn(
42
+ "data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 fixed inset-0 isolate z-50 bg-black/80 duration-100 supports-backdrop-filter:backdrop-blur-xs",
43
+ className
44
+ )}
45
+ {...props}
46
+ />
47
+ )
48
+ }
49
+
50
+ function DialogContent({
51
+ className,
52
+ children,
53
+ showCloseButton = true,
54
+ ...props
55
+ }: React.ComponentProps<typeof DialogPrimitive.Content> & {
56
+ showCloseButton?: boolean
57
+ }) {
58
+ return (
59
+ <DialogPortal>
60
+ <DialogOverlay />
61
+ <DialogPrimitive.Content
62
+ data-slot="dialog-content"
63
+ className={cn(
64
+ "bg-background data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 ring-foreground/5 fixed top-1/2 left-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-6 rounded-4xl p-6 text-sm ring-1 duration-100 outline-none sm:max-w-md",
65
+ className
66
+ )}
67
+ {...props}
68
+ >
69
+ {children}
70
+ {showCloseButton && (
71
+ <DialogPrimitive.Close data-slot="dialog-close" asChild>
72
+ <Button
73
+ variant="ghost"
74
+ className="absolute top-4 right-4"
75
+ size="icon-sm"
76
+ >
77
+ <XIcon
78
+ />
79
+ <span className="sr-only">Close</span>
80
+ </Button>
81
+ </DialogPrimitive.Close>
82
+ )}
83
+ </DialogPrimitive.Content>
84
+ </DialogPortal>
85
+ )
86
+ }
87
+
88
+ function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
89
+ return (
90
+ <div
91
+ data-slot="dialog-header"
92
+ className={cn("flex flex-col gap-2", className)}
93
+ {...props}
94
+ />
95
+ )
96
+ }
97
+
98
+ function DialogFooter({
99
+ className,
100
+ showCloseButton = false,
101
+ children,
102
+ ...props
103
+ }: React.ComponentProps<"div"> & {
104
+ showCloseButton?: boolean
105
+ }) {
106
+ return (
107
+ <div
108
+ data-slot="dialog-footer"
109
+ className={cn(
110
+ "flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
111
+ className
112
+ )}
113
+ {...props}
114
+ >
115
+ {children}
116
+ {showCloseButton && (
117
+ <DialogPrimitive.Close asChild>
118
+ <Button variant="outline">Close</Button>
119
+ </DialogPrimitive.Close>
120
+ )}
121
+ </div>
122
+ )
123
+ }
124
+
125
+ function DialogTitle({
126
+ className,
127
+ ...props
128
+ }: React.ComponentProps<typeof DialogPrimitive.Title>) {
129
+ return (
130
+ <DialogPrimitive.Title
131
+ data-slot="dialog-title"
132
+ className={cn("text-base leading-none font-medium", className)}
133
+ {...props}
134
+ />
135
+ )
136
+ }
137
+
138
+ function DialogDescription({
139
+ className,
140
+ ...props
141
+ }: React.ComponentProps<typeof DialogPrimitive.Description>) {
142
+ return (
143
+ <DialogPrimitive.Description
144
+ data-slot="dialog-description"
145
+ className={cn(
146
+ "text-muted-foreground *:[a]:hover:text-foreground text-sm *:[a]:underline *:[a]:underline-offset-3",
147
+ className
148
+ )}
149
+ {...props}
150
+ />
151
+ )
152
+ }
153
+
154
+ export {
155
+ Dialog,
156
+ DialogClose,
157
+ DialogContent,
158
+ DialogDescription,
159
+ DialogFooter,
160
+ DialogHeader,
161
+ DialogOverlay,
162
+ DialogPortal,
163
+ DialogTitle,
164
+ DialogTrigger,
165
+ }
demo/src/components/ui/dropdown-menu.tsx ADDED
@@ -0,0 +1,269 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui"
5
+
6
+ import { cn } from "@/lib/utils"
7
+ import { CheckIcon, ChevronRightIcon } from "lucide-react"
8
+
9
+ function DropdownMenu({
10
+ ...props
11
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
12
+ return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
13
+ }
14
+
15
+ function DropdownMenuPortal({
16
+ ...props
17
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
18
+ return (
19
+ <DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
20
+ )
21
+ }
22
+
23
+ function DropdownMenuTrigger({
24
+ ...props
25
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
26
+ return (
27
+ <DropdownMenuPrimitive.Trigger
28
+ data-slot="dropdown-menu-trigger"
29
+ {...props}
30
+ />
31
+ )
32
+ }
33
+
34
+ function DropdownMenuContent({
35
+ className,
36
+ align = "start",
37
+ sideOffset = 4,
38
+ ...props
39
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
40
+ return (
41
+ <DropdownMenuPrimitive.Portal>
42
+ <DropdownMenuPrimitive.Content
43
+ data-slot="dropdown-menu-content"
44
+ sideOffset={sideOffset}
45
+ align={align}
46
+ className={cn("data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/5 bg-popover text-popover-foreground z-50 max-h-(--radix-dropdown-menu-content-available-height) w-(--radix-dropdown-menu-trigger-width) min-w-48 origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-2xl p-1 shadow-2xl ring-1 duration-100 data-[state=closed]:overflow-hidden", className )}
47
+ {...props}
48
+ />
49
+ </DropdownMenuPrimitive.Portal>
50
+ )
51
+ }
52
+
53
+ function DropdownMenuGroup({
54
+ ...props
55
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
56
+ return (
57
+ <DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
58
+ )
59
+ }
60
+
61
+ function DropdownMenuItem({
62
+ className,
63
+ inset,
64
+ variant = "default",
65
+ ...props
66
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
67
+ inset?: boolean
68
+ variant?: "default" | "destructive"
69
+ }) {
70
+ return (
71
+ <DropdownMenuPrimitive.Item
72
+ data-slot="dropdown-menu-item"
73
+ data-inset={inset}
74
+ data-variant={variant}
75
+ className={cn(
76
+ "focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:text-destructive not-data-[variant=destructive]:focus:**:text-accent-foreground group/dropdown-menu-item relative flex cursor-default items-center gap-2.5 rounded-xl px-3 py-2 text-sm outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-inset:pl-9.5 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
77
+ className
78
+ )}
79
+ {...props}
80
+ />
81
+ )
82
+ }
83
+
84
+ function DropdownMenuCheckboxItem({
85
+ className,
86
+ children,
87
+ checked,
88
+ inset,
89
+ ...props
90
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem> & {
91
+ inset?: boolean
92
+ }) {
93
+ return (
94
+ <DropdownMenuPrimitive.CheckboxItem
95
+ data-slot="dropdown-menu-checkbox-item"
96
+ data-inset={inset}
97
+ className={cn(
98
+ "focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground relative flex cursor-default items-center gap-2.5 rounded-xl py-2 pr-8 pl-3 text-sm outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-inset:pl-9.5 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
99
+ className
100
+ )}
101
+ checked={checked}
102
+ {...props}
103
+ >
104
+ <span
105
+ className="pointer-events-none absolute right-2 flex items-center justify-center"
106
+ data-slot="dropdown-menu-checkbox-item-indicator"
107
+ >
108
+ <DropdownMenuPrimitive.ItemIndicator>
109
+ <CheckIcon
110
+ />
111
+ </DropdownMenuPrimitive.ItemIndicator>
112
+ </span>
113
+ {children}
114
+ </DropdownMenuPrimitive.CheckboxItem>
115
+ )
116
+ }
117
+
118
+ function DropdownMenuRadioGroup({
119
+ ...props
120
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
121
+ return (
122
+ <DropdownMenuPrimitive.RadioGroup
123
+ data-slot="dropdown-menu-radio-group"
124
+ {...props}
125
+ />
126
+ )
127
+ }
128
+
129
+ function DropdownMenuRadioItem({
130
+ className,
131
+ children,
132
+ inset,
133
+ ...props
134
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem> & {
135
+ inset?: boolean
136
+ }) {
137
+ return (
138
+ <DropdownMenuPrimitive.RadioItem
139
+ data-slot="dropdown-menu-radio-item"
140
+ data-inset={inset}
141
+ className={cn(
142
+ "focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground relative flex cursor-default items-center gap-2.5 rounded-xl py-2 pr-8 pl-3 text-sm outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-inset:pl-9.5 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
143
+ className
144
+ )}
145
+ {...props}
146
+ >
147
+ <span
148
+ className="pointer-events-none absolute right-2 flex items-center justify-center"
149
+ data-slot="dropdown-menu-radio-item-indicator"
150
+ >
151
+ <DropdownMenuPrimitive.ItemIndicator>
152
+ <CheckIcon
153
+ />
154
+ </DropdownMenuPrimitive.ItemIndicator>
155
+ </span>
156
+ {children}
157
+ </DropdownMenuPrimitive.RadioItem>
158
+ )
159
+ }
160
+
161
+ function DropdownMenuLabel({
162
+ className,
163
+ inset,
164
+ ...props
165
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
166
+ inset?: boolean
167
+ }) {
168
+ return (
169
+ <DropdownMenuPrimitive.Label
170
+ data-slot="dropdown-menu-label"
171
+ data-inset={inset}
172
+ className={cn(
173
+ "text-muted-foreground px-3 py-2.5 text-xs data-inset:pl-9.5",
174
+ className
175
+ )}
176
+ {...props}
177
+ />
178
+ )
179
+ }
180
+
181
+ function DropdownMenuSeparator({
182
+ className,
183
+ ...props
184
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
185
+ return (
186
+ <DropdownMenuPrimitive.Separator
187
+ data-slot="dropdown-menu-separator"
188
+ className={cn("bg-border/50 -mx-1 my-1 h-px", className)}
189
+ {...props}
190
+ />
191
+ )
192
+ }
193
+
194
+ function DropdownMenuShortcut({
195
+ className,
196
+ ...props
197
+ }: React.ComponentProps<"span">) {
198
+ return (
199
+ <span
200
+ data-slot="dropdown-menu-shortcut"
201
+ className={cn(
202
+ "text-muted-foreground group-focus/dropdown-menu-item:text-accent-foreground ml-auto text-xs tracking-widest",
203
+ className
204
+ )}
205
+ {...props}
206
+ />
207
+ )
208
+ }
209
+
210
+ function DropdownMenuSub({
211
+ ...props
212
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
213
+ return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
214
+ }
215
+
216
+ function DropdownMenuSubTrigger({
217
+ className,
218
+ inset,
219
+ children,
220
+ ...props
221
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
222
+ inset?: boolean
223
+ }) {
224
+ return (
225
+ <DropdownMenuPrimitive.SubTrigger
226
+ data-slot="dropdown-menu-sub-trigger"
227
+ data-inset={inset}
228
+ className={cn(
229
+ "focus:bg-accent focus:text-accent-foreground data-open:bg-accent data-open:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground flex cursor-default items-center gap-2 rounded-xl px-3 py-2 text-sm outline-hidden select-none data-inset:pl-9.5 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
230
+ className
231
+ )}
232
+ {...props}
233
+ >
234
+ {children}
235
+ <ChevronRightIcon className="ml-auto" />
236
+ </DropdownMenuPrimitive.SubTrigger>
237
+ )
238
+ }
239
+
240
+ function DropdownMenuSubContent({
241
+ className,
242
+ ...props
243
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
244
+ return (
245
+ <DropdownMenuPrimitive.SubContent
246
+ data-slot="dropdown-menu-sub-content"
247
+ className={cn("data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/5 bg-popover text-popover-foreground z-50 min-w-36 origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-2xl p-1 shadow-2xl ring-1 duration-100", className )}
248
+ {...props}
249
+ />
250
+ )
251
+ }
252
+
253
+ export {
254
+ DropdownMenu,
255
+ DropdownMenuPortal,
256
+ DropdownMenuTrigger,
257
+ DropdownMenuContent,
258
+ DropdownMenuGroup,
259
+ DropdownMenuLabel,
260
+ DropdownMenuItem,
261
+ DropdownMenuCheckboxItem,
262
+ DropdownMenuRadioGroup,
263
+ DropdownMenuRadioItem,
264
+ DropdownMenuSeparator,
265
+ DropdownMenuShortcut,
266
+ DropdownMenuSub,
267
+ DropdownMenuSubTrigger,
268
+ DropdownMenuSubContent,
269
+ }
demo/src/components/ui/input.tsx ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+
3
+ import { cn } from "@/lib/utils"
4
+
5
+ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
6
+ return (
7
+ <input
8
+ type={type}
9
+ data-slot="input"
10
+ className={cn(
11
+ "bg-input/30 border-input focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 file:text-foreground placeholder:text-muted-foreground h-9 w-full min-w-0 rounded-4xl border px-3 py-1 text-base transition-colors outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:ring-[3px] disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:ring-[3px] md:text-sm",
12
+ className
13
+ )}
14
+ {...props}
15
+ />
16
+ )
17
+ }
18
+
19
+ export { Input }
demo/src/components/ui/label.tsx ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { Label as LabelPrimitive } from "radix-ui"
5
+
6
+ import { cn } from "@/lib/utils"
7
+
8
+ function Label({
9
+ className,
10
+ ...props
11
+ }: React.ComponentProps<typeof LabelPrimitive.Root>) {
12
+ return (
13
+ <LabelPrimitive.Root
14
+ data-slot="label"
15
+ className={cn(
16
+ "flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
17
+ className
18
+ )}
19
+ {...props}
20
+ />
21
+ )
22
+ }
23
+
24
+ export { Label }
demo/src/components/ui/progress.tsx ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { Progress as ProgressPrimitive } from "radix-ui"
5
+
6
+ import { cn } from "@/lib/utils"
7
+
8
+ function Progress({
9
+ className,
10
+ value,
11
+ ...props
12
+ }: React.ComponentProps<typeof ProgressPrimitive.Root>) {
13
+ return (
14
+ <ProgressPrimitive.Root
15
+ data-slot="progress"
16
+ className={cn(
17
+ "bg-muted relative flex h-3 w-full items-center overflow-x-hidden rounded-4xl",
18
+ className
19
+ )}
20
+ {...props}
21
+ >
22
+ <ProgressPrimitive.Indicator
23
+ data-slot="progress-indicator"
24
+ className="bg-primary size-full flex-1 transition-all"
25
+ style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
26
+ />
27
+ </ProgressPrimitive.Root>
28
+ )
29
+ }
30
+
31
+ export { Progress }
demo/src/components/ui/select.tsx ADDED
@@ -0,0 +1,195 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { Select as SelectPrimitive } from "radix-ui"
5
+
6
+ import { cn } from "@/lib/utils"
7
+ import { ChevronDownIcon, CheckIcon, ChevronUpIcon } from "lucide-react"
8
+
9
+ function Select({
10
+ ...props
11
+ }: React.ComponentProps<typeof SelectPrimitive.Root>) {
12
+ return <SelectPrimitive.Root data-slot="select" {...props} />
13
+ }
14
+
15
+ function SelectGroup({
16
+ className,
17
+ ...props
18
+ }: React.ComponentProps<typeof SelectPrimitive.Group>) {
19
+ return (
20
+ <SelectPrimitive.Group
21
+ data-slot="select-group"
22
+ className={cn("scroll-my-1 p-1", className)}
23
+ {...props}
24
+ />
25
+ )
26
+ }
27
+
28
+ function SelectValue({
29
+ ...props
30
+ }: React.ComponentProps<typeof SelectPrimitive.Value>) {
31
+ return <SelectPrimitive.Value data-slot="select-value" {...props} />
32
+ }
33
+
34
+ function SelectTrigger({
35
+ className,
36
+ size = "default",
37
+ children,
38
+ ...props
39
+ }: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
40
+ size?: "sm" | "default"
41
+ }) {
42
+ return (
43
+ <SelectPrimitive.Trigger
44
+ data-slot="select-trigger"
45
+ data-size={size}
46
+ className={cn(
47
+ "border-input data-placeholder:text-muted-foreground bg-input/30 dark:hover:bg-input/50 focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 flex w-fit items-center justify-between gap-1.5 rounded-4xl border px-3 py-2 text-sm whitespace-nowrap transition-colors outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:ring-[3px] data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
48
+ className
49
+ )}
50
+ {...props}
51
+ >
52
+ {children}
53
+ <SelectPrimitive.Icon asChild>
54
+ <ChevronDownIcon className="text-muted-foreground pointer-events-none size-4" />
55
+ </SelectPrimitive.Icon>
56
+ </SelectPrimitive.Trigger>
57
+ )
58
+ }
59
+
60
+ function SelectContent({
61
+ className,
62
+ children,
63
+ position = "item-aligned",
64
+ align = "center",
65
+ ...props
66
+ }: React.ComponentProps<typeof SelectPrimitive.Content>) {
67
+ return (
68
+ <SelectPrimitive.Portal>
69
+ <SelectPrimitive.Content
70
+ data-slot="select-content"
71
+ data-align-trigger={position === "item-aligned"}
72
+ className={cn("bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/5 relative z-50 max-h-(--radix-select-content-available-height) min-w-36 origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-2xl shadow-2xl ring-1 duration-100 data-[align-trigger=true]:animate-none", position ==="popper"&&"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1", className )}
73
+ position={position}
74
+ align={align}
75
+ {...props}
76
+ >
77
+ <SelectScrollUpButton />
78
+ <SelectPrimitive.Viewport
79
+ data-position={position}
80
+ className={cn(
81
+ "data-[position=popper]:h-(--radix-select-trigger-height) data-[position=popper]:w-full data-[position=popper]:min-w-(--radix-select-trigger-width)",
82
+ position === "popper" && ""
83
+ )}
84
+ >
85
+ {children}
86
+ </SelectPrimitive.Viewport>
87
+ <SelectScrollDownButton />
88
+ </SelectPrimitive.Content>
89
+ </SelectPrimitive.Portal>
90
+ )
91
+ }
92
+
93
+ function SelectLabel({
94
+ className,
95
+ ...props
96
+ }: React.ComponentProps<typeof SelectPrimitive.Label>) {
97
+ return (
98
+ <SelectPrimitive.Label
99
+ data-slot="select-label"
100
+ className={cn("text-muted-foreground px-3 py-2.5 text-xs", className)}
101
+ {...props}
102
+ />
103
+ )
104
+ }
105
+
106
+ function SelectItem({
107
+ className,
108
+ children,
109
+ ...props
110
+ }: React.ComponentProps<typeof SelectPrimitive.Item>) {
111
+ return (
112
+ <SelectPrimitive.Item
113
+ data-slot="select-item"
114
+ className={cn(
115
+ "focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground relative flex w-full cursor-default items-center gap-2.5 rounded-xl py-2 pr-8 pl-3 text-sm outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
116
+ className
117
+ )}
118
+ {...props}
119
+ >
120
+ <span className="pointer-events-none absolute right-2 flex size-4 items-center justify-center">
121
+ <SelectPrimitive.ItemIndicator>
122
+ <CheckIcon className="pointer-events-none" />
123
+ </SelectPrimitive.ItemIndicator>
124
+ </span>
125
+ <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
126
+ </SelectPrimitive.Item>
127
+ )
128
+ }
129
+
130
+ function SelectSeparator({
131
+ className,
132
+ ...props
133
+ }: React.ComponentProps<typeof SelectPrimitive.Separator>) {
134
+ return (
135
+ <SelectPrimitive.Separator
136
+ data-slot="select-separator"
137
+ className={cn(
138
+ "bg-border/50 pointer-events-none -mx-1 my-1 h-px",
139
+ className
140
+ )}
141
+ {...props}
142
+ />
143
+ )
144
+ }
145
+
146
+ function SelectScrollUpButton({
147
+ className,
148
+ ...props
149
+ }: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
150
+ return (
151
+ <SelectPrimitive.ScrollUpButton
152
+ data-slot="select-scroll-up-button"
153
+ className={cn(
154
+ "bg-popover z-10 flex cursor-default items-center justify-center py-1 [&_svg:not([class*='size-'])]:size-4",
155
+ className
156
+ )}
157
+ {...props}
158
+ >
159
+ <ChevronUpIcon
160
+ />
161
+ </SelectPrimitive.ScrollUpButton>
162
+ )
163
+ }
164
+
165
+ function SelectScrollDownButton({
166
+ className,
167
+ ...props
168
+ }: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
169
+ return (
170
+ <SelectPrimitive.ScrollDownButton
171
+ data-slot="select-scroll-down-button"
172
+ className={cn(
173
+ "bg-popover z-10 flex cursor-default items-center justify-center py-1 [&_svg:not([class*='size-'])]:size-4",
174
+ className
175
+ )}
176
+ {...props}
177
+ >
178
+ <ChevronDownIcon
179
+ />
180
+ </SelectPrimitive.ScrollDownButton>
181
+ )
182
+ }
183
+
184
+ export {
185
+ Select,
186
+ SelectContent,
187
+ SelectGroup,
188
+ SelectItem,
189
+ SelectLabel,
190
+ SelectScrollDownButton,
191
+ SelectScrollUpButton,
192
+ SelectSeparator,
193
+ SelectTrigger,
194
+ SelectValue,
195
+ }
demo/src/components/ui/separator.tsx ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { Separator as SeparatorPrimitive } from "radix-ui"
5
+
6
+ import { cn } from "@/lib/utils"
7
+
8
+ function Separator({
9
+ className,
10
+ orientation = "horizontal",
11
+ decorative = true,
12
+ ...props
13
+ }: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
14
+ return (
15
+ <SeparatorPrimitive.Root
16
+ data-slot="separator"
17
+ decorative={decorative}
18
+ orientation={orientation}
19
+ className={cn(
20
+ "bg-border shrink-0 data-horizontal:h-px data-horizontal:w-full data-vertical:w-px data-vertical:self-stretch",
21
+ className
22
+ )}
23
+ {...props}
24
+ />
25
+ )
26
+ }
27
+
28
+ export { Separator }
demo/src/components/ui/skeleton.tsx ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { cn } from "@/lib/utils"
2
+
3
+ function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
4
+ return (
5
+ <div
6
+ data-slot="skeleton"
7
+ className={cn("bg-muted animate-pulse rounded-xl", className)}
8
+ {...props}
9
+ />
10
+ )
11
+ }
12
+
13
+ export { Skeleton }
demo/src/components/ui/switch.tsx ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { Switch as SwitchPrimitive } from "radix-ui"
5
+
6
+ import { cn } from "@/lib/utils"
7
+
8
+ function Switch({
9
+ className,
10
+ size = "default",
11
+ ...props
12
+ }: React.ComponentProps<typeof SwitchPrimitive.Root> & {
13
+ size?: "sm" | "default"
14
+ }) {
15
+ return (
16
+ <SwitchPrimitive.Root
17
+ data-slot="switch"
18
+ data-size={size}
19
+ className={cn(
20
+ "data-checked:bg-primary data-unchecked:bg-input focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 dark:data-unchecked:bg-input/80 peer group/switch relative inline-flex shrink-0 items-center rounded-full border border-transparent transition-all outline-none after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:ring-[3px] aria-invalid:ring-[3px] data-disabled:cursor-not-allowed data-disabled:opacity-50 data-[size=default]:h-[18.4px] data-[size=default]:w-[32px] data-[size=sm]:h-[14px] data-[size=sm]:w-[24px]",
21
+ className
22
+ )}
23
+ {...props}
24
+ >
25
+ <SwitchPrimitive.Thumb
26
+ data-slot="switch-thumb"
27
+ className="bg-background dark:data-unchecked:bg-foreground dark:data-checked:bg-primary-foreground pointer-events-none block rounded-full ring-0 transition-transform group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3 group-data-[size=default]/switch:data-checked:translate-x-[calc(100%-2px)] group-data-[size=sm]/switch:data-checked:translate-x-[calc(100%-2px)] group-data-[size=default]/switch:data-unchecked:translate-x-0 group-data-[size=sm]/switch:data-unchecked:translate-x-0"
28
+ />
29
+ </SwitchPrimitive.Root>
30
+ )
31
+ }
32
+
33
+ export { Switch }
demo/src/components/ui/table.tsx ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+
5
+ import { cn } from "@/lib/utils"
6
+
7
+ function Table({ className, ...props }: React.ComponentProps<"table">) {
8
+ return (
9
+ <div
10
+ data-slot="table-container"
11
+ className="relative w-full overflow-x-auto"
12
+ >
13
+ <table
14
+ data-slot="table"
15
+ className={cn("w-full caption-bottom text-sm", className)}
16
+ {...props}
17
+ />
18
+ </div>
19
+ )
20
+ }
21
+
22
+ function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
23
+ return (
24
+ <thead
25
+ data-slot="table-header"
26
+ className={cn("[&_tr]:border-b", className)}
27
+ {...props}
28
+ />
29
+ )
30
+ }
31
+
32
+ function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
33
+ return (
34
+ <tbody
35
+ data-slot="table-body"
36
+ className={cn("[&_tr:last-child]:border-0", className)}
37
+ {...props}
38
+ />
39
+ )
40
+ }
41
+
42
+ function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
43
+ return (
44
+ <tfoot
45
+ data-slot="table-footer"
46
+ className={cn(
47
+ "bg-muted/50 border-t font-medium [&>tr]:last:border-b-0",
48
+ className
49
+ )}
50
+ {...props}
51
+ />
52
+ )
53
+ }
54
+
55
+ function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
56
+ return (
57
+ <tr
58
+ data-slot="table-row"
59
+ className={cn(
60
+ "hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
61
+ className
62
+ )}
63
+ {...props}
64
+ />
65
+ )
66
+ }
67
+
68
+ function TableHead({ className, ...props }: React.ComponentProps<"th">) {
69
+ return (
70
+ <th
71
+ data-slot="table-head"
72
+ className={cn(
73
+ "text-foreground h-12 px-3 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0",
74
+ className
75
+ )}
76
+ {...props}
77
+ />
78
+ )
79
+ }
80
+
81
+ function TableCell({ className, ...props }: React.ComponentProps<"td">) {
82
+ return (
83
+ <td
84
+ data-slot="table-cell"
85
+ className={cn(
86
+ "p-3 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0",
87
+ className
88
+ )}
89
+ {...props}
90
+ />
91
+ )
92
+ }
93
+
94
+ function TableCaption({
95
+ className,
96
+ ...props
97
+ }: React.ComponentProps<"caption">) {
98
+ return (
99
+ <caption
100
+ data-slot="table-caption"
101
+ className={cn("text-muted-foreground mt-4 text-sm", className)}
102
+ {...props}
103
+ />
104
+ )
105
+ }
106
+
107
+ export {
108
+ Table,
109
+ TableHeader,
110
+ TableBody,
111
+ TableFooter,
112
+ TableHead,
113
+ TableRow,
114
+ TableCell,
115
+ TableCaption,
116
+ }