barton
commited on
Commit
·
a85305f
0
Parent(s):
Duplicate from bmorphism/vibecraft
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .env +3 -0
- .eslintrc.json +3 -0
- .gitignore +35 -0
- .nvmrc +1 -0
- Dockerfile +65 -0
- README.md +41 -0
- components.json +16 -0
- next.config.js +10 -0
- package-lock.json +0 -0
- package.json +60 -0
- postcss.config.js +6 -0
- public/next.svg +1 -0
- public/vercel.svg +1 -0
- scripts/test.js +23 -0
- src/app/data/data.ts +71 -0
- src/app/data/mock.json +16 -0
- src/app/data/schema.ts +13 -0
- src/app/favicon.ico +0 -0
- src/app/forlater/page.tsx +18 -0
- src/app/globals.css +27 -0
- src/app/layout.tsx +24 -0
- src/app/page.tsx +27 -0
- src/app/studio/[ownerId]/main.tsx +26 -0
- src/app/studio/[ownerId]/page.tsx +22 -0
- src/app/types.ts +258 -0
- src/components/business/menu.tsx +200 -0
- src/components/business/refresh.tsx +27 -0
- src/components/business/timeline/index.tsx +71 -0
- src/components/business/video-form.tsx +63 -0
- src/components/business/video-player.tsx +33 -0
- src/components/business/videos/change-status-button.tsx +20 -0
- src/components/business/videos/column-header.tsx +71 -0
- src/components/business/videos/columns.tsx +151 -0
- src/components/business/videos/video-actions.tsx +52 -0
- src/components/business/videos/video-table.tsx +128 -0
- src/components/ui/accordion.tsx +60 -0
- src/components/ui/alert.tsx +59 -0
- src/components/ui/avatar.tsx +50 -0
- src/components/ui/badge.tsx +36 -0
- src/components/ui/button.tsx +56 -0
- src/components/ui/calendar.tsx +64 -0
- src/components/ui/card.tsx +79 -0
- src/components/ui/checkbox.tsx +30 -0
- src/components/ui/collapsible.tsx +11 -0
- src/components/ui/command.tsx +155 -0
- src/components/ui/dialog.tsx +123 -0
- src/components/ui/dropdown-menu.tsx +200 -0
- src/components/ui/input.tsx +25 -0
- src/components/ui/menubar.tsx +236 -0
- 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'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 }
|