barton commited on
Commit
a85305f
·
0 Parent(s):

Duplicate from bmorphism/vibecraft

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .env +3 -0
  2. .eslintrc.json +3 -0
  3. .gitignore +35 -0
  4. .nvmrc +1 -0
  5. Dockerfile +65 -0
  6. README.md +41 -0
  7. components.json +16 -0
  8. next.config.js +10 -0
  9. package-lock.json +0 -0
  10. package.json +60 -0
  11. postcss.config.js +6 -0
  12. public/next.svg +1 -0
  13. public/vercel.svg +1 -0
  14. scripts/test.js +23 -0
  15. src/app/data/data.ts +71 -0
  16. src/app/data/mock.json +16 -0
  17. src/app/data/schema.ts +13 -0
  18. src/app/favicon.ico +0 -0
  19. src/app/forlater/page.tsx +18 -0
  20. src/app/globals.css +27 -0
  21. src/app/layout.tsx +24 -0
  22. src/app/page.tsx +27 -0
  23. src/app/studio/[ownerId]/main.tsx +26 -0
  24. src/app/studio/[ownerId]/page.tsx +22 -0
  25. src/app/types.ts +258 -0
  26. src/components/business/menu.tsx +200 -0
  27. src/components/business/refresh.tsx +27 -0
  28. src/components/business/timeline/index.tsx +71 -0
  29. src/components/business/video-form.tsx +63 -0
  30. src/components/business/video-player.tsx +33 -0
  31. src/components/business/videos/change-status-button.tsx +20 -0
  32. src/components/business/videos/column-header.tsx +71 -0
  33. src/components/business/videos/columns.tsx +151 -0
  34. src/components/business/videos/video-actions.tsx +52 -0
  35. src/components/business/videos/video-table.tsx +128 -0
  36. src/components/ui/accordion.tsx +60 -0
  37. src/components/ui/alert.tsx +59 -0
  38. src/components/ui/avatar.tsx +50 -0
  39. src/components/ui/badge.tsx +36 -0
  40. src/components/ui/button.tsx +56 -0
  41. src/components/ui/calendar.tsx +64 -0
  42. src/components/ui/card.tsx +79 -0
  43. src/components/ui/checkbox.tsx +30 -0
  44. src/components/ui/collapsible.tsx +11 -0
  45. src/components/ui/command.tsx +155 -0
  46. src/components/ui/dialog.tsx +123 -0
  47. src/components/ui/dropdown-menu.tsx +200 -0
  48. src/components/ui/input.tsx +25 -0
  49. src/components/ui/menubar.tsx +236 -0
  50. src/components/ui/popover.tsx +31 -0
.env ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ NEXT_PUBLIC_BASE_URL=https://jbilcke-hf-videochain-ui.hf.space
2
+ NEXT_PUBLIC_DOWNLOAD_URL=https://jbilcke-hf-videochain-api.hf.space
3
+ VC_VIDEOCHAIN_API_URL=https://jbilcke-hf-videochain-api.hf.space
.eslintrc.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ {
2
+ "extends": "next/core-web-vitals"
3
+ }
.gitignore ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2
+
3
+ # dependencies
4
+ /node_modules
5
+ /.pnp
6
+ .pnp.js
7
+
8
+ # testing
9
+ /coverage
10
+
11
+ # next.js
12
+ /.next/
13
+ /out/
14
+
15
+ # production
16
+ /build
17
+
18
+ # misc
19
+ .DS_Store
20
+ *.pem
21
+
22
+ # debug
23
+ npm-debug.log*
24
+ yarn-debug.log*
25
+ yarn-error.log*
26
+
27
+ # local env files
28
+ .env*.local
29
+
30
+ # vercel
31
+ .vercel
32
+
33
+ # typescript
34
+ *.tsbuildinfo
35
+ next-env.d.ts
.nvmrc ADDED
@@ -0,0 +1 @@
 
 
1
+ v18.16.0
Dockerfile ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM node:18-alpine AS base
2
+
3
+ # Install dependencies only when needed
4
+ FROM base AS deps
5
+ # Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
6
+ RUN apk add --no-cache libc6-compat
7
+ WORKDIR /app
8
+
9
+ # Install dependencies based on the preferred package manager
10
+ COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
11
+ RUN \
12
+ if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
13
+ elif [ -f package-lock.json ]; then npm ci; \
14
+ elif [ -f pnpm-lock.yaml ]; then yarn global add pnpm && pnpm i --frozen-lockfile; \
15
+ else echo "Lockfile not found." && exit 1; \
16
+ fi
17
+
18
+ # Uncomment the following lines if you want to use a secret at buildtime,
19
+ # for example to access your private npm packages
20
+ # RUN --mount=type=secret,id=HF_EXAMPLE_SECRET,mode=0444,required=true \
21
+ # $(cat /run/secrets/HF_EXAMPLE_SECRET)
22
+
23
+ # Rebuild the source code only when needed
24
+ FROM base AS builder
25
+ WORKDIR /app
26
+ COPY --from=deps /app/node_modules ./node_modules
27
+ COPY . .
28
+
29
+ # Next.js collects completely anonymous telemetry data about general usage.
30
+ # Learn more here: https://nextjs.org/telemetry
31
+ # Uncomment the following line in case you want to disable telemetry during the build.
32
+ # ENV NEXT_TELEMETRY_DISABLED 1
33
+
34
+ # RUN yarn build
35
+
36
+ # If you use yarn, comment out this line and use the line above
37
+ RUN npm run build
38
+
39
+ # Production image, copy all the files and run next
40
+ FROM base AS runner
41
+ WORKDIR /app
42
+
43
+ ENV NODE_ENV production
44
+ # Uncomment the following line in case you want to disable telemetry during runtime.
45
+ # ENV NEXT_TELEMETRY_DISABLED 1
46
+
47
+ RUN addgroup --system --gid 1001 nodejs
48
+ RUN adduser --system --uid 1001 nextjs
49
+
50
+ COPY --from=builder /app/public ./public
51
+
52
+ # Automatically leverage output traces to reduce image size
53
+ # https://nextjs.org/docs/advanced-features/output-file-tracing
54
+ COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
55
+ COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
56
+ COPY --from=builder --chown=nextjs:nodejs /app/.next/cache ./.next/cache
57
+ # COPY --from=builder --chown=nextjs:nodejs /app/.next/cache/fetch-cache ./.next/cache/fetch-cache
58
+
59
+ USER nextjs
60
+
61
+ EXPOSE 3000
62
+
63
+ ENV PORT 3000
64
+
65
+ CMD ["node", "server.js"]
README.md ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: VideoChain-UI
3
+ emoji: 🎬🔗
4
+ colorFrom: '#1a1b1c'
5
+ colorTo: '#acb1b5'
6
+ sdk: docker
7
+ pinned: false
8
+ app_port: 3000
9
+ duplicated_from: bmorphism/vibecraft
10
+ ---
11
+
12
+ This is the frontend interface to VideoChain-API, a server to generate videos using AI.
13
+
14
+ This space cannot be easily duplicated yet, as you will have to configure a lot of things
15
+ to make it work (you need the API, you need separate spaces for upscaling, interpolation etc)
16
+ ## Getting Started
17
+
18
+ First, run the development server:
19
+
20
+ ```bash
21
+ npm run dev
22
+ ```
23
+
24
+ Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
25
+
26
+ You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
27
+
28
+ This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
29
+
30
+ ## Things to know
31
+
32
+ Next will cache the API calls!
33
+ So be careful about this (this is why we invalidate them in the fetch() method)
34
+
35
+ ## Environment variable
36
+
37
+ ```
38
+ NEXT_PUBLIC_BASE_URL=http://localhost:3000
39
+ VC_VIDEOCHAIN_API_URL=http://localhost:7860
40
+ VC_SECRET_ACCESS_TOKEN=***SECRENT***
41
+ ```
components.json ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema.json",
3
+ "style": "default",
4
+ "rsc": true,
5
+ "tsx": true,
6
+ "tailwind": {
7
+ "config": "tailwind.config.js",
8
+ "css": "app/globals.css",
9
+ "baseColor": "zinc",
10
+ "cssVariables": false
11
+ },
12
+ "aliases": {
13
+ "components": "@/components",
14
+ "utils": "@/lib/utils"
15
+ }
16
+ }
next.config.js ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ /** @type {import('next').NextConfig} */
2
+ const nextConfig = {
3
+ output: 'standalone',
4
+
5
+ experimental: {
6
+ serverActions: true,
7
+ },
8
+ }
9
+
10
+ module.exports = nextConfig
package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
package.json ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "videochain-ui",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "next dev",
7
+ "build": "next build",
8
+ "start": "next start",
9
+ "lint": "next lint"
10
+ },
11
+ "dependencies": {
12
+ "@gradio/client": "^0.1.4",
13
+ "@radix-ui/react-accordion": "^1.1.2",
14
+ "@radix-ui/react-avatar": "^1.0.3",
15
+ "@radix-ui/react-checkbox": "^1.0.4",
16
+ "@radix-ui/react-collapsible": "^1.0.3",
17
+ "@radix-ui/react-dialog": "^1.0.4",
18
+ "@radix-ui/react-dropdown-menu": "^2.0.5",
19
+ "@radix-ui/react-icons": "^1.3.0",
20
+ "@radix-ui/react-menubar": "^1.0.3",
21
+ "@radix-ui/react-popover": "^1.0.6",
22
+ "@radix-ui/react-select": "^1.2.2",
23
+ "@radix-ui/react-separator": "^1.0.3",
24
+ "@radix-ui/react-slot": "^1.0.2",
25
+ "@silevis/reactgrid": "^4.0.5",
26
+ "@tanstack/react-query": "^4.30.0",
27
+ "@tanstack/react-table": "^8.9.3",
28
+ "@types/fluent-ffmpeg": "^2.1.21",
29
+ "@types/node": "20.4.2",
30
+ "@types/react": "18.2.15",
31
+ "@types/react-dom": "18.2.7",
32
+ "@types/uuid": "^9.0.2",
33
+ "autoprefixer": "10.4.14",
34
+ "class-variance-authority": "^0.6.1",
35
+ "clsx": "^2.0.0",
36
+ "cmdk": "^0.2.0",
37
+ "cookies-next": "^2.1.2",
38
+ "date-fns": "^2.30.0",
39
+ "eslint": "8.45.0",
40
+ "eslint-config-next": "13.4.10",
41
+ "fluent-ffmpeg": "^2.1.2",
42
+ "fs-extra": "^11.1.1",
43
+ "lucide-react": "^0.260.0",
44
+ "next": "13.4.10",
45
+ "postcss": "8.4.26",
46
+ "puppeteer": "^20.8.2",
47
+ "react": "18.2.0",
48
+ "react-day-picker": "^8.8.0",
49
+ "react-dom": "18.2.0",
50
+ "tailwind-merge": "^1.13.2",
51
+ "tailwindcss": "3.3.3",
52
+ "tailwindcss-animate": "^1.0.6",
53
+ "temp-dir": "^3.0.0",
54
+ "ts-node": "^10.9.1",
55
+ "typescript": "5.1.6",
56
+ "uuid": "^9.0.0",
57
+ "zod": "^3.21.4",
58
+ "zustand": "^4.3.9"
59
+ }
60
+ }
postcss.config.js ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ module.exports = {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {},
5
+ },
6
+ }
public/next.svg ADDED
public/vercel.svg ADDED
scripts/test.js ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const { promises: fs } = require("node:fs")
2
+
3
+ const main = async () => {
4
+ console.log('generating shot..')
5
+ const response = await fetch("http://localhost:3000/api/shot", {
6
+ method: "POST",
7
+ headers: {
8
+ "Accept": "application/json",
9
+ "Content-Type": "application/json"
10
+ },
11
+ body: JSON.stringify({
12
+ token: process.env.VC_SECRET_ACCESS_TOKEN,
13
+ shotPrompt: "video of a dancing cat"
14
+ })
15
+ });
16
+
17
+ console.log('response:', response)
18
+ const buffer = await response.buffer()
19
+
20
+ fs.writeFile(`./test-juju.mp4`, buffer)
21
+ }
22
+
23
+ main()
src/app/data/data.ts ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {
2
+ ArrowDownIcon,
3
+ ArrowRightIcon,
4
+ ArrowUpIcon,
5
+ CheckCircledIcon,
6
+ CircleIcon,
7
+ CrossCircledIcon,
8
+ QuestionMarkCircledIcon,
9
+ StopwatchIcon,
10
+ } from "@radix-ui/react-icons"
11
+
12
+ export const labels = [
13
+ {
14
+ value: "bug",
15
+ label: "Bug",
16
+ },
17
+ {
18
+ value: "feature",
19
+ label: "Feature",
20
+ },
21
+ {
22
+ value: "documentation",
23
+ label: "Documentation",
24
+ },
25
+ ]
26
+
27
+ export const statuses = [
28
+ {
29
+ value: "backlog",
30
+ label: "Backlog",
31
+ icon: QuestionMarkCircledIcon,
32
+ },
33
+ {
34
+ value: "todo",
35
+ label: "Todo",
36
+ icon: CircleIcon,
37
+ },
38
+ {
39
+ value: "in progress",
40
+ label: "In Progress",
41
+ icon: StopwatchIcon,
42
+ },
43
+ {
44
+ value: "done",
45
+ label: "Done",
46
+ icon: CheckCircledIcon,
47
+ },
48
+ {
49
+ value: "canceled",
50
+ label: "Canceled",
51
+ icon: CrossCircledIcon,
52
+ },
53
+ ]
54
+
55
+ export const priorities = [
56
+ {
57
+ label: "Low",
58
+ value: "low",
59
+ icon: ArrowDownIcon,
60
+ },
61
+ {
62
+ label: "Medium",
63
+ value: "medium",
64
+ icon: ArrowRightIcon,
65
+ },
66
+ {
67
+ label: "High",
68
+ value: "high",
69
+ icon: ArrowUpIcon,
70
+ },
71
+ ]
src/app/data/mock.json ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "id": "TASK-8782",
4
+ "title": "You can't compress the program without quantifying the open-source SSD pixel!",
5
+ "status": "in progress",
6
+ "label": "documentation",
7
+ "priority": "medium"
8
+ },
9
+ {
10
+ "id": "TASK-7878",
11
+ "title": "Try to calculate the EXE feed, maybe it will index the multi-byte pixel!",
12
+ "status": "backlog",
13
+ "label": "documentation",
14
+ "priority": "medium"
15
+ }
16
+ ]
src/app/data/schema.ts ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { z } from "zod"
2
+
3
+ // We're keeping a simple non-relational schema here.
4
+ // IRL, you will have a schema for your data models.
5
+ export const taskSchema = z.object({
6
+ id: z.string(),
7
+ title: z.string(),
8
+ status: z.string(),
9
+ label: z.string(),
10
+ priority: z.string(),
11
+ })
12
+
13
+ export type Task = z.infer<typeof taskSchema>
src/app/favicon.ico ADDED
src/app/forlater/page.tsx ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import Head from "next/head"
2
+
3
+ import { Timeline } from "@/components/business/timeline"
4
+
5
+ export default function Index() {
6
+ return (
7
+ <div>
8
+ <Head>
9
+ <meta name="viewport" content="width=device-width, initial-scale=0.86, maximum-scale=5.0, minimum-scale=0.86" />
10
+ </Head>
11
+ <main className="h-screen w-full flex bg-gray-700 text-gray-200">
12
+ <div className="flex flex-col">
13
+ <Timeline />
14
+ </div>
15
+ </main>
16
+ </div>
17
+ )
18
+ }
src/app/globals.css ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ :root {
6
+ --foreground-rgb: 0, 0, 0;
7
+ --background-start-rgb: 214, 219, 220;
8
+ --background-end-rgb: 255, 255, 255;
9
+ }
10
+
11
+ @media (prefers-color-scheme: dark) {
12
+ :root {
13
+ --foreground-rgb: 255, 255, 255;
14
+ --background-start-rgb: 0, 0, 0;
15
+ --background-end-rgb: 0, 0, 0;
16
+ }
17
+ }
18
+
19
+ body {
20
+ color: rgb(var(--foreground-rgb));
21
+ background: linear-gradient(
22
+ to bottom,
23
+ transparent,
24
+ rgb(var(--background-end-rgb))
25
+ )
26
+ rgb(var(--background-start-rgb));
27
+ }
src/app/layout.tsx ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import './globals.css'
2
+ import type { Metadata } from 'next'
3
+ import { Inter } from 'next/font/google'
4
+
5
+ const inter = Inter({ subsets: ['latin'] })
6
+
7
+ export const metadata: Metadata = {
8
+ title: 'VideoChain UI',
9
+ description: 'Generate AI videos using this Hugging Face Space!',
10
+ }
11
+
12
+ export default function RootLayout({
13
+ children,
14
+ }: {
15
+ children: React.ReactNode
16
+ }) {
17
+ return (
18
+ <html lang="en">
19
+ <body className={inter.className}>
20
+ {children}
21
+ </body>
22
+ </html>
23
+ )
24
+ }
src/app/page.tsx ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import { useEffect, useState } from "react"
4
+ import { v4 as uuidv4 } from "uuid"
5
+
6
+ const key = "VideoChain-UI-Owner-ID"
7
+
8
+ export default function Index() {
9
+ const [uuid, setUuid] = useState<string>()
10
+
11
+ useEffect(() => {
12
+ if (uuid) {
13
+ window.location.href = `/studio/${uuid}`
14
+ } else {
15
+ const existingUuid = `${localStorage.getItem(key) || ""}`
16
+ if (existingUuid?.length > 10) {
17
+ setUuid(existingUuid)
18
+ } else {
19
+ const newUuid = uuidv4()
20
+ setUuid(newUuid)
21
+ localStorage.setItem(key, newUuid)
22
+ }
23
+ }
24
+ }, [uuid])
25
+
26
+ return <div>Loading..</div>
27
+ }
src/app/studio/[ownerId]/main.tsx ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import { useState } from "react"
4
+
5
+ import { VideosQueue } from "@/components/business/videos/video-table"
6
+ import { RefreshStudio } from "@/components/business/refresh"
7
+ import { VideoForm } from "@/components/business/video-form"
8
+ import { Video } from "@/app/types"
9
+ import { VideoPlayer } from "@/components/business/video-player"
10
+
11
+ export default function Main({ videos }: { videos: Video[] }) {
12
+ const [selectedVideo, selectVideo] = useState<Video>()
13
+
14
+ return (
15
+ <div className="flex flex-col md:flex-row w-full">
16
+ <div className="h-full flex flex-col space-y-4 w-full md:w-1/2 px-4 py-8">
17
+ <VideoForm />
18
+ <VideosQueue videos={videos} onSelectVideo={selectVideo} />
19
+ <RefreshStudio />
20
+ </div>
21
+ <div className="flex flex-col w-1/2">
22
+ <VideoPlayer video={selectedVideo} />
23
+ </div>
24
+ </div>
25
+ )
26
+ }
src/app/studio/[ownerId]/page.tsx ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use server"
2
+
3
+ import Head from "next/head"
4
+
5
+ import { getVideos } from "@/server"
6
+
7
+ import Main from "./main"
8
+
9
+ export default async function StudioPage({ params: { ownerId } }: { params: { ownerId: string }}) {
10
+ const videos = await getVideos(ownerId)
11
+
12
+ return (
13
+ <div>
14
+ <Head>
15
+ <meta name="viewport" content="width=device-width, initial-scale=0.86, maximum-scale=5.0, minimum-scale=0.86" />
16
+ </Head>
17
+ <main className="dark fixed inset-0 flex flex-col items-center bg-stone-900 text-stone-10 overflow-y-scroll">
18
+ <Main videos={videos} />
19
+ </main>
20
+ </div>
21
+ )
22
+ }
src/app/types.ts ADDED
@@ -0,0 +1,258 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export type VideoStatus =
2
+ | 'pending'
3
+ | 'abort' // this is an order (the video might still being processed by a task)
4
+ | 'delete' // this is an order (the video might still being processed by a task)
5
+ | 'pause' // this is an order (the video might still being processed by a task)
6
+ | 'completed'
7
+ | 'unknown'
8
+
9
+
10
+ export type VideoTransition =
11
+ | 'dissolve'
12
+ | 'bookflip'
13
+ | 'bounce'
14
+ | 'bowtiehorizontal'
15
+ | 'bowtievertical'
16
+ | 'bowtiewithparameter'
17
+ | 'butterflywavescrawler'
18
+ | 'circlecrop'
19
+ | 'colourdistance'
20
+ | 'crazyparametricfun'
21
+ | 'crosszoom'
22
+ | 'directional'
23
+ | 'directionalscaled'
24
+ | 'doomscreentransition'
25
+ | 'dreamy'
26
+ | 'dreamyzoom'
27
+ | 'edgetransition'
28
+ | 'filmburn'
29
+ | 'filmburnglitchdisplace'
30
+ | 'glitchmemories'
31
+ | 'gridflip'
32
+ | 'horizontalclose'
33
+ | 'horizontalopen'
34
+ | 'invertedpagecurl'
35
+ | 'leftright'
36
+ | 'linearblur'
37
+ | 'mosaic'
38
+ | 'overexposure'
39
+ | 'polkadotscurtain'
40
+ | 'radial'
41
+ | 'rectangle'
42
+ | 'rectanglecrop'
43
+ | 'rolls'
44
+ | 'rotatescalevanish'
45
+ | 'simplezoom'
46
+ | 'simplezoomout'
47
+ | 'slides'
48
+ | 'staticfade'
49
+ | 'stereoviewer'
50
+ | 'swirl'
51
+ | 'tvstatic'
52
+ | 'topbottom'
53
+ | 'verticalclose'
54
+ | 'verticalopen'
55
+ | 'waterdrop'
56
+ | 'waterdropzoomincircles'
57
+ | 'zoomleftwipe'
58
+ | 'zoomrigthwipe'
59
+ | 'angular'
60
+ | 'burn'
61
+ | 'cannabisleaf'
62
+ | 'circle'
63
+ | 'circleopen'
64
+ | 'colorphase'
65
+ | 'coordfromin'
66
+ | 'crosshatch'
67
+ | 'crosswarp'
68
+ | 'cube'
69
+ | 'directionaleasing'
70
+ | 'directionalwarp'
71
+ | 'directionalwipe'
72
+ | 'displacement'
73
+ | 'doorway'
74
+ | 'fade'
75
+ | 'fadecolor'
76
+ | 'fadegrayscale'
77
+ | 'flyeye'
78
+ | 'heart'
79
+ | 'hexagonalize'
80
+ | 'kaleidoscope'
81
+ | 'luma'
82
+ | 'luminance_melt'
83
+ | 'morph'
84
+ | 'mosaic_transition'
85
+ | 'multiply_blend'
86
+ | 'perlin'
87
+ | 'pinwheel'
88
+ | 'pixelize'
89
+ | 'polar_function'
90
+ | 'powerkaleido'
91
+ | 'randomnoisex'
92
+ | 'randomsquares'
93
+ | 'ripple'
94
+ | 'rotatetransition'
95
+ | 'rotate_scale_fade'
96
+ | 'scalein'
97
+ | 'squareswire'
98
+ | 'squeeze'
99
+ | 'static_wipe'
100
+ | 'swap'
101
+ | 'tangentmotionblur'
102
+ | 'undulatingburnout'
103
+ | 'wind'
104
+ | 'windowblinds'
105
+ | 'windowslice'
106
+ | 'wipedown'
107
+ | 'wipeleft'
108
+ | 'wiperight'
109
+ | 'wipeup'
110
+ | 'x_axistranslation'
111
+
112
+
113
+ export interface VideoShotMeta {
114
+ shotPrompt: string
115
+ // inputVideo?: string
116
+
117
+ // describe the background audio (crowd, birds, wind, sea etc..)
118
+ backgroundAudioPrompt: string
119
+
120
+ // describe the foreground audio (cars revving, footsteps, objects breaking, explosion etc)
121
+ foregroundAudioPrompt: string
122
+
123
+ // describe the main actor visible in the shot (optional)
124
+ actorPrompt: string
125
+
126
+ // describe the main actor voice (man, woman, old, young, amused, annoyed.. etc)
127
+ actorVoicePrompt: string
128
+
129
+ // describe the main actor dialogue line
130
+ actorDialoguePrompt: string
131
+
132
+ seed: number
133
+ noise: boolean // add movie noise
134
+ noiseAmount: number // noise strength (default is 2, and 10 is very visible)
135
+
136
+ durationMs: number // in milliseconds
137
+ steps: number
138
+
139
+ fps: number // 8, 12, 24, 30, 60
140
+
141
+ resolution: string // {width}x{height} (256, 512, 576, 720, 1080)
142
+
143
+ introTransition: VideoTransition
144
+ introDurationMs: number // in milliseconds
145
+ }
146
+
147
+
148
+ export interface VideoShotData {
149
+ // must be unique
150
+ id: string
151
+ sequenceId: string
152
+ ownerId: string
153
+
154
+ fileName: string
155
+
156
+ // used to check compatibility
157
+ version: number
158
+
159
+ // for internal use
160
+ hasGeneratedPreview: boolean
161
+ hasGeneratedVideo: boolean
162
+ hasUpscaledVideo: boolean
163
+ hasGeneratedBackgroundAudio: boolean
164
+ hasGeneratedForegroundAudio: boolean
165
+ hasGeneratedActor: boolean
166
+ hasInterpolatedVideo: boolean
167
+ hasAddedAudio: boolean
168
+ hasPostProcessedVideo: boolean
169
+ nbCompletedSteps: number
170
+ nbTotalSteps: number
171
+ progressPercent: number
172
+ createdAt: string
173
+ completedAt: string
174
+ completed: boolean
175
+ error: string
176
+ }
177
+
178
+ export type VideoShot = VideoShotMeta & VideoShotData
179
+
180
+ export interface VideoSequenceMeta {
181
+
182
+ // describe the whole movie
183
+ videoPrompt: string
184
+
185
+ // describe the background audio (crowd, birds, wind, sea etc..)
186
+ backgroundAudioPrompt: string
187
+
188
+ // describe the foreground audio (cars revving, footsteps, objects breaking, explosion etc)
189
+ foregroundAudioPrompt: string
190
+
191
+ // describe the main actor visible in the shot (optional)
192
+ actorPrompt: string
193
+
194
+ // describe the main actor voice (man, woman, old, young, amused, annoyed.. etc)
195
+ actorVoicePrompt: string
196
+
197
+ // describe the main actor dialogue line
198
+ actorDialoguePrompt: string
199
+
200
+ seed: number
201
+
202
+ noise: boolean // add movie noise
203
+ noiseAmount: number // noise strength (default is 2, and 10 is very visible)
204
+
205
+ steps: number // between 10 and 50
206
+
207
+ fps: number // 8, 12, 24, 30, 60
208
+
209
+ resolution: string // 256, 512, 576, 720, 1080
210
+
211
+ outroTransition: VideoTransition
212
+ outroDurationMs: number
213
+ }
214
+
215
+
216
+ export interface VideoSequenceData {
217
+ // must be unique
218
+ id: string
219
+
220
+ ownerId: string
221
+
222
+ fileName: string
223
+
224
+ // used to check compatibility
225
+ version: number
226
+
227
+ status: VideoStatus
228
+
229
+ hasGeneratedSpecs: boolean
230
+ hasAssembledVideo: boolean
231
+ nbCompletedShots: number
232
+ progressPercent: number
233
+ createdAt: string
234
+ completedAt: string
235
+ completed: boolean
236
+ error: string
237
+ }
238
+
239
+ export type VideoSequence = VideoSequenceMeta & VideoSequenceData
240
+
241
+ export type VideoStatusRequest = {
242
+ status: VideoStatus
243
+ }
244
+
245
+ export type GenericAPIResponse = {
246
+ success?: boolean
247
+ error?: string
248
+ }
249
+
250
+ export type VideoAPIRequest = Partial<{
251
+ prompt: string
252
+ sequence: Partial<VideoSequenceMeta>
253
+ shots: Array<Partial<VideoShotMeta>>
254
+ }>
255
+
256
+ export type Video = VideoSequence & {
257
+ shots: VideoShot[]
258
+ }
src/components/business/menu.tsx ADDED
@@ -0,0 +1,200 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {
2
+ Menubar,
3
+ MenubarCheckboxItem,
4
+ MenubarContent,
5
+ MenubarItem,
6
+ MenubarLabel,
7
+ MenubarMenu,
8
+ MenubarRadioGroup,
9
+ MenubarRadioItem,
10
+ MenubarSeparator,
11
+ MenubarShortcut,
12
+ MenubarSub,
13
+ MenubarSubContent,
14
+ MenubarSubTrigger,
15
+ MenubarTrigger,
16
+ } from "@/components/ui/menubar"
17
+
18
+ export function Menu() {
19
+ return (
20
+ <Menubar className="rounded-none border-b border-none px-2 lg:px-4">
21
+ <MenubarMenu>
22
+ <MenubarTrigger className="font-bold">Music</MenubarTrigger>
23
+ <MenubarContent>
24
+ <MenubarItem>About Music</MenubarItem>
25
+ <MenubarSeparator />
26
+ <MenubarItem>
27
+ Preferences... <MenubarShortcut>⌘,</MenubarShortcut>
28
+ </MenubarItem>
29
+ <MenubarSeparator />
30
+ <MenubarItem>
31
+ Hide Music... <MenubarShortcut>⌘H</MenubarShortcut>
32
+ </MenubarItem>
33
+ <MenubarItem>
34
+ Hide Others... <MenubarShortcut>⇧⌘H</MenubarShortcut>
35
+ </MenubarItem>
36
+ <MenubarShortcut />
37
+ <MenubarItem>
38
+ Quit Music <MenubarShortcut>⌘Q</MenubarShortcut>
39
+ </MenubarItem>
40
+ </MenubarContent>
41
+ </MenubarMenu>
42
+ <MenubarMenu>
43
+ <MenubarTrigger className="relative">File</MenubarTrigger>
44
+ <MenubarContent>
45
+ <MenubarSub>
46
+ <MenubarSubTrigger>New</MenubarSubTrigger>
47
+ <MenubarSubContent className="w-[230px]">
48
+ <MenubarItem>
49
+ Playlist <MenubarShortcut>⌘N</MenubarShortcut>
50
+ </MenubarItem>
51
+ <MenubarItem disabled>
52
+ Playlist from Selection <MenubarShortcut>⇧⌘N</MenubarShortcut>
53
+ </MenubarItem>
54
+ <MenubarItem>
55
+ Smart Playlist... <MenubarShortcut>⌥⌘N</MenubarShortcut>
56
+ </MenubarItem>
57
+ <MenubarItem>Playlist Folder</MenubarItem>
58
+ <MenubarItem disabled>Genius Playlist</MenubarItem>
59
+ </MenubarSubContent>
60
+ </MenubarSub>
61
+ <MenubarItem>
62
+ Open Stream URL... <MenubarShortcut>⌘U</MenubarShortcut>
63
+ </MenubarItem>
64
+ <MenubarItem>
65
+ Close Window <MenubarShortcut>⌘W</MenubarShortcut>
66
+ </MenubarItem>
67
+ <MenubarSeparator />
68
+ <MenubarSub>
69
+ <MenubarSubTrigger>Library</MenubarSubTrigger>
70
+ <MenubarSubContent>
71
+ <MenubarItem>Update Cloud Library</MenubarItem>
72
+ <MenubarItem>Update Genius</MenubarItem>
73
+ <MenubarSeparator />
74
+ <MenubarItem>Organize Library...</MenubarItem>
75
+ <MenubarItem>Export Library...</MenubarItem>
76
+ <MenubarSeparator />
77
+ <MenubarItem>Import Playlist...</MenubarItem>
78
+ <MenubarItem disabled>Export Playlist...</MenubarItem>
79
+ <MenubarItem>Show Duplicate Items</MenubarItem>
80
+ <MenubarSeparator />
81
+ <MenubarItem>Get Album Artwork</MenubarItem>
82
+ <MenubarItem disabled>Get Track Names</MenubarItem>
83
+ </MenubarSubContent>
84
+ </MenubarSub>
85
+ <MenubarItem>
86
+ Import... <MenubarShortcut>⌘O</MenubarShortcut>
87
+ </MenubarItem>
88
+ <MenubarItem disabled>Burn Playlist to Disc...</MenubarItem>
89
+ <MenubarSeparator />
90
+ <MenubarItem>
91
+ Show in Finder <MenubarShortcut>⇧⌘R</MenubarShortcut>{" "}
92
+ </MenubarItem>
93
+ <MenubarItem>Convert</MenubarItem>
94
+ <MenubarSeparator />
95
+ <MenubarItem>Page Setup...</MenubarItem>
96
+ <MenubarItem disabled>
97
+ Print... <MenubarShortcut>⌘P</MenubarShortcut>
98
+ </MenubarItem>
99
+ </MenubarContent>
100
+ </MenubarMenu>
101
+ <MenubarMenu>
102
+ <MenubarTrigger>Edit</MenubarTrigger>
103
+ <MenubarContent>
104
+ <MenubarItem disabled>
105
+ Undo <MenubarShortcut>⌘Z</MenubarShortcut>
106
+ </MenubarItem>
107
+ <MenubarItem disabled>
108
+ Redo <MenubarShortcut>⇧⌘Z</MenubarShortcut>
109
+ </MenubarItem>
110
+ <MenubarSeparator />
111
+ <MenubarItem disabled>
112
+ Cut <MenubarShortcut>⌘X</MenubarShortcut>
113
+ </MenubarItem>
114
+ <MenubarItem disabled>
115
+ Copy <MenubarShortcut>⌘C</MenubarShortcut>
116
+ </MenubarItem>
117
+ <MenubarItem disabled>
118
+ Paste <MenubarShortcut>⌘V</MenubarShortcut>
119
+ </MenubarItem>
120
+ <MenubarSeparator />
121
+ <MenubarItem>
122
+ Select All <MenubarShortcut>⌘A</MenubarShortcut>
123
+ </MenubarItem>
124
+ <MenubarItem disabled>
125
+ Deselect All <MenubarShortcut>⇧⌘A</MenubarShortcut>
126
+ </MenubarItem>
127
+ <MenubarSeparator />
128
+ <MenubarItem>
129
+ Smart Dictation...{" "}
130
+ <MenubarShortcut>
131
+ <svg
132
+ xmlns="http://www.w3.org/2000/svg"
133
+ fill="none"
134
+ stroke="currentColor"
135
+ strokeLinecap="round"
136
+ strokeLinejoin="round"
137
+ strokeWidth="2"
138
+ className="h-4 w-4"
139
+ viewBox="0 0 24 24"
140
+ >
141
+ <path d="m12 8-9.04 9.06a2.82 2.82 0 1 0 3.98 3.98L16 12" />
142
+ <circle cx="17" cy="7" r="5" />
143
+ </svg>
144
+ </MenubarShortcut>
145
+ </MenubarItem>
146
+ <MenubarItem>
147
+ Emoji & Symbols{" "}
148
+ <MenubarShortcut>
149
+ <svg
150
+ xmlns="http://www.w3.org/2000/svg"
151
+ fill="none"
152
+ stroke="currentColor"
153
+ strokeLinecap="round"
154
+ strokeLinejoin="round"
155
+ strokeWidth="2"
156
+ className="h-4 w-4"
157
+ viewBox="0 0 24 24"
158
+ >
159
+ <circle cx="12" cy="12" r="10" />
160
+ <path d="M2 12h20M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" />
161
+ </svg>
162
+ </MenubarShortcut>
163
+ </MenubarItem>
164
+ </MenubarContent>
165
+ </MenubarMenu>
166
+ <MenubarMenu>
167
+ <MenubarTrigger>View</MenubarTrigger>
168
+ <MenubarContent>
169
+ <MenubarCheckboxItem>Show Playing Next</MenubarCheckboxItem>
170
+ <MenubarCheckboxItem checked>Show Lyrics</MenubarCheckboxItem>
171
+ <MenubarSeparator />
172
+ <MenubarItem inset disabled>
173
+ Show Status Bar
174
+ </MenubarItem>
175
+ <MenubarSeparator />
176
+ <MenubarItem inset>Hide Sidebar</MenubarItem>
177
+ <MenubarItem disabled inset>
178
+ Enter Full Screen
179
+ </MenubarItem>
180
+ </MenubarContent>
181
+ </MenubarMenu>
182
+ <MenubarMenu>
183
+ <MenubarTrigger className="hidden md:block">Account</MenubarTrigger>
184
+ <MenubarContent forceMount>
185
+ <MenubarLabel inset>Switch Account</MenubarLabel>
186
+ <MenubarSeparator />
187
+ <MenubarRadioGroup value="benoit">
188
+ <MenubarRadioItem value="andy">Andy</MenubarRadioItem>
189
+ <MenubarRadioItem value="benoit">Benoit</MenubarRadioItem>
190
+ <MenubarRadioItem value="Luis">Luis</MenubarRadioItem>
191
+ </MenubarRadioGroup>
192
+ <MenubarSeparator />
193
+ <MenubarItem inset>Manage Famliy...</MenubarItem>
194
+ <MenubarSeparator />
195
+ <MenubarItem inset>Add Account...</MenubarItem>
196
+ </MenubarContent>
197
+ </MenubarMenu>
198
+ </Menubar>
199
+ )
200
+ }
src/components/business/refresh.tsx ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import { useEffect, useTransition } from "react"
4
+ import { usePathname } from "next/navigation"
5
+
6
+ import { refreshStudio } from "@/server/actions"
7
+
8
+ export function RefreshStudio() {
9
+ const pathname = usePathname()
10
+ const [isPending, startTransition] = useTransition()
11
+
12
+ useEffect(() => {
13
+ const slug = `${pathname.split("/").pop()}`
14
+ setInterval(() => {
15
+ startTransition(() => {
16
+ try {
17
+ refreshStudio(slug)
18
+ } catch (err) {
19
+ // ignoring
20
+ }
21
+ })
22
+ }, 2000)
23
+ }, [pathname])
24
+
25
+ // TODO we could display a spinner here
26
+ return <></>
27
+ }
src/components/business/timeline/index.tsx ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import { ReactGrid, Column, Row } from "@silevis/reactgrid"
4
+ import "@silevis/reactgrid/styles.css"
5
+ import { useState } from "react"
6
+
7
+ type RowData = Record<string, string>
8
+
9
+ const nbColumns = 20
10
+
11
+ const getRowsData = (nbLayers: number, nbShots: number): RowData[] => [
12
+ { name: "Thomas", surname: "Goldman" },
13
+ { name: "Susie", surname: "Quattro" },
14
+ { name: "", surname: "" }
15
+ ];
16
+
17
+ const getColumns = (nbColumns: number): Column[] => {
18
+
19
+ const columns: Column[] = []
20
+ for (let i = 0; i < nbColumns; i++) {
21
+ columns.push({
22
+ columnId: `Shot ${i}`,
23
+ width: 150,
24
+ })
25
+ }
26
+
27
+ return columns
28
+ }
29
+
30
+
31
+
32
+ const getRows = (nbShots: number, rows: RowData[]): Row[] => [
33
+ {
34
+ rowId: 'header',
35
+ cells: [...Array(nbShots)].map((_, i) => ({
36
+ type: "text",
37
+ text: `Shot ${i}`,
38
+ })),
39
+ },
40
+ ...rows.map<Row>((row, idx) => ({
41
+ rowId: idx,
42
+ cells: Object.entries(row).map(([_, value]) => ({
43
+ type: "text",
44
+ text: value
45
+ }))
46
+ }))
47
+ ]
48
+
49
+ export function Timeline() {
50
+
51
+ const nbLayers = 8
52
+ const nbShots = 30
53
+
54
+ const [rowsData] = useState<RowData[]>(getRowsData(nbLayers, nbShots))
55
+
56
+ const rows = getRows(nbShots, rowsData)
57
+ const columns = getColumns(nbShots)
58
+
59
+ return (
60
+ <ReactGrid
61
+ rows={rows}
62
+ columns={columns}
63
+ onCellsChanged={(changes) => {
64
+ const change = changes[0]
65
+ const { columnId, newCell, previousCell, rowId, type } = change
66
+
67
+ console.log('change:', { columnId, newCell, previousCell, rowId, type })
68
+ }}
69
+ />
70
+ )
71
+ }
src/components/business/video-form.tsx ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import { useEffect, useTransition } from "react"
4
+ import { usePathname } from "next/navigation"
5
+
6
+ import { experimental_useFormStatus as useFormStatus } from "react-dom"
7
+ import { Textarea } from "@/components/ui/textarea"
8
+ import { Button } from "@/components/ui/button"
9
+ import { handleFormSubmit } from "@/server/actions"
10
+
11
+ export const VideoForm = () => {
12
+ const pathname = usePathname()
13
+ const ownerId = pathname.split("/").pop()
14
+ const { pending } = useFormStatus()
15
+
16
+ return (
17
+ <form
18
+ action={handleFormSubmit}
19
+ >
20
+ <div className="flex flex-row w-full mb-3">
21
+ <div className="flex flex-col w-1/2 text-center">
22
+ <h2 className="text-4xl font-thin tracking-tight">VideoChain 🎬</h2>
23
+ <p className="text-md font-thin">
24
+ Powered by <span className="font-normal">Hugging Face 🤗</span>
25
+ </p>
26
+ </div>
27
+ <div className="flex flex-col w-1/2 text-center">
28
+ <p className="text-sl font-thin">
29
+ For demonstration purposes only. Please use responsibly, and cancel any video you don&apos;t need anymore.
30
+ You have been assigned this permalink ID: <a href={`/studio/${ownerId}`} className="font-normal" target="_blank">{ownerId}</a>
31
+
32
+ </p>
33
+ </div>
34
+ </div>
35
+
36
+ <div className="flex items-center justify-between md:space-x-3 w-full">
37
+ <input
38
+ type="hidden"
39
+ id="ownerId"
40
+ name="ownerId"
41
+ value={ownerId}
42
+ />
43
+
44
+ <Textarea
45
+ id="prompt"
46
+ name="prompt"
47
+ placeholder="3 clips of a cat playing the piano"
48
+ className="mr-3 md:mr-0"
49
+ />
50
+
51
+ <Button
52
+ variant="secondary"
53
+ size="lg"
54
+ className="text-md md:w-32"
55
+ type="submit"
56
+ disabled={pending}
57
+ >
58
+ {pending ? "Loading.." : "Generate"}
59
+ </Button>
60
+ </div>
61
+ </form>
62
+ )
63
+ }
src/components/business/video-player.tsx ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import { Video } from "@/app/types"
4
+
5
+ export const VideoPlayer = ({ video }: { video?: Video }) => {
6
+
7
+ if (typeof video === "undefined") {
8
+ return <div className="flex w-full h-screen items-center justify-center text-center">
9
+ <div>No video to display</div>
10
+ </div>
11
+ }
12
+
13
+ return (
14
+ <div className="w-full py-8 px-2">
15
+ <video
16
+ src={`${
17
+ process.env.NEXT_PUBLIC_DOWNLOAD_URL
18
+ }/${
19
+ video.ownerId
20
+ }/${
21
+ video.id
22
+ }.mp4?progress=${
23
+ video.progressPercent
24
+ }`}
25
+ muted
26
+ autoPlay
27
+ loop
28
+ controls
29
+ className="w-full rounded-md overflow-hidden"
30
+ />
31
+ </div>
32
+ )
33
+ }
src/components/business/videos/change-status-button.tsx ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import { ReactNode, useTransition } from "react"
4
+
5
+ import { Video, VideoStatus } from "@/app/types"
6
+ import { setVideoStatus } from "@/server"
7
+
8
+ export function ChangeStatusButton({ video, children, status }: { video: Video; children: ReactNode; status: VideoStatus }) {
9
+ let [isPending, startTransition] = useTransition()
10
+
11
+ return (
12
+ <div
13
+ className="hover:underline cursor-pointer"
14
+ onClick={() => {
15
+ startTransition(async () => {
16
+ await setVideoStatus(video.ownerId, video.id, status)
17
+ })
18
+ }}>{children}</div>
19
+ )
20
+ }
src/components/business/videos/column-header.tsx ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {
2
+ ArrowDownIcon,
3
+ ArrowUpIcon,
4
+ CaretSortIcon,
5
+ EyeNoneIcon,
6
+ } from "@radix-ui/react-icons"
7
+ import { Column } from "@tanstack/react-table"
8
+
9
+ import { cn } from "@/lib/utils"
10
+ import { Button } from "@/components/ui/button"
11
+ import {
12
+ DropdownMenu,
13
+ DropdownMenuContent,
14
+ DropdownMenuItem,
15
+ DropdownMenuSeparator,
16
+ DropdownMenuTrigger,
17
+ } from "@/components/ui/dropdown-menu"
18
+
19
+ interface DataTableColumnHeaderProps<TData, TValue>
20
+ extends React.HTMLAttributes<HTMLDivElement> {
21
+ column: Column<TData, TValue>
22
+ title: string
23
+ }
24
+
25
+ export function DataTableColumnHeader<TData, TValue>({
26
+ column,
27
+ title,
28
+ className,
29
+ }: DataTableColumnHeaderProps<TData, TValue>) {
30
+ if (!column.getCanSort()) {
31
+ return <div className={cn(className)}>{title}</div>
32
+ }
33
+
34
+ return (
35
+ <div className={cn("flex items-center space-x-2", className)}>
36
+ <DropdownMenu>
37
+ <DropdownMenuTrigger asChild>
38
+ <Button
39
+ variant="ghost"
40
+ size="sm"
41
+ className="-ml-3 h-8 data-[state=open]:bg-accent"
42
+ >
43
+ <span>{title}</span>
44
+ {column.getIsSorted() === "desc" ? (
45
+ <ArrowDownIcon className="ml-2 h-4 w-4" />
46
+ ) : column.getIsSorted() === "asc" ? (
47
+ <ArrowUpIcon className="ml-2 h-4 w-4" />
48
+ ) : (
49
+ <CaretSortIcon className="ml-2 h-4 w-4" />
50
+ )}
51
+ </Button>
52
+ </DropdownMenuTrigger>
53
+ <DropdownMenuContent align="start">
54
+ <DropdownMenuItem onClick={() => column.toggleSorting(false)}>
55
+ <ArrowUpIcon className="mr-2 h-3.5 w-3.5 text-muted-foreground/70" />
56
+ Asc
57
+ </DropdownMenuItem>
58
+ <DropdownMenuItem onClick={() => column.toggleSorting(true)}>
59
+ <ArrowDownIcon className="mr-2 h-3.5 w-3.5 text-muted-foreground/70" />
60
+ Desc
61
+ </DropdownMenuItem>
62
+ <DropdownMenuSeparator />
63
+ <DropdownMenuItem onClick={() => column.toggleVisibility(false)}>
64
+ <EyeNoneIcon className="mr-2 h-3.5 w-3.5 text-muted-foreground/70" />
65
+ Hide
66
+ </DropdownMenuItem>
67
+ </DropdownMenuContent>
68
+ </DropdownMenu>
69
+ </div>
70
+ )
71
+ }
src/components/business/videos/columns.tsx ADDED
@@ -0,0 +1,151 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import { ColumnDef } from "@tanstack/react-table"
4
+ import { Checkbox } from "@/components/ui/checkbox"
5
+
6
+ import { DataTableColumnHeader } from "./column-header"
7
+
8
+ import { Video } from "@/app/types"
9
+ import { triggerDownload } from "@/lib/triggerDownload"
10
+ import { ChangeStatusButton } from "./change-status-button"
11
+
12
+ export const columns: ColumnDef<Video>[] = [
13
+ /*
14
+ {
15
+ id: "select",
16
+ header: ({ table }) => (
17
+ <Checkbox
18
+ checked={table.getIsAllPageRowsSelected()}
19
+ onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
20
+ aria-label="Select all"
21
+ className="translate-y-[2px]"
22
+ />
23
+ ),
24
+ cell: ({ row }) => (
25
+ <Checkbox
26
+ checked={row.getIsSelected()}
27
+ onCheckedChange={(value) => row.toggleSelected(!!value)}
28
+ aria-label="Select video"
29
+ className="translate-y-[2px]"
30
+ />
31
+ ),
32
+ enableSorting: false,
33
+ enableHiding: false,
34
+ },
35
+ */
36
+ {
37
+ accessorKey: "id",
38
+ header: ({ column }) => null,
39
+ cell: ({ row }) => null,
40
+ enableSorting: false,
41
+ enableHiding: true,
42
+ },
43
+ {
44
+ accessorKey: "videoPrompt",
45
+ header: ({ column }) => (
46
+ <DataTableColumnHeader column={column} title="Prompt" />
47
+ ),
48
+ cell: ({ row: { original: { videoPrompt }} }) => (
49
+ <div className="flex space-x-2">
50
+ <span className="max-w-[500px] font-medium">{videoPrompt}</span>
51
+ </div>
52
+ ),
53
+ enableSorting: false,
54
+ },
55
+ {
56
+ accessorKey: "progressPercent",
57
+ header: ({ column }) => (
58
+ <DataTableColumnHeader column={column} title="Progress" />
59
+ ),
60
+ cell: ({ row: { original: { progressPercent, status }} }) => (
61
+ <div className="flex items-center"><span>{
62
+ status === "pending"
63
+ ? `${Number(progressPercent || 0)}%`
64
+ : status === "completed"
65
+ ? "done"
66
+ : status === "abort"
67
+ ? "N.A."
68
+ : status === "delete"
69
+ ? "N.A."
70
+ : status === "pause"
71
+ ? "paused"
72
+ : "N.A."
73
+ }</span></div>
74
+ ),
75
+ enableSorting: false,
76
+ },
77
+ {
78
+ id: "preview",
79
+ header: ({ column }) => null,// no header
80
+ cell: ({ row: { original: { ownerId, id, progressPercent } } }) => <div className="w-[100px]">
81
+ <video src={`${process.env.NEXT_PUBLIC_DOWNLOAD_URL}/${ownerId}/${id}.mp4?progress=${progressPercent || 0}`} muted />
82
+ </div>,
83
+ enableSorting: false,
84
+ enableHiding: false,
85
+ },
86
+ {
87
+ id: "save",
88
+ header: ({ column }) => null,
89
+ cell: ({ row: { original: { ownerId, id }} }) => <div className="">
90
+ <a
91
+ className="hover:underline cursor-pointer"
92
+ target="_blank"
93
+ href={`${process.env.NEXT_PUBLIC_DOWNLOAD_URL}/${ownerId}/${id}.mp4`}>Save</a>
94
+ </div>,
95
+ enableSorting: false,
96
+ enableHiding: false,
97
+ },
98
+ {
99
+ id: "scene",
100
+ header: ({ column }) => null,
101
+ cell: ({ row: { original } }) => {
102
+ const scene = JSON.stringify({
103
+ videoPrompt: original.videoPrompt,
104
+ backgroundAudioPrompt: original.backgroundAudioPrompt,
105
+ foregroundAudioPrompt: original.foregroundAudioPrompt,
106
+ shots: original.shots.map(shot => ({
107
+ shotPrompt: shot.shotPrompt,
108
+ backgroundAudioPrompt: shot.backgroundAudioPrompt,
109
+ foregroundAudioPrompt: shot.foregroundAudioPrompt,
110
+ actorPrompt: shot.actorPrompt,
111
+ actorVoicePrompt: shot.actorVoicePrompt,
112
+ actorDialoguePrompt: shot.actorDialoguePrompt,
113
+ }))
114
+ }, null, 2)
115
+ return (<div className="">
116
+ <a
117
+ className="hover:underline cursor-pointer"
118
+ target="_blank"
119
+ onClick={() => triggerDownload("scene.json", scene)}>Scene</a>
120
+ </div>
121
+ )
122
+ },
123
+ enableSorting: false,
124
+ enableHiding: false,
125
+ },
126
+ {
127
+ id: "status",
128
+ header: ({ column }) => null, // no header
129
+ cell: ({ row: { original, original: { status } } }) =>
130
+ status === "pending" ? <ChangeStatusButton video={original} status="pause">Pause</ChangeStatusButton> :
131
+ status === "pause" ? <ChangeStatusButton video={original} status="pending">Continue</ChangeStatusButton> :
132
+ null,
133
+ enableSorting: false,
134
+ enableHiding: false,
135
+ },
136
+
137
+ {
138
+ id: "delete",
139
+ header: ({ column }) => null, // no header
140
+ cell: ({ row: { original, original: { status } } }) =>
141
+ <ChangeStatusButton video={original} status="delete">Delete</ChangeStatusButton>,
142
+ enableSorting: false,
143
+ enableHiding: false,
144
+ },
145
+ /*
146
+ {
147
+ id: "actions",
148
+ cell: ({ row }) => <VideoActions row={row} />,
149
+ },
150
+ */
151
+ ]
src/components/business/videos/video-actions.tsx ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import { DotsHorizontalIcon } from "@radix-ui/react-icons"
4
+ import { Row } from "@tanstack/react-table"
5
+
6
+ import { Button } from "@/components/ui/button"
7
+ import {
8
+ DropdownMenu,
9
+ DropdownMenuContent,
10
+ DropdownMenuItem,
11
+ DropdownMenuSeparator,
12
+ DropdownMenuShortcut,
13
+ DropdownMenuTrigger,
14
+ } from "@/components/ui/dropdown-menu"
15
+
16
+ import { Video } from "@/app/types"
17
+
18
+ export function VideoActions({
19
+ row,
20
+ }: {
21
+ row: Row<Video>
22
+ }) {
23
+ const task = row.original
24
+
25
+ return (
26
+ <DropdownMenu>
27
+ <DropdownMenuTrigger asChild>
28
+ <Button
29
+ variant="ghost"
30
+ className="flex h-8 w-8 p-0 data-[state=open]:bg-muted"
31
+ >
32
+ <DotsHorizontalIcon className="h-4 w-4" />
33
+ <span className="sr-only">Open menu</span>
34
+ </Button>
35
+ </DropdownMenuTrigger>
36
+ <DropdownMenuContent align="end" className="w-[160px]">
37
+ {/*
38
+ <DropdownMenuItem>
39
+ Pause generation
40
+ <DropdownMenuShortcut>⌘⌫</DropdownMenuShortcut>
41
+ </DropdownMenuItem>
42
+ */}
43
+ <DropdownMenuItem>Download</DropdownMenuItem>
44
+ <DropdownMenuSeparator />
45
+ <DropdownMenuItem>
46
+ Delete
47
+ <DropdownMenuShortcut>⌘⌫</DropdownMenuShortcut>
48
+ </DropdownMenuItem>
49
+ </DropdownMenuContent>
50
+ </DropdownMenu>
51
+ )
52
+ }
src/components/business/videos/video-table.tsx ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import {
4
+ ColumnDef,
5
+ ColumnFiltersState,
6
+ SortingState,
7
+ VisibilityState,
8
+ flexRender,
9
+ getCoreRowModel,
10
+ getFacetedRowModel,
11
+ getFacetedUniqueValues,
12
+ getFilteredRowModel,
13
+ getPaginationRowModel,
14
+ getSortedRowModel,
15
+ useReactTable,
16
+ } from "@tanstack/react-table"
17
+
18
+ import {
19
+ Table,
20
+ TableBody,
21
+ TableCell,
22
+ TableHead,
23
+ TableHeader,
24
+ TableRow,
25
+ } from "@/components/ui/table"
26
+
27
+ import { columns } from "@/components/business/videos/columns"
28
+ import { Video } from "@/app/types"
29
+ import { useState } from "react"
30
+
31
+ export function VideosQueue({
32
+ videos = [],
33
+ onSelectVideo,
34
+ }: {
35
+ videos: Video[]
36
+ onSelectVideo: (task: Video) => void
37
+ }) {
38
+ const [rowSelection, setRowSelection] = useState({})
39
+ const [columnVisibility, setColumnVisibility] =
40
+ useState<VisibilityState>({})
41
+ const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>(
42
+ []
43
+ )
44
+ const [sorting, setSorting] = useState<SortingState>([])
45
+
46
+ const table = useReactTable({
47
+ data: videos,
48
+ columns: columns as ColumnDef<Video, any>[],
49
+ state: {
50
+ sorting,
51
+ columnVisibility,
52
+ rowSelection,
53
+ columnFilters,
54
+ },
55
+ enableRowSelection: true,
56
+ onRowSelectionChange: setRowSelection,
57
+ onSortingChange: setSorting,
58
+ onColumnFiltersChange: setColumnFilters,
59
+ onColumnVisibilityChange: setColumnVisibility,
60
+ getCoreRowModel: getCoreRowModel(),
61
+ getFilteredRowModel: getFilteredRowModel(),
62
+ getPaginationRowModel: getPaginationRowModel(),
63
+ getSortedRowModel: getSortedRowModel(),
64
+ getFacetedRowModel: getFacetedRowModel(),
65
+ getFacetedUniqueValues: getFacetedUniqueValues(),
66
+ })
67
+
68
+ return (
69
+ <div className="rounded-lg border">
70
+ <Table>
71
+ <TableHeader>
72
+ {table.getHeaderGroups().map((headerGroup) => (
73
+ <TableRow key={headerGroup.id}>
74
+ {headerGroup.headers.map((header) => {
75
+ return (
76
+ <TableHead key={header.id}>
77
+ {header.isPlaceholder
78
+ ? null
79
+ : flexRender(
80
+ header.column.columnDef.header,
81
+ header.getContext()
82
+ )}
83
+ </TableHead>
84
+ )
85
+ })}
86
+ </TableRow>
87
+ ))}
88
+ </TableHeader>
89
+ <TableBody>
90
+ {table.getRowModel().rows?.length ? (
91
+ table.getRowModel().rows.map((row) => (
92
+ <TableRow
93
+ key={row.original.id}
94
+ data-state={row.getIsSelected() && "selected"}
95
+ >
96
+ {row.getVisibleCells().map((cell) => (
97
+ <TableCell key={cell.id}
98
+ className="cursor-pointer"
99
+ onClick={() => {
100
+ const videoId = `${row.getValue("id") || ""}`
101
+ const video = videos.find(({ id }) => id === videoId)
102
+ if (video) {
103
+ onSelectVideo(video)
104
+ }
105
+ }}>
106
+ {flexRender(
107
+ cell.column.columnDef.cell,
108
+ cell.getContext()
109
+ )}
110
+ </TableCell>
111
+ ))}
112
+ </TableRow>
113
+ ))
114
+ ) : (
115
+ <TableRow>
116
+ <TableCell
117
+ colSpan={columns.length}
118
+ className="h-24 text-center"
119
+ >
120
+ No recent video.
121
+ </TableCell>
122
+ </TableRow>
123
+ )}
124
+ </TableBody>
125
+ </Table>
126
+ </div>
127
+ )
128
+ }
src/components/ui/accordion.tsx ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as AccordionPrimitive from "@radix-ui/react-accordion"
5
+ import { ChevronDown } from "lucide-react"
6
+
7
+ import { cn } from "@/lib/utils"
8
+
9
+ const Accordion = AccordionPrimitive.Root
10
+
11
+ const AccordionItem = React.forwardRef<
12
+ React.ElementRef<typeof AccordionPrimitive.Item>,
13
+ React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
14
+ >(({ className, ...props }, ref) => (
15
+ <AccordionPrimitive.Item
16
+ ref={ref}
17
+ className={cn("border-b", className)}
18
+ {...props}
19
+ />
20
+ ))
21
+ AccordionItem.displayName = "AccordionItem"
22
+
23
+ const AccordionTrigger = React.forwardRef<
24
+ React.ElementRef<typeof AccordionPrimitive.Trigger>,
25
+ React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
26
+ >(({ className, children, ...props }, ref) => (
27
+ <AccordionPrimitive.Header className="flex">
28
+ <AccordionPrimitive.Trigger
29
+ ref={ref}
30
+ className={cn(
31
+ "flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
32
+ className
33
+ )}
34
+ {...props}
35
+ >
36
+ {children}
37
+ <ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
38
+ </AccordionPrimitive.Trigger>
39
+ </AccordionPrimitive.Header>
40
+ ))
41
+ AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
42
+
43
+ const AccordionContent = React.forwardRef<
44
+ React.ElementRef<typeof AccordionPrimitive.Content>,
45
+ React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
46
+ >(({ className, children, ...props }, ref) => (
47
+ <AccordionPrimitive.Content
48
+ ref={ref}
49
+ className={cn(
50
+ "overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down",
51
+ className
52
+ )}
53
+ {...props}
54
+ >
55
+ <div className="pb-4 pt-0">{children}</div>
56
+ </AccordionPrimitive.Content>
57
+ ))
58
+ AccordionContent.displayName = AccordionPrimitive.Content.displayName
59
+
60
+ export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
src/components/ui/alert.tsx ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import { cva, type VariantProps } from "class-variance-authority"
3
+
4
+ import { cn } from "@/lib/utils"
5
+
6
+ const alertVariants = cva(
7
+ "relative w-full rounded-lg border border-stone-200 p-4 [&:has(svg)]:pl-11 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-stone-950 dark:border-stone-800 dark:[&>svg]:text-stone-50",
8
+ {
9
+ variants: {
10
+ variant: {
11
+ default: "bg-white text-stone-950 dark:bg-stone-950 dark:text-stone-50",
12
+ destructive:
13
+ "border-red-500/50 text-red-500 dark:border-red-500 [&>svg]:text-red-500 dark:border-red-900/50 dark:text-red-900 dark:dark:border-red-900 dark:[&>svg]:text-red-900",
14
+ },
15
+ },
16
+ defaultVariants: {
17
+ variant: "default",
18
+ },
19
+ }
20
+ )
21
+
22
+ const Alert = React.forwardRef<
23
+ HTMLDivElement,
24
+ React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
25
+ >(({ className, variant, ...props }, ref) => (
26
+ <div
27
+ ref={ref}
28
+ role="alert"
29
+ className={cn(alertVariants({ variant }), className)}
30
+ {...props}
31
+ />
32
+ ))
33
+ Alert.displayName = "Alert"
34
+
35
+ const AlertTitle = React.forwardRef<
36
+ HTMLParagraphElement,
37
+ React.HTMLAttributes<HTMLHeadingElement>
38
+ >(({ className, ...props }, ref) => (
39
+ <h5
40
+ ref={ref}
41
+ className={cn("mb-1 font-medium leading-none tracking-tight", className)}
42
+ {...props}
43
+ />
44
+ ))
45
+ AlertTitle.displayName = "AlertTitle"
46
+
47
+ const AlertDescription = React.forwardRef<
48
+ HTMLParagraphElement,
49
+ React.HTMLAttributes<HTMLParagraphElement>
50
+ >(({ className, ...props }, ref) => (
51
+ <div
52
+ ref={ref}
53
+ className={cn("text-sm [&_p]:leading-relaxed", className)}
54
+ {...props}
55
+ />
56
+ ))
57
+ AlertDescription.displayName = "AlertDescription"
58
+
59
+ export { Alert, AlertTitle, AlertDescription }
src/components/ui/avatar.tsx ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as AvatarPrimitive from "@radix-ui/react-avatar"
5
+
6
+ import { cn } from "@/lib/utils"
7
+
8
+ const Avatar = React.forwardRef<
9
+ React.ElementRef<typeof AvatarPrimitive.Root>,
10
+ React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
11
+ >(({ className, ...props }, ref) => (
12
+ <AvatarPrimitive.Root
13
+ ref={ref}
14
+ className={cn(
15
+ "relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
16
+ className
17
+ )}
18
+ {...props}
19
+ />
20
+ ))
21
+ Avatar.displayName = AvatarPrimitive.Root.displayName
22
+
23
+ const AvatarImage = React.forwardRef<
24
+ React.ElementRef<typeof AvatarPrimitive.Image>,
25
+ React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
26
+ >(({ className, ...props }, ref) => (
27
+ <AvatarPrimitive.Image
28
+ ref={ref}
29
+ className={cn("aspect-square h-full w-full", className)}
30
+ {...props}
31
+ />
32
+ ))
33
+ AvatarImage.displayName = AvatarPrimitive.Image.displayName
34
+
35
+ const AvatarFallback = React.forwardRef<
36
+ React.ElementRef<typeof AvatarPrimitive.Fallback>,
37
+ React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
38
+ >(({ className, ...props }, ref) => (
39
+ <AvatarPrimitive.Fallback
40
+ ref={ref}
41
+ className={cn(
42
+ "flex h-full w-full items-center justify-center rounded-full bg-stone-100 dark:bg-stone-800",
43
+ className
44
+ )}
45
+ {...props}
46
+ />
47
+ ))
48
+ AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
49
+
50
+ export { Avatar, AvatarImage, AvatarFallback }
src/components/ui/badge.tsx ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import { cva, type VariantProps } from "class-variance-authority"
3
+
4
+ import { cn } from "@/lib/utils"
5
+
6
+ const badgeVariants = cva(
7
+ "inline-flex items-center rounded-full border border-stone-200 px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-stone-400 focus:ring-offset-2 dark:border-stone-800 dark:focus:ring-stone-800",
8
+ {
9
+ variants: {
10
+ variant: {
11
+ default:
12
+ "border-transparent bg-stone-900 text-stone-50 hover:bg-stone-900/80 dark:bg-stone-50 dark:text-stone-900 dark:hover:bg-stone-50/80",
13
+ secondary:
14
+ "border-transparent bg-stone-100 text-stone-900 hover:bg-stone-100/80 dark:bg-stone-800 dark:text-stone-50 dark:hover:bg-stone-800/80",
15
+ destructive:
16
+ "border-transparent bg-red-500 text-stone-50 hover:bg-red-500/80 dark:bg-red-900 dark:text-red-50 dark:hover:bg-red-900/80",
17
+ outline: "text-stone-950 dark:text-stone-50",
18
+ },
19
+ },
20
+ defaultVariants: {
21
+ variant: "default",
22
+ },
23
+ }
24
+ )
25
+
26
+ export interface BadgeProps
27
+ extends React.HTMLAttributes<HTMLDivElement>,
28
+ VariantProps<typeof badgeVariants> {}
29
+
30
+ function Badge({ className, variant, ...props }: BadgeProps) {
31
+ return (
32
+ <div className={cn(badgeVariants({ variant }), className)} {...props} />
33
+ )
34
+ }
35
+
36
+ export { Badge, badgeVariants }
src/components/ui/button.tsx ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import { Slot } from "@radix-ui/react-slot"
3
+ import { cva, type VariantProps } from "class-variance-authority"
4
+
5
+ import { cn } from "@/lib/utils"
6
+
7
+ const buttonVariants = cva(
8
+ "inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-white transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-stone-400 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 dark:ring-offset-stone-950 dark:focus-visible:ring-stone-800",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default: "bg-stone-900 text-stone-50 hover:bg-stone-900/90 dark:bg-stone-50 dark:text-stone-900 dark:hover:bg-stone-50/90",
13
+ destructive:
14
+ "bg-red-500 text-stone-50 hover:bg-red-500/90 dark:bg-red-900 dark:text-red-50 dark:hover:bg-red-900/90",
15
+ outline:
16
+ "border border-stone-200 bg-white hover:bg-stone-100 hover:text-stone-900 dark:border-stone-800 dark:bg-stone-950 dark:hover:bg-stone-800 dark:hover:text-stone-50",
17
+ secondary:
18
+ "bg-stone-100 text-stone-900 hover:bg-stone-100/80 dark:bg-stone-800 dark:text-stone-50 dark:hover:bg-stone-800/80",
19
+ ghost: "hover:bg-stone-100 hover:text-stone-900 dark:hover:bg-stone-800 dark:hover:text-stone-50",
20
+ link: "text-stone-900 underline-offset-4 hover:underline dark:text-stone-50",
21
+ },
22
+ size: {
23
+ default: "h-10 px-4 py-2",
24
+ sm: "h-9 rounded-md px-3",
25
+ lg: "h-11 rounded-md px-8",
26
+ icon: "h-10 w-10",
27
+ },
28
+ },
29
+ defaultVariants: {
30
+ variant: "default",
31
+ size: "default",
32
+ },
33
+ }
34
+ )
35
+
36
+ export interface ButtonProps
37
+ extends React.ButtonHTMLAttributes<HTMLButtonElement>,
38
+ VariantProps<typeof buttonVariants> {
39
+ asChild?: boolean
40
+ }
41
+
42
+ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
43
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
44
+ const Comp = asChild ? Slot : "button"
45
+ return (
46
+ <Comp
47
+ className={cn(buttonVariants({ variant, size, className }))}
48
+ ref={ref}
49
+ {...props}
50
+ />
51
+ )
52
+ }
53
+ )
54
+ Button.displayName = "Button"
55
+
56
+ export { Button, buttonVariants }
src/components/ui/calendar.tsx ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { ChevronLeft, ChevronRight } from "lucide-react"
5
+ import { DayPicker } from "react-day-picker"
6
+
7
+ import { cn } from "@/lib/utils"
8
+ import { buttonVariants } from "@/components/ui/button"
9
+
10
+ export type CalendarProps = React.ComponentProps<typeof DayPicker>
11
+
12
+ function Calendar({
13
+ className,
14
+ classNames,
15
+ showOutsideDays = true,
16
+ ...props
17
+ }: CalendarProps) {
18
+ return (
19
+ <DayPicker
20
+ showOutsideDays={showOutsideDays}
21
+ className={cn("p-3", className)}
22
+ classNames={{
23
+ months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
24
+ month: "space-y-4",
25
+ caption: "flex justify-center pt-1 relative items-center",
26
+ caption_label: "text-sm font-medium",
27
+ nav: "space-x-1 flex items-center",
28
+ nav_button: cn(
29
+ buttonVariants({ variant: "outline" }),
30
+ "h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100"
31
+ ),
32
+ nav_button_previous: "absolute left-1",
33
+ nav_button_next: "absolute right-1",
34
+ table: "w-full border-collapse space-y-1",
35
+ head_row: "flex",
36
+ head_cell:
37
+ "text-stone-500 rounded-md w-9 font-normal text-[0.8rem] dark:text-stone-400",
38
+ row: "flex w-full mt-2",
39
+ cell: "text-center text-sm p-0 relative [&:has([aria-selected])]:bg-stone-100 first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20 dark:[&:has([aria-selected])]:bg-stone-800",
40
+ day: cn(
41
+ buttonVariants({ variant: "ghost" }),
42
+ "h-9 w-9 p-0 font-normal aria-selected:opacity-100"
43
+ ),
44
+ day_selected:
45
+ "bg-stone-900 text-stone-50 hover:bg-stone-900 hover:text-stone-50 focus:bg-stone-900 focus:text-stone-50 dark:bg-stone-50 dark:text-stone-900 dark:hover:bg-stone-50 dark:hover:text-stone-900 dark:focus:bg-stone-50 dark:focus:text-stone-900",
46
+ day_today: "bg-stone-100 text-stone-900 dark:bg-stone-800 dark:text-stone-50",
47
+ day_outside: "text-stone-500 opacity-50 dark:text-stone-400",
48
+ day_disabled: "text-stone-500 opacity-50 dark:text-stone-400",
49
+ day_range_middle:
50
+ "aria-selected:bg-stone-100 aria-selected:text-stone-900 dark:aria-selected:bg-stone-800 dark:aria-selected:text-stone-50",
51
+ day_hidden: "invisible",
52
+ ...classNames,
53
+ }}
54
+ components={{
55
+ IconLeft: ({ ...props }) => <ChevronLeft className="h-4 w-4" />,
56
+ IconRight: ({ ...props }) => <ChevronRight className="h-4 w-4" />,
57
+ }}
58
+ {...props}
59
+ />
60
+ )
61
+ }
62
+ Calendar.displayName = "Calendar"
63
+
64
+ export { Calendar }
src/components/ui/card.tsx ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+
3
+ import { cn } from "@/lib/utils"
4
+
5
+ const Card = React.forwardRef<
6
+ HTMLDivElement,
7
+ React.HTMLAttributes<HTMLDivElement>
8
+ >(({ className, ...props }, ref) => (
9
+ <div
10
+ ref={ref}
11
+ className={cn(
12
+ "rounded-lg border border-stone-200 bg-white text-stone-950 shadow-sm dark:border-stone-800 dark:bg-stone-950 dark:text-stone-50",
13
+ className
14
+ )}
15
+ {...props}
16
+ />
17
+ ))
18
+ Card.displayName = "Card"
19
+
20
+ const CardHeader = React.forwardRef<
21
+ HTMLDivElement,
22
+ React.HTMLAttributes<HTMLDivElement>
23
+ >(({ className, ...props }, ref) => (
24
+ <div
25
+ ref={ref}
26
+ className={cn("flex flex-col space-y-1.5 p-6", className)}
27
+ {...props}
28
+ />
29
+ ))
30
+ CardHeader.displayName = "CardHeader"
31
+
32
+ const CardTitle = React.forwardRef<
33
+ HTMLParagraphElement,
34
+ React.HTMLAttributes<HTMLHeadingElement>
35
+ >(({ className, ...props }, ref) => (
36
+ <h3
37
+ ref={ref}
38
+ className={cn(
39
+ "text-2xl font-semibold leading-none tracking-tight",
40
+ className
41
+ )}
42
+ {...props}
43
+ />
44
+ ))
45
+ CardTitle.displayName = "CardTitle"
46
+
47
+ const CardDescription = React.forwardRef<
48
+ HTMLParagraphElement,
49
+ React.HTMLAttributes<HTMLParagraphElement>
50
+ >(({ className, ...props }, ref) => (
51
+ <p
52
+ ref={ref}
53
+ className={cn("text-sm text-stone-500 dark:text-stone-400", className)}
54
+ {...props}
55
+ />
56
+ ))
57
+ CardDescription.displayName = "CardDescription"
58
+
59
+ const CardContent = React.forwardRef<
60
+ HTMLDivElement,
61
+ React.HTMLAttributes<HTMLDivElement>
62
+ >(({ className, ...props }, ref) => (
63
+ <div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
64
+ ))
65
+ CardContent.displayName = "CardContent"
66
+
67
+ const CardFooter = React.forwardRef<
68
+ HTMLDivElement,
69
+ React.HTMLAttributes<HTMLDivElement>
70
+ >(({ className, ...props }, ref) => (
71
+ <div
72
+ ref={ref}
73
+ className={cn("flex items-center p-6 pt-0", className)}
74
+ {...props}
75
+ />
76
+ ))
77
+ CardFooter.displayName = "CardFooter"
78
+
79
+ export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
src/components/ui/checkbox.tsx ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
5
+ import { Check } from "lucide-react"
6
+
7
+ import { cn } from "@/lib/utils"
8
+
9
+ const Checkbox = React.forwardRef<
10
+ React.ElementRef<typeof CheckboxPrimitive.Root>,
11
+ React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
12
+ >(({ className, ...props }, ref) => (
13
+ <CheckboxPrimitive.Root
14
+ ref={ref}
15
+ className={cn(
16
+ "peer h-4 w-4 shrink-0 rounded-sm border border-stone-200 border-stone-900 ring-offset-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-stone-400 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-stone-900 data-[state=checked]:text-stone-50 dark:border-stone-800 dark:border-stone-50 dark:ring-offset-stone-950 dark:focus-visible:ring-stone-800 dark:data-[state=checked]:bg-stone-50 dark:data-[state=checked]:text-stone-900",
17
+ className
18
+ )}
19
+ {...props}
20
+ >
21
+ <CheckboxPrimitive.Indicator
22
+ className={cn("flex items-center justify-center text-current")}
23
+ >
24
+ <Check className="h-4 w-4" />
25
+ </CheckboxPrimitive.Indicator>
26
+ </CheckboxPrimitive.Root>
27
+ ))
28
+ Checkbox.displayName = CheckboxPrimitive.Root.displayName
29
+
30
+ export { Checkbox }
src/components/ui/collapsible.tsx ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
4
+
5
+ const Collapsible = CollapsiblePrimitive.Root
6
+
7
+ const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
8
+
9
+ const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
10
+
11
+ export { Collapsible, CollapsibleTrigger, CollapsibleContent }
src/components/ui/command.tsx ADDED
@@ -0,0 +1,155 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { DialogProps } from "@radix-ui/react-dialog"
5
+ import { Command as CommandPrimitive } from "cmdk"
6
+ import { Search } from "lucide-react"
7
+
8
+ import { cn } from "@/lib/utils"
9
+ import { Dialog, DialogContent } from "@/components/ui/dialog"
10
+
11
+ const Command = React.forwardRef<
12
+ React.ElementRef<typeof CommandPrimitive>,
13
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive>
14
+ >(({ className, ...props }, ref) => (
15
+ <CommandPrimitive
16
+ ref={ref}
17
+ className={cn(
18
+ "flex h-full w-full flex-col overflow-hidden rounded-md bg-white text-stone-950 dark:bg-stone-950 dark:text-stone-50",
19
+ className
20
+ )}
21
+ {...props}
22
+ />
23
+ ))
24
+ Command.displayName = CommandPrimitive.displayName
25
+
26
+ interface CommandDialogProps extends DialogProps {}
27
+
28
+ const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
29
+ return (
30
+ <Dialog {...props}>
31
+ <DialogContent className="overflow-hidden p-0 shadow-lg">
32
+ <Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-stone-500 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5 dark:[&_[cmdk-group-heading]]:text-stone-400">
33
+ {children}
34
+ </Command>
35
+ </DialogContent>
36
+ </Dialog>
37
+ )
38
+ }
39
+
40
+ const CommandInput = React.forwardRef<
41
+ React.ElementRef<typeof CommandPrimitive.Input>,
42
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
43
+ >(({ className, ...props }, ref) => (
44
+ <div className="flex items-center border-b px-3" cmdk-input-wrapper="">
45
+ <Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
46
+ <CommandPrimitive.Input
47
+ ref={ref}
48
+ className={cn(
49
+ "flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-stone-500 disabled:cursor-not-allowed disabled:opacity-50 dark:placeholder:text-stone-400",
50
+ className
51
+ )}
52
+ {...props}
53
+ />
54
+ </div>
55
+ ))
56
+
57
+ CommandInput.displayName = CommandPrimitive.Input.displayName
58
+
59
+ const CommandList = React.forwardRef<
60
+ React.ElementRef<typeof CommandPrimitive.List>,
61
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
62
+ >(({ className, ...props }, ref) => (
63
+ <CommandPrimitive.List
64
+ ref={ref}
65
+ className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
66
+ {...props}
67
+ />
68
+ ))
69
+
70
+ CommandList.displayName = CommandPrimitive.List.displayName
71
+
72
+ const CommandEmpty = React.forwardRef<
73
+ React.ElementRef<typeof CommandPrimitive.Empty>,
74
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
75
+ >((props, ref) => (
76
+ <CommandPrimitive.Empty
77
+ ref={ref}
78
+ className="py-6 text-center text-sm"
79
+ {...props}
80
+ />
81
+ ))
82
+
83
+ CommandEmpty.displayName = CommandPrimitive.Empty.displayName
84
+
85
+ const CommandGroup = React.forwardRef<
86
+ React.ElementRef<typeof CommandPrimitive.Group>,
87
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
88
+ >(({ className, ...props }, ref) => (
89
+ <CommandPrimitive.Group
90
+ ref={ref}
91
+ className={cn(
92
+ "overflow-hidden p-1 text-stone-950 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-stone-500 dark:text-stone-50 dark:[&_[cmdk-group-heading]]:text-stone-400",
93
+ className
94
+ )}
95
+ {...props}
96
+ />
97
+ ))
98
+
99
+ CommandGroup.displayName = CommandPrimitive.Group.displayName
100
+
101
+ const CommandSeparator = React.forwardRef<
102
+ React.ElementRef<typeof CommandPrimitive.Separator>,
103
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
104
+ >(({ className, ...props }, ref) => (
105
+ <CommandPrimitive.Separator
106
+ ref={ref}
107
+ className={cn("-mx-1 h-px bg-stone-200 dark:bg-stone-800", className)}
108
+ {...props}
109
+ />
110
+ ))
111
+ CommandSeparator.displayName = CommandPrimitive.Separator.displayName
112
+
113
+ const CommandItem = React.forwardRef<
114
+ React.ElementRef<typeof CommandPrimitive.Item>,
115
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
116
+ >(({ className, ...props }, ref) => (
117
+ <CommandPrimitive.Item
118
+ ref={ref}
119
+ className={cn(
120
+ "relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none aria-selected:bg-stone-100 aria-selected:text-stone-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:aria-selected:bg-stone-800 dark:aria-selected:text-stone-50",
121
+ className
122
+ )}
123
+ {...props}
124
+ />
125
+ ))
126
+
127
+ CommandItem.displayName = CommandPrimitive.Item.displayName
128
+
129
+ const CommandShortcut = ({
130
+ className,
131
+ ...props
132
+ }: React.HTMLAttributes<HTMLSpanElement>) => {
133
+ return (
134
+ <span
135
+ className={cn(
136
+ "ml-auto text-xs tracking-widest text-stone-500 dark:text-stone-400",
137
+ className
138
+ )}
139
+ {...props}
140
+ />
141
+ )
142
+ }
143
+ CommandShortcut.displayName = "CommandShortcut"
144
+
145
+ export {
146
+ Command,
147
+ CommandDialog,
148
+ CommandInput,
149
+ CommandList,
150
+ CommandEmpty,
151
+ CommandGroup,
152
+ CommandItem,
153
+ CommandShortcut,
154
+ CommandSeparator,
155
+ }
src/components/ui/dialog.tsx ADDED
@@ -0,0 +1,123 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as DialogPrimitive from "@radix-ui/react-dialog"
5
+ import { X } from "lucide-react"
6
+
7
+ import { cn } from "@/lib/utils"
8
+
9
+ const Dialog = DialogPrimitive.Root
10
+
11
+ const DialogTrigger = DialogPrimitive.Trigger
12
+
13
+ const DialogPortal = ({
14
+ className,
15
+ ...props
16
+ }: DialogPrimitive.DialogPortalProps) => (
17
+ <DialogPrimitive.Portal className={cn(className)} {...props} />
18
+ )
19
+ DialogPortal.displayName = DialogPrimitive.Portal.displayName
20
+
21
+ const DialogOverlay = React.forwardRef<
22
+ React.ElementRef<typeof DialogPrimitive.Overlay>,
23
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
24
+ >(({ className, ...props }, ref) => (
25
+ <DialogPrimitive.Overlay
26
+ ref={ref}
27
+ className={cn(
28
+ "fixed inset-0 z-50 bg-white/80 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 dark:bg-stone-950/80",
29
+ className
30
+ )}
31
+ {...props}
32
+ />
33
+ ))
34
+ DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
35
+
36
+ const DialogContent = React.forwardRef<
37
+ React.ElementRef<typeof DialogPrimitive.Content>,
38
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
39
+ >(({ className, children, ...props }, ref) => (
40
+ <DialogPortal>
41
+ <DialogOverlay />
42
+ <DialogPrimitive.Content
43
+ ref={ref}
44
+ className={cn(
45
+ "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border border-stone-200 bg-white p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg md:w-full dark:border-stone-800 dark:bg-stone-950",
46
+ className
47
+ )}
48
+ {...props}
49
+ >
50
+ {children}
51
+ <DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-white transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-stone-400 focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-stone-100 data-[state=open]:text-stone-500 dark:ring-offset-stone-950 dark:focus:ring-stone-800 dark:data-[state=open]:bg-stone-800 dark:data-[state=open]:text-stone-400">
52
+ <X className="h-4 w-4" />
53
+ <span className="sr-only">Close</span>
54
+ </DialogPrimitive.Close>
55
+ </DialogPrimitive.Content>
56
+ </DialogPortal>
57
+ ))
58
+ DialogContent.displayName = DialogPrimitive.Content.displayName
59
+
60
+ const DialogHeader = ({
61
+ className,
62
+ ...props
63
+ }: React.HTMLAttributes<HTMLDivElement>) => (
64
+ <div
65
+ className={cn(
66
+ "flex flex-col space-y-1.5 text-center sm:text-left",
67
+ className
68
+ )}
69
+ {...props}
70
+ />
71
+ )
72
+ DialogHeader.displayName = "DialogHeader"
73
+
74
+ const DialogFooter = ({
75
+ className,
76
+ ...props
77
+ }: React.HTMLAttributes<HTMLDivElement>) => (
78
+ <div
79
+ className={cn(
80
+ "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
81
+ className
82
+ )}
83
+ {...props}
84
+ />
85
+ )
86
+ DialogFooter.displayName = "DialogFooter"
87
+
88
+ const DialogTitle = React.forwardRef<
89
+ React.ElementRef<typeof DialogPrimitive.Title>,
90
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
91
+ >(({ className, ...props }, ref) => (
92
+ <DialogPrimitive.Title
93
+ ref={ref}
94
+ className={cn(
95
+ "text-lg font-semibold leading-none tracking-tight",
96
+ className
97
+ )}
98
+ {...props}
99
+ />
100
+ ))
101
+ DialogTitle.displayName = DialogPrimitive.Title.displayName
102
+
103
+ const DialogDescription = React.forwardRef<
104
+ React.ElementRef<typeof DialogPrimitive.Description>,
105
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
106
+ >(({ className, ...props }, ref) => (
107
+ <DialogPrimitive.Description
108
+ ref={ref}
109
+ className={cn("text-sm text-stone-500 dark:text-stone-400", className)}
110
+ {...props}
111
+ />
112
+ ))
113
+ DialogDescription.displayName = DialogPrimitive.Description.displayName
114
+
115
+ export {
116
+ Dialog,
117
+ DialogTrigger,
118
+ DialogContent,
119
+ DialogHeader,
120
+ DialogFooter,
121
+ DialogTitle,
122
+ DialogDescription,
123
+ }
src/components/ui/dropdown-menu.tsx ADDED
@@ -0,0 +1,200 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
5
+ import { Check, ChevronRight, Circle } from "lucide-react"
6
+
7
+ import { cn } from "@/lib/utils"
8
+
9
+ const DropdownMenu = DropdownMenuPrimitive.Root
10
+
11
+ const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
12
+
13
+ const DropdownMenuGroup = DropdownMenuPrimitive.Group
14
+
15
+ const DropdownMenuPortal = DropdownMenuPrimitive.Portal
16
+
17
+ const DropdownMenuSub = DropdownMenuPrimitive.Sub
18
+
19
+ const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
20
+
21
+ const DropdownMenuSubTrigger = React.forwardRef<
22
+ React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
23
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
24
+ inset?: boolean
25
+ }
26
+ >(({ className, inset, children, ...props }, ref) => (
27
+ <DropdownMenuPrimitive.SubTrigger
28
+ ref={ref}
29
+ className={cn(
30
+ "flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-stone-100 data-[state=open]:bg-stone-100 dark:focus:bg-stone-800 dark:data-[state=open]:bg-stone-800",
31
+ inset && "pl-8",
32
+ className
33
+ )}
34
+ {...props}
35
+ >
36
+ {children}
37
+ <ChevronRight className="ml-auto h-4 w-4" />
38
+ </DropdownMenuPrimitive.SubTrigger>
39
+ ))
40
+ DropdownMenuSubTrigger.displayName =
41
+ DropdownMenuPrimitive.SubTrigger.displayName
42
+
43
+ const DropdownMenuSubContent = React.forwardRef<
44
+ React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
45
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
46
+ >(({ className, ...props }, ref) => (
47
+ <DropdownMenuPrimitive.SubContent
48
+ ref={ref}
49
+ className={cn(
50
+ "z-50 min-w-[8rem] overflow-hidden rounded-md border border-stone-200 bg-white p-1 text-stone-950 shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=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 dark:border-stone-800 dark:bg-stone-950 dark:text-stone-50",
51
+ className
52
+ )}
53
+ {...props}
54
+ />
55
+ ))
56
+ DropdownMenuSubContent.displayName =
57
+ DropdownMenuPrimitive.SubContent.displayName
58
+
59
+ const DropdownMenuContent = React.forwardRef<
60
+ React.ElementRef<typeof DropdownMenuPrimitive.Content>,
61
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
62
+ >(({ className, sideOffset = 4, ...props }, ref) => (
63
+ <DropdownMenuPrimitive.Portal>
64
+ <DropdownMenuPrimitive.Content
65
+ ref={ref}
66
+ sideOffset={sideOffset}
67
+ className={cn(
68
+ "z-50 min-w-[8rem] overflow-hidden rounded-md border border-stone-200 bg-white p-1 text-stone-950 shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=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 dark:border-stone-800 dark:bg-stone-950 dark:text-stone-50",
69
+ className
70
+ )}
71
+ {...props}
72
+ />
73
+ </DropdownMenuPrimitive.Portal>
74
+ ))
75
+ DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
76
+
77
+ const DropdownMenuItem = React.forwardRef<
78
+ React.ElementRef<typeof DropdownMenuPrimitive.Item>,
79
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
80
+ inset?: boolean
81
+ }
82
+ >(({ className, inset, ...props }, ref) => (
83
+ <DropdownMenuPrimitive.Item
84
+ ref={ref}
85
+ className={cn(
86
+ "relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-stone-100 focus:text-stone-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-stone-800 dark:focus:text-stone-50",
87
+ inset && "pl-8",
88
+ className
89
+ )}
90
+ {...props}
91
+ />
92
+ ))
93
+ DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
94
+
95
+ const DropdownMenuCheckboxItem = React.forwardRef<
96
+ React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
97
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
98
+ >(({ className, children, checked, ...props }, ref) => (
99
+ <DropdownMenuPrimitive.CheckboxItem
100
+ ref={ref}
101
+ className={cn(
102
+ "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-stone-100 focus:text-stone-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-stone-800 dark:focus:text-stone-50",
103
+ className
104
+ )}
105
+ checked={checked}
106
+ {...props}
107
+ >
108
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
109
+ <DropdownMenuPrimitive.ItemIndicator>
110
+ <Check className="h-4 w-4" />
111
+ </DropdownMenuPrimitive.ItemIndicator>
112
+ </span>
113
+ {children}
114
+ </DropdownMenuPrimitive.CheckboxItem>
115
+ ))
116
+ DropdownMenuCheckboxItem.displayName =
117
+ DropdownMenuPrimitive.CheckboxItem.displayName
118
+
119
+ const DropdownMenuRadioItem = React.forwardRef<
120
+ React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
121
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
122
+ >(({ className, children, ...props }, ref) => (
123
+ <DropdownMenuPrimitive.RadioItem
124
+ ref={ref}
125
+ className={cn(
126
+ "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-stone-100 focus:text-stone-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-stone-800 dark:focus:text-stone-50",
127
+ className
128
+ )}
129
+ {...props}
130
+ >
131
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
132
+ <DropdownMenuPrimitive.ItemIndicator>
133
+ <Circle className="h-2 w-2 fill-current" />
134
+ </DropdownMenuPrimitive.ItemIndicator>
135
+ </span>
136
+ {children}
137
+ </DropdownMenuPrimitive.RadioItem>
138
+ ))
139
+ DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
140
+
141
+ const DropdownMenuLabel = React.forwardRef<
142
+ React.ElementRef<typeof DropdownMenuPrimitive.Label>,
143
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
144
+ inset?: boolean
145
+ }
146
+ >(({ className, inset, ...props }, ref) => (
147
+ <DropdownMenuPrimitive.Label
148
+ ref={ref}
149
+ className={cn(
150
+ "px-2 py-1.5 text-sm font-semibold",
151
+ inset && "pl-8",
152
+ className
153
+ )}
154
+ {...props}
155
+ />
156
+ ))
157
+ DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
158
+
159
+ const DropdownMenuSeparator = React.forwardRef<
160
+ React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
161
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
162
+ >(({ className, ...props }, ref) => (
163
+ <DropdownMenuPrimitive.Separator
164
+ ref={ref}
165
+ className={cn("-mx-1 my-1 h-px bg-stone-100 dark:bg-stone-800", className)}
166
+ {...props}
167
+ />
168
+ ))
169
+ DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
170
+
171
+ const DropdownMenuShortcut = ({
172
+ className,
173
+ ...props
174
+ }: React.HTMLAttributes<HTMLSpanElement>) => {
175
+ return (
176
+ <span
177
+ className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
178
+ {...props}
179
+ />
180
+ )
181
+ }
182
+ DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
183
+
184
+ export {
185
+ DropdownMenu,
186
+ DropdownMenuTrigger,
187
+ DropdownMenuContent,
188
+ DropdownMenuItem,
189
+ DropdownMenuCheckboxItem,
190
+ DropdownMenuRadioItem,
191
+ DropdownMenuLabel,
192
+ DropdownMenuSeparator,
193
+ DropdownMenuShortcut,
194
+ DropdownMenuGroup,
195
+ DropdownMenuPortal,
196
+ DropdownMenuSub,
197
+ DropdownMenuSubContent,
198
+ DropdownMenuSubTrigger,
199
+ DropdownMenuRadioGroup,
200
+ }
src/components/ui/input.tsx ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+
3
+ import { cn } from "@/lib/utils"
4
+
5
+ export interface InputProps
6
+ extends React.InputHTMLAttributes<HTMLInputElement> {}
7
+
8
+ const Input = React.forwardRef<HTMLInputElement, InputProps>(
9
+ ({ className, type, ...props }, ref) => {
10
+ return (
11
+ <input
12
+ type={type}
13
+ className={cn(
14
+ "flex h-10 w-full rounded-md border border-stone-200 bg-white px-3 py-2 text-sm ring-offset-white file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-stone-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-stone-400 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-stone-800 dark:bg-stone-950 dark:ring-offset-stone-950 dark:placeholder:text-stone-400 dark:focus-visible:ring-stone-800",
15
+ className
16
+ )}
17
+ ref={ref}
18
+ {...props}
19
+ />
20
+ )
21
+ }
22
+ )
23
+ Input.displayName = "Input"
24
+
25
+ export { Input }
src/components/ui/menubar.tsx ADDED
@@ -0,0 +1,236 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as MenubarPrimitive from "@radix-ui/react-menubar"
5
+ import { Check, ChevronRight, Circle } from "lucide-react"
6
+
7
+ import { cn } from "@/lib/utils"
8
+
9
+ const MenubarMenu = MenubarPrimitive.Menu
10
+
11
+ const MenubarGroup = MenubarPrimitive.Group
12
+
13
+ const MenubarPortal = MenubarPrimitive.Portal
14
+
15
+ const MenubarSub = MenubarPrimitive.Sub
16
+
17
+ const MenubarRadioGroup = MenubarPrimitive.RadioGroup
18
+
19
+ const Menubar = React.forwardRef<
20
+ React.ElementRef<typeof MenubarPrimitive.Root>,
21
+ React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Root>
22
+ >(({ className, ...props }, ref) => (
23
+ <MenubarPrimitive.Root
24
+ ref={ref}
25
+ className={cn(
26
+ "flex h-10 items-center space-x-1 rounded-md border border-stone-200 bg-white p-1 dark:border-stone-800 dark:bg-stone-950",
27
+ className
28
+ )}
29
+ {...props}
30
+ />
31
+ ))
32
+ Menubar.displayName = MenubarPrimitive.Root.displayName
33
+
34
+ const MenubarTrigger = React.forwardRef<
35
+ React.ElementRef<typeof MenubarPrimitive.Trigger>,
36
+ React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Trigger>
37
+ >(({ className, ...props }, ref) => (
38
+ <MenubarPrimitive.Trigger
39
+ ref={ref}
40
+ className={cn(
41
+ "flex cursor-default select-none items-center rounded-sm px-3 py-1.5 text-sm font-medium outline-none focus:bg-stone-100 focus:text-stone-900 data-[state=open]:bg-stone-100 data-[state=open]:text-stone-900 dark:focus:bg-stone-800 dark:focus:text-stone-50 dark:data-[state=open]:bg-stone-800 dark:data-[state=open]:text-stone-50",
42
+ className
43
+ )}
44
+ {...props}
45
+ />
46
+ ))
47
+ MenubarTrigger.displayName = MenubarPrimitive.Trigger.displayName
48
+
49
+ const MenubarSubTrigger = React.forwardRef<
50
+ React.ElementRef<typeof MenubarPrimitive.SubTrigger>,
51
+ React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubTrigger> & {
52
+ inset?: boolean
53
+ }
54
+ >(({ className, inset, children, ...props }, ref) => (
55
+ <MenubarPrimitive.SubTrigger
56
+ ref={ref}
57
+ className={cn(
58
+ "flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-stone-100 focus:text-stone-900 data-[state=open]:bg-stone-100 data-[state=open]:text-stone-900 dark:focus:bg-stone-800 dark:focus:text-stone-50 dark:data-[state=open]:bg-stone-800 dark:data-[state=open]:text-stone-50",
59
+ inset && "pl-8",
60
+ className
61
+ )}
62
+ {...props}
63
+ >
64
+ {children}
65
+ <ChevronRight className="ml-auto h-4 w-4" />
66
+ </MenubarPrimitive.SubTrigger>
67
+ ))
68
+ MenubarSubTrigger.displayName = MenubarPrimitive.SubTrigger.displayName
69
+
70
+ const MenubarSubContent = React.forwardRef<
71
+ React.ElementRef<typeof MenubarPrimitive.SubContent>,
72
+ React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubContent>
73
+ >(({ className, ...props }, ref) => (
74
+ <MenubarPrimitive.SubContent
75
+ ref={ref}
76
+ className={cn(
77
+ "z-50 min-w-[8rem] overflow-hidden rounded-md border border-stone-200 bg-white p-1 text-stone-950 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=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 dark:border-stone-800 dark:bg-stone-950 dark:text-stone-50",
78
+ className
79
+ )}
80
+ {...props}
81
+ />
82
+ ))
83
+ MenubarSubContent.displayName = MenubarPrimitive.SubContent.displayName
84
+
85
+ const MenubarContent = React.forwardRef<
86
+ React.ElementRef<typeof MenubarPrimitive.Content>,
87
+ React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Content>
88
+ >(
89
+ (
90
+ { className, align = "start", alignOffset = -4, sideOffset = 8, ...props },
91
+ ref
92
+ ) => (
93
+ <MenubarPrimitive.Portal>
94
+ <MenubarPrimitive.Content
95
+ ref={ref}
96
+ align={align}
97
+ alignOffset={alignOffset}
98
+ sideOffset={sideOffset}
99
+ className={cn(
100
+ "z-50 min-w-[12rem] overflow-hidden rounded-md border border-stone-200 bg-white p-1 text-stone-950 shadow-md data-[state=open]:animate-in data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=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 dark:border-stone-800 dark:bg-stone-950 dark:text-stone-50",
101
+ className
102
+ )}
103
+ {...props}
104
+ />
105
+ </MenubarPrimitive.Portal>
106
+ )
107
+ )
108
+ MenubarContent.displayName = MenubarPrimitive.Content.displayName
109
+
110
+ const MenubarItem = React.forwardRef<
111
+ React.ElementRef<typeof MenubarPrimitive.Item>,
112
+ React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Item> & {
113
+ inset?: boolean
114
+ }
115
+ >(({ className, inset, ...props }, ref) => (
116
+ <MenubarPrimitive.Item
117
+ ref={ref}
118
+ className={cn(
119
+ "relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-stone-100 focus:text-stone-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-stone-800 dark:focus:text-stone-50",
120
+ inset && "pl-8",
121
+ className
122
+ )}
123
+ {...props}
124
+ />
125
+ ))
126
+ MenubarItem.displayName = MenubarPrimitive.Item.displayName
127
+
128
+ const MenubarCheckboxItem = React.forwardRef<
129
+ React.ElementRef<typeof MenubarPrimitive.CheckboxItem>,
130
+ React.ComponentPropsWithoutRef<typeof MenubarPrimitive.CheckboxItem>
131
+ >(({ className, children, checked, ...props }, ref) => (
132
+ <MenubarPrimitive.CheckboxItem
133
+ ref={ref}
134
+ className={cn(
135
+ "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-stone-100 focus:text-stone-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-stone-800 dark:focus:text-stone-50",
136
+ className
137
+ )}
138
+ checked={checked}
139
+ {...props}
140
+ >
141
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
142
+ <MenubarPrimitive.ItemIndicator>
143
+ <Check className="h-4 w-4" />
144
+ </MenubarPrimitive.ItemIndicator>
145
+ </span>
146
+ {children}
147
+ </MenubarPrimitive.CheckboxItem>
148
+ ))
149
+ MenubarCheckboxItem.displayName = MenubarPrimitive.CheckboxItem.displayName
150
+
151
+ const MenubarRadioItem = React.forwardRef<
152
+ React.ElementRef<typeof MenubarPrimitive.RadioItem>,
153
+ React.ComponentPropsWithoutRef<typeof MenubarPrimitive.RadioItem>
154
+ >(({ className, children, ...props }, ref) => (
155
+ <MenubarPrimitive.RadioItem
156
+ ref={ref}
157
+ className={cn(
158
+ "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-stone-100 focus:text-stone-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-stone-800 dark:focus:text-stone-50",
159
+ className
160
+ )}
161
+ {...props}
162
+ >
163
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
164
+ <MenubarPrimitive.ItemIndicator>
165
+ <Circle className="h-2 w-2 fill-current" />
166
+ </MenubarPrimitive.ItemIndicator>
167
+ </span>
168
+ {children}
169
+ </MenubarPrimitive.RadioItem>
170
+ ))
171
+ MenubarRadioItem.displayName = MenubarPrimitive.RadioItem.displayName
172
+
173
+ const MenubarLabel = React.forwardRef<
174
+ React.ElementRef<typeof MenubarPrimitive.Label>,
175
+ React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Label> & {
176
+ inset?: boolean
177
+ }
178
+ >(({ className, inset, ...props }, ref) => (
179
+ <MenubarPrimitive.Label
180
+ ref={ref}
181
+ className={cn(
182
+ "px-2 py-1.5 text-sm font-semibold",
183
+ inset && "pl-8",
184
+ className
185
+ )}
186
+ {...props}
187
+ />
188
+ ))
189
+ MenubarLabel.displayName = MenubarPrimitive.Label.displayName
190
+
191
+ const MenubarSeparator = React.forwardRef<
192
+ React.ElementRef<typeof MenubarPrimitive.Separator>,
193
+ React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Separator>
194
+ >(({ className, ...props }, ref) => (
195
+ <MenubarPrimitive.Separator
196
+ ref={ref}
197
+ className={cn("-mx-1 my-1 h-px bg-stone-100 dark:bg-stone-800", className)}
198
+ {...props}
199
+ />
200
+ ))
201
+ MenubarSeparator.displayName = MenubarPrimitive.Separator.displayName
202
+
203
+ const MenubarShortcut = ({
204
+ className,
205
+ ...props
206
+ }: React.HTMLAttributes<HTMLSpanElement>) => {
207
+ return (
208
+ <span
209
+ className={cn(
210
+ "ml-auto text-xs tracking-widest text-stone-500 dark:text-stone-400",
211
+ className
212
+ )}
213
+ {...props}
214
+ />
215
+ )
216
+ }
217
+ MenubarShortcut.displayname = "MenubarShortcut"
218
+
219
+ export {
220
+ Menubar,
221
+ MenubarMenu,
222
+ MenubarTrigger,
223
+ MenubarContent,
224
+ MenubarItem,
225
+ MenubarSeparator,
226
+ MenubarLabel,
227
+ MenubarCheckboxItem,
228
+ MenubarRadioGroup,
229
+ MenubarRadioItem,
230
+ MenubarPortal,
231
+ MenubarSubContent,
232
+ MenubarSubTrigger,
233
+ MenubarGroup,
234
+ MenubarSub,
235
+ MenubarShortcut,
236
+ }
src/components/ui/popover.tsx ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as PopoverPrimitive from "@radix-ui/react-popover"
5
+
6
+ import { cn } from "@/lib/utils"
7
+
8
+ const Popover = PopoverPrimitive.Root
9
+
10
+ const PopoverTrigger = PopoverPrimitive.Trigger
11
+
12
+ const PopoverContent = React.forwardRef<
13
+ React.ElementRef<typeof PopoverPrimitive.Content>,
14
+ React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
15
+ >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
16
+ <PopoverPrimitive.Portal>
17
+ <PopoverPrimitive.Content
18
+ ref={ref}
19
+ align={align}
20
+ sideOffset={sideOffset}
21
+ className={cn(
22
+ "z-50 w-72 rounded-md border border-stone-200 bg-white p-4 text-stone-950 shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=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 dark:border-stone-800 dark:bg-stone-950 dark:text-stone-50",
23
+ className
24
+ )}
25
+ {...props}
26
+ />
27
+ </PopoverPrimitive.Portal>
28
+ ))
29
+ PopoverContent.displayName = PopoverPrimitive.Content.displayName
30
+
31
+ export { Popover, PopoverTrigger, PopoverContent }