Spaces:
Sleeping
Sleeping
feat: implement Ethos branding, extract Navbar, and polish Studio UI
Browse files- .context/technical_spec.md +1 -1
- demo/src/app/globals.css +78 -56
- demo/src/app/layout.tsx +4 -4
- demo/src/app/page.tsx +195 -63
- demo/src/app/studio/page.tsx +291 -292
- demo/src/components/navbar.tsx +33 -0
- demo/src/components/ui/collapsible.tsx +33 -0
- demo/src/components/ui/dialog.tsx +165 -0
- demo/src/components/ui/dropdown-menu.tsx +269 -0
- demo/src/components/ui/input.tsx +19 -0
- demo/src/components/ui/label.tsx +24 -0
- demo/src/components/ui/progress.tsx +31 -0
- demo/src/components/ui/select.tsx +195 -0
- demo/src/components/ui/separator.tsx +28 -0
- demo/src/components/ui/skeleton.tsx +13 -0
- demo/src/components/ui/switch.tsx +33 -0
- demo/src/components/ui/table.tsx +116 -0
.context/technical_spec.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
## core components
|
| 2 |
-
- **base model:**
|
| 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.
|
| 52 |
-
|
| 53 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
--card: oklch(1 0 0);
|
| 55 |
-
--card-foreground: oklch(0.
|
|
|
|
|
|
|
| 56 |
--popover: oklch(1 0 0);
|
| 57 |
-
--popover-foreground: oklch(0.
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
--
|
| 61 |
-
--
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
--
|
| 65 |
-
--
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
--destructive: oklch(0.577 0.245 27.325);
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
--
|
| 70 |
-
--
|
| 71 |
-
--
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
--chart-
|
| 75 |
-
--
|
| 76 |
-
--
|
| 77 |
-
--
|
| 78 |
-
--
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
--sidebar
|
| 82 |
-
--sidebar-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
}
|
| 84 |
|
| 85 |
.dark {
|
| 86 |
-
--background: oklch(0.
|
| 87 |
-
--foreground: oklch(0.
|
| 88 |
-
--card: oklch(0.
|
| 89 |
-
--card-foreground: oklch(0.
|
| 90 |
-
--popover: oklch(0.
|
| 91 |
-
--popover-foreground: oklch(0.
|
| 92 |
-
--primary: oklch(0.
|
| 93 |
-
--primary-foreground: oklch(0.
|
| 94 |
-
--secondary: oklch(0.
|
| 95 |
-
--secondary-foreground: oklch(0.
|
| 96 |
-
--muted: oklch(0.
|
| 97 |
-
--muted-foreground: oklch(0.
|
| 98 |
-
--accent: oklch(0.
|
| 99 |
-
--accent-foreground: oklch(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.
|
| 104 |
-
--chart-1: oklch(0.
|
| 105 |
-
--chart-2: oklch(0.
|
| 106 |
-
--chart-3: oklch(0.
|
| 107 |
-
--chart-4: oklch(0.
|
| 108 |
-
--chart-5: oklch(0.
|
| 109 |
-
--sidebar: oklch(0.
|
| 110 |
-
--sidebar-foreground: oklch(0.
|
| 111 |
-
--sidebar-primary: oklch(0.
|
| 112 |
-
--sidebar-primary-foreground: oklch(0.
|
| 113 |
-
--sidebar-accent: oklch(0.
|
| 114 |
-
--sidebar-accent-foreground: oklch(0.
|
| 115 |
--sidebar-border: oklch(1 0 0 / 10%);
|
| 116 |
-
--sidebar-ring: oklch(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: "
|
| 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"
|
| 24 |
<body
|
| 25 |
-
className={`${lato.variable} antialiased selection:bg-
|
| 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 {
|
|
|
|
|
|
|
|
|
|
| 7 |
import { Button } from "@/components/ui/button"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
|
| 9 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
return (
|
| 11 |
-
<
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
<div className="space-y-4">
|
| 22 |
-
<
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
</div>
|
| 40 |
|
| 41 |
-
<
|
| 42 |
-
<
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
</Button>
|
| 51 |
</div>
|
| 52 |
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
|
|
|
| 64 |
</div>
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
className="w-[2px] bg-white rounded-full"
|
| 75 |
/>
|
| 76 |
-
|
| 77 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
|
|
|
| 4 |
import { motion, AnimatePresence } from "framer-motion"
|
| 5 |
import {
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 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,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
|
| 26 |
// --- Mock Data ---
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
|
| 28 |
-
const
|
| 29 |
-
{ id: 1, speaker: "
|
| 30 |
-
{ id: 2, speaker: "
|
| 31 |
-
{ id: 3, speaker: "
|
| 32 |
-
{ id: 4, speaker: "
|
|
|
|
|
|
|
|
|
|
| 33 |
]
|
| 34 |
|
| 35 |
-
|
|
|
|
|
|
|
|
|
|
| 36 |
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
</div>
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
<div className="w-4 h-4 rounded-full bg-blue-500" />
|
| 54 |
</div>
|
| 55 |
-
</div>
|
| 56 |
-
</header>
|
| 57 |
-
)
|
| 58 |
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 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 |
-
</
|
| 105 |
-
|
| 106 |
-
|
| 107 |
|
| 108 |
-
|
|
|
|
| 109 |
return (
|
| 110 |
-
<div className="
|
| 111 |
-
{/*
|
| 112 |
-
<
|
| 113 |
-
<div className="flex items-center justify-
|
| 114 |
-
<
|
| 115 |
-
|
| 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 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 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 |
-
|
| 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 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
<
|
| 172 |
-
<
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
<div className="
|
| 186 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 195 |
-
|
| 196 |
-
</div>
|
| 197 |
-
))}
|
| 198 |
-
</div>
|
| 199 |
-
</Card>
|
| 200 |
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
<
|
| 205 |
-
<
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 226 |
</div>
|
| 227 |
-
</
|
| 228 |
-
|
| 229 |
</div>
|
| 230 |
-
</
|
| 231 |
</div>
|
| 232 |
)
|
| 233 |
}
|
| 234 |
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
<
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
<
|
| 248 |
-
|
|
|
|
|
|
|
| 249 |
</Button>
|
| 250 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 251 |
</div>
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
<
|
| 256 |
-
|
| 257 |
-
className=
|
| 258 |
-
|
| 259 |
-
/>
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
|
|
|
| 267 |
</div>
|
| 268 |
-
<Slider defaultValue={[25]} max={100} step={1} className="w-24" />
|
| 269 |
</div>
|
| 270 |
-
</div>
|
| 271 |
-
</footer>
|
| 272 |
-
)
|
| 273 |
|
| 274 |
-
|
| 275 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 276 |
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 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 |
-
{
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
/>
|
| 314 |
-
)}
|
| 315 |
-
</motion.button>
|
| 316 |
</div>
|
| 317 |
)
|
| 318 |
}
|
| 319 |
|
|
|
|
| 320 |
export default function StudioPage() {
|
| 321 |
-
const [
|
|
|
|
|
|
|
|
|
|
| 322 |
|
| 323 |
return (
|
| 324 |
-
<
|
| 325 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 326 |
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 334 |
</div>
|
| 335 |
|
| 336 |
-
|
| 337 |
-
<div className="flex-1 flex flex-col relative">
|
| 338 |
-
<QuadViewAnalytics activeSegment={activeSegment} />
|
| 339 |
|
| 340 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 341 |
|
| 342 |
-
|
| 343 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 344 |
</div>
|
| 345 |
</div>
|
| 346 |
|
| 347 |
-
|
| 348 |
-
|
|
|
|
| 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 |
+
}
|