Spaces:
Sleeping
Sleeping
Commit
·
624088c
0
Parent(s):
initial commit
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 +11 -0
- components.json +16 -0
- next.config.js +10 -0
- package-lock.json +0 -0
- package.json +66 -0
- postcss.config.js +6 -0
- public/next.svg +1 -0
- public/vercel.svg +1 -0
- scripts/test.js +23 -0
- src/app/engine/censorship.ts +4 -0
- src/app/engine/forbidden.ts +6 -0
- src/app/engine/presets.ts +136 -0
- src/app/engine/render.ts +132 -0
- src/app/favicon.ico +0 -0
- src/app/globals.css +27 -0
- src/app/interface/display/index.tsx +12 -0
- src/app/interface/panel/bubble.tsx +45 -0
- src/app/interface/panel/index.tsx +139 -0
- src/app/interface/progress/index.tsx +56 -0
- src/app/interface/progress/progress-bar.tsx +57 -0
- src/app/interface/top-menu/index.tsx +103 -0
- src/app/layout.tsx +24 -0
- src/app/main.tsx +112 -0
- src/app/page.tsx +28 -0
- src/app/queries/getBackground.ts +56 -0
- src/app/queries/predict.ts +56 -0
- src/components/icons/full-screen.tsx +16 -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/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/label.tsx +26 -0
- src/components/ui/menubar.tsx +236 -0
- src/components/ui/popover.tsx +31 -0
- src/components/ui/select.tsx +121 -0
- src/components/ui/separator.tsx +31 -0
- src/components/ui/switch.tsx +29 -0
.env
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
NEXT_PUBLIC_BASE_URL=https://jbilcke-hf-fishtank.hf.space
|
2 |
+
# NEXT_PUBLIC_RENDERING_ENGINE_API=https://hysts-zeroscope-v2.hf.space
|
3 |
+
RENDERING_ENGINE_API=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,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
---
|
2 |
+
title: Comic Factory
|
3 |
+
emoji: 👩🎨
|
4 |
+
colorFrom: red
|
5 |
+
colorTo: yellow
|
6 |
+
sdk: docker
|
7 |
+
pinned: true
|
8 |
+
app_port: 3000
|
9 |
+
---
|
10 |
+
|
11 |
+
Comic Factory
|
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": "stone",
|
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,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"name": "@jbilcke/comic-factory",
|
3 |
+
"version": "0.0.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 |
+
"@huggingface/inference": "^2.6.1",
|
13 |
+
"@photo-sphere-viewer/core": "^5.1.7",
|
14 |
+
"@radix-ui/react-accordion": "^1.1.2",
|
15 |
+
"@radix-ui/react-avatar": "^1.0.3",
|
16 |
+
"@radix-ui/react-checkbox": "^1.0.4",
|
17 |
+
"@radix-ui/react-collapsible": "^1.0.3",
|
18 |
+
"@radix-ui/react-dialog": "^1.0.4",
|
19 |
+
"@radix-ui/react-dropdown-menu": "^2.0.5",
|
20 |
+
"@radix-ui/react-icons": "^1.3.0",
|
21 |
+
"@radix-ui/react-label": "^2.0.2",
|
22 |
+
"@radix-ui/react-menubar": "^1.0.3",
|
23 |
+
"@radix-ui/react-popover": "^1.0.6",
|
24 |
+
"@radix-ui/react-select": "^1.2.2",
|
25 |
+
"@radix-ui/react-separator": "^1.0.3",
|
26 |
+
"@radix-ui/react-slot": "^1.0.2",
|
27 |
+
"@radix-ui/react-switch": "^1.0.3",
|
28 |
+
"@radix-ui/react-tooltip": "^1.0.6",
|
29 |
+
"@react-pdf/renderer": "^3.1.12",
|
30 |
+
"@types/node": "20.4.2",
|
31 |
+
"@types/react": "18.2.15",
|
32 |
+
"@types/react-dom": "18.2.7",
|
33 |
+
"@types/uuid": "^9.0.2",
|
34 |
+
"autoprefixer": "10.4.14",
|
35 |
+
"class-variance-authority": "^0.6.1",
|
36 |
+
"clsx": "^2.0.0",
|
37 |
+
"cmdk": "^0.2.0",
|
38 |
+
"cookies-next": "^2.1.2",
|
39 |
+
"date-fns": "^2.30.0",
|
40 |
+
"eslint": "8.45.0",
|
41 |
+
"eslint-config-next": "13.4.10",
|
42 |
+
"lucide-react": "^0.260.0",
|
43 |
+
"next": "13.4.10",
|
44 |
+
"pick": "^0.0.1",
|
45 |
+
"postcss": "8.4.26",
|
46 |
+
"react": "18.2.0",
|
47 |
+
"react-circular-progressbar": "^2.1.0",
|
48 |
+
"react-dom": "18.2.0",
|
49 |
+
"react-virtualized": "^9.22.5",
|
50 |
+
"sbd": "^1.0.19",
|
51 |
+
"styled-components": "^6.0.7",
|
52 |
+
"tailwind-merge": "^1.13.2",
|
53 |
+
"tailwindcss": "3.3.3",
|
54 |
+
"tailwindcss-animate": "^1.0.6",
|
55 |
+
"temp-dir": "^3.0.0",
|
56 |
+
"ts-node": "^10.9.1",
|
57 |
+
"typescript": "5.1.6",
|
58 |
+
"usehooks-ts": "^2.9.1",
|
59 |
+
"uuid": "^9.0.0"
|
60 |
+
},
|
61 |
+
"devDependencies": {
|
62 |
+
"@types/qs": "^6.9.7",
|
63 |
+
"@types/react-virtualized": "^9.21.22",
|
64 |
+
"@types/sbd": "^1.0.3"
|
65 |
+
}
|
66 |
+
}
|
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/engine/censorship.ts
ADDED
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
1 |
+
|
2 |
+
|
3 |
+
// unfortunately due to abuse by some users, I have to add this NSFW filter
|
4 |
+
const secretSalt = `${process.env.SECRET_CENSORSHIP_KEY || ""}`
|
src/app/engine/forbidden.ts
ADDED
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
|
2 |
+
// the NSFW has to contain bad words, but doing so might get the code flagged
|
3 |
+
// or attract unwanted attention, so we hash them
|
4 |
+
export const forbidden = [
|
5 |
+
|
6 |
+
]
|
src/app/engine/presets.ts
ADDED
@@ -0,0 +1,136 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { FontName, actionman, komika, vtc } from "@/lib/fonts"
|
2 |
+
import { NextFontWithVariable } from "next/dist/compiled/@next/font"
|
3 |
+
|
4 |
+
export type ComicFamily =
|
5 |
+
| "american"
|
6 |
+
| "asian"
|
7 |
+
| "european"
|
8 |
+
|
9 |
+
export type ComicColor =
|
10 |
+
| "color"
|
11 |
+
| "grayscale"
|
12 |
+
| "monochrome"
|
13 |
+
|
14 |
+
export interface Preset {
|
15 |
+
label: string
|
16 |
+
family: ComicFamily
|
17 |
+
color: ComicColor
|
18 |
+
font: FontName
|
19 |
+
llmPrompt: string
|
20 |
+
imagePrompt: (prompt: string) => string[]
|
21 |
+
negativePrompt: (prompt: string) => string[]
|
22 |
+
}
|
23 |
+
|
24 |
+
// ATTENTION!! negative prompts are not supported by the VideoChain API yet
|
25 |
+
|
26 |
+
export const presets: Record<string, Preset> = {
|
27 |
+
japanese_manga: {
|
28 |
+
label: "Japanese",
|
29 |
+
family: "asian",
|
30 |
+
color: "grayscale",
|
31 |
+
font: "komika",
|
32 |
+
llmPrompt: "japanese manga",
|
33 |
+
imagePrompt: (prompt: string) => [
|
34 |
+
`japanese manga about ${prompt}`,
|
35 |
+
"single panel",
|
36 |
+
"manga",
|
37 |
+
"japanese",
|
38 |
+
"grayscale",
|
39 |
+
"intricate",
|
40 |
+
"detailed",
|
41 |
+
"drawing"
|
42 |
+
],
|
43 |
+
negativePrompt: () => [
|
44 |
+
"franco-belgian comic",
|
45 |
+
"color album",
|
46 |
+
"color",
|
47 |
+
"american comic",
|
48 |
+
"photo",
|
49 |
+
"painting",
|
50 |
+
"3D render"
|
51 |
+
],
|
52 |
+
},
|
53 |
+
franco_belgian: {
|
54 |
+
label: "Franco-Belgian",
|
55 |
+
family: "european",
|
56 |
+
color: "color",
|
57 |
+
font: "paeteround",
|
58 |
+
llmPrompt: "Franco-Belgian comic (a \"bande dessinée\"), in the style of Franquin, Moebius etc",
|
59 |
+
imagePrompt: (prompt: string) => [
|
60 |
+
`franco-belgian color comic about ${prompt}`,
|
61 |
+
"bande dessinée",
|
62 |
+
"franco-belgian comic",
|
63 |
+
"comic album",
|
64 |
+
"color drawing"
|
65 |
+
],
|
66 |
+
negativePrompt: () => [
|
67 |
+
"manga",
|
68 |
+
"anime",
|
69 |
+
"american comic",
|
70 |
+
"grayscale",
|
71 |
+
"monochrome",
|
72 |
+
"photo",
|
73 |
+
"painting",
|
74 |
+
"3D render"
|
75 |
+
],
|
76 |
+
},
|
77 |
+
american_comic: {
|
78 |
+
label: "American",
|
79 |
+
family: "american",
|
80 |
+
color: "color",
|
81 |
+
font: "actionman",
|
82 |
+
llmPrompt: "american comic",
|
83 |
+
imagePrompt: (prompt: string) => [
|
84 |
+
`american comic about ${prompt}`,
|
85 |
+
"single panel",
|
86 |
+
"american comic",
|
87 |
+
"comicbook style",
|
88 |
+
"color comicbook",
|
89 |
+
"color drawing"
|
90 |
+
],
|
91 |
+
negativePrompt: () => [
|
92 |
+
"manga",
|
93 |
+
"anime",
|
94 |
+
"american comic",
|
95 |
+
"action",
|
96 |
+
"grayscale",
|
97 |
+
"monochrome",
|
98 |
+
"photo",
|
99 |
+
"painting",
|
100 |
+
"3D render"
|
101 |
+
],
|
102 |
+
},
|
103 |
+
sanglier: {
|
104 |
+
label: "Sanglier",
|
105 |
+
family: "european",
|
106 |
+
color: "monochrome",
|
107 |
+
font: "paeteround",
|
108 |
+
llmPrompt: "new color album",
|
109 |
+
imagePrompt: (prompt: string) => [
|
110 |
+
`color album panel`,
|
111 |
+
`about ${prompt}`,
|
112 |
+
"bande dessinée",
|
113 |
+
"single panel",
|
114 |
+
"comical",
|
115 |
+
"franco-belgian comic",
|
116 |
+
"comic album",
|
117 |
+
"color drawing"
|
118 |
+
],
|
119 |
+
negativePrompt: () => [
|
120 |
+
"manga",
|
121 |
+
"anime",
|
122 |
+
"american comic",
|
123 |
+
"grayscale",
|
124 |
+
"monochrome",
|
125 |
+
"photo",
|
126 |
+
"painting",
|
127 |
+
"3D render"
|
128 |
+
],
|
129 |
+
}
|
130 |
+
}
|
131 |
+
|
132 |
+
export type PresetName = keyof typeof presets
|
133 |
+
|
134 |
+
export const defaultPreset: PresetName = "japanese_manga"
|
135 |
+
|
136 |
+
export const getPreset = (preset?: PresetName): Preset => presets[preset || defaultPreset] || presets[defaultPreset]
|
src/app/engine/render.ts
ADDED
@@ -0,0 +1,132 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use server"
|
2 |
+
|
3 |
+
import { RenderRequest, RenderedScene } from "@/types"
|
4 |
+
|
5 |
+
// note: there is no / at the end in the variable
|
6 |
+
// so we have to add it ourselves if needed
|
7 |
+
const apiUrl = process.env.RENDERING_ENGINE_API
|
8 |
+
|
9 |
+
const cacheDurationInSec = 30 * 60 // 30 minutes
|
10 |
+
|
11 |
+
export async function newRender({
|
12 |
+
prompt,
|
13 |
+
// negativePrompt,
|
14 |
+
width,
|
15 |
+
height
|
16 |
+
}: {
|
17 |
+
prompt: string
|
18 |
+
// negativePrompt: string[]
|
19 |
+
width: number
|
20 |
+
height: number
|
21 |
+
}) {
|
22 |
+
console.log(`newRender(${prompt})`)
|
23 |
+
if (!prompt) {
|
24 |
+
console.error(`cannot call the rendering API without a prompt, aborting..`)
|
25 |
+
throw new Error(`cannot call the rendering API without a prompt, aborting..`)
|
26 |
+
}
|
27 |
+
|
28 |
+
let defaulResult: RenderedScene = {
|
29 |
+
renderId: "",
|
30 |
+
status: "error",
|
31 |
+
assetUrl: "",
|
32 |
+
maskUrl: "",
|
33 |
+
error: "failed to fetch the data",
|
34 |
+
segments: []
|
35 |
+
}
|
36 |
+
|
37 |
+
|
38 |
+
try {
|
39 |
+
console.log(`calling POST ${apiUrl}/render with prompt: ${prompt}`)
|
40 |
+
|
41 |
+
const res = await fetch(`${apiUrl}/render`, {
|
42 |
+
method: "POST",
|
43 |
+
headers: {
|
44 |
+
Accept: "application/json",
|
45 |
+
"Content-Type": "application/json",
|
46 |
+
// Authorization: `Bearer ${process.env.VC_SECRET_ACCESS_TOKEN}`,
|
47 |
+
},
|
48 |
+
body: JSON.stringify({
|
49 |
+
prompt,
|
50 |
+
// negativePrompt, unused for now
|
51 |
+
nbFrames: 1,
|
52 |
+
nbSteps: 20, // 20 = fast, 30 = better, 50 = best
|
53 |
+
actionnables: [],
|
54 |
+
segmentation: "disabled", // one day we will remove this param, to make it automatic
|
55 |
+
width,
|
56 |
+
height,
|
57 |
+
cache: "ignore"
|
58 |
+
} as Partial<RenderRequest>),
|
59 |
+
cache: 'no-store',
|
60 |
+
// we can also use this (see https://vercel.com/blog/vercel-cache-api-nextjs-cache)
|
61 |
+
// next: { revalidate: 1 }
|
62 |
+
})
|
63 |
+
|
64 |
+
// console.log("res:", res)
|
65 |
+
// The return value is *not* serialized
|
66 |
+
// You can return Date, Map, Set, etc.
|
67 |
+
|
68 |
+
// Recommendation: handle errors
|
69 |
+
if (res.status !== 200) {
|
70 |
+
// This will activate the closest `error.js` Error Boundary
|
71 |
+
throw new Error('Failed to fetch data')
|
72 |
+
}
|
73 |
+
|
74 |
+
const response = (await res.json()) as RenderedScene
|
75 |
+
|
76 |
+
return response
|
77 |
+
} catch (err) {
|
78 |
+
console.error(err)
|
79 |
+
return defaulResult
|
80 |
+
}
|
81 |
+
}
|
82 |
+
|
83 |
+
export async function getRender(renderId: string) {
|
84 |
+
if (!renderId) {
|
85 |
+
console.error(`cannot call the rendering API without a renderId, aborting..`)
|
86 |
+
throw new Error(`cannot call the rendering API without a renderId, aborting..`)
|
87 |
+
}
|
88 |
+
|
89 |
+
let defaulResult: RenderedScene = {
|
90 |
+
renderId: "",
|
91 |
+
status: "error",
|
92 |
+
assetUrl: "",
|
93 |
+
maskUrl: "",
|
94 |
+
error: "failed to fetch the data",
|
95 |
+
segments: []
|
96 |
+
}
|
97 |
+
|
98 |
+
try {
|
99 |
+
// console.log(`calling GET ${apiUrl}/render with renderId: ${renderId}`)
|
100 |
+
const res = await fetch(`${apiUrl}/render/${renderId}`, {
|
101 |
+
method: "GET",
|
102 |
+
headers: {
|
103 |
+
Accept: "application/json",
|
104 |
+
"Content-Type": "application/json",
|
105 |
+
// Authorization: `Bearer ${process.env.VC_SECRET_ACCESS_TOKEN}`,
|
106 |
+
},
|
107 |
+
cache: 'no-store',
|
108 |
+
// we can also use this (see https://vercel.com/blog/vercel-cache-api-nextjs-cache)
|
109 |
+
// next: { revalidate: 1 }
|
110 |
+
})
|
111 |
+
|
112 |
+
// console.log("res:", res)
|
113 |
+
// The return value is *not* serialized
|
114 |
+
// You can return Date, Map, Set, etc.
|
115 |
+
|
116 |
+
// Recommendation: handle errors
|
117 |
+
if (res.status !== 200) {
|
118 |
+
// This will activate the closest `error.js` Error Boundary
|
119 |
+
throw new Error('Failed to fetch data')
|
120 |
+
}
|
121 |
+
|
122 |
+
const response = (await res.json()) as RenderedScene
|
123 |
+
// console.log("response:", response)
|
124 |
+
return response
|
125 |
+
} catch (err) {
|
126 |
+
console.error(err)
|
127 |
+
// Gorgon.clear(cacheKey)
|
128 |
+
return defaulResult
|
129 |
+
}
|
130 |
+
|
131 |
+
// }, cacheDurationInSec * 1000)
|
132 |
+
}
|
src/app/favicon.ico
ADDED
|
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/interface/display/index.tsx
ADDED
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { RenderedScene } from "@/types"
|
2 |
+
|
3 |
+
export function Display ({ rendered }: { rendered: RenderedScene }) {
|
4 |
+
return (
|
5 |
+
<>
|
6 |
+
<img
|
7 |
+
src={rendered.assetUrl || undefined}
|
8 |
+
className="fixed w-screen top-0 left-0 right-0"
|
9 |
+
/>
|
10 |
+
</>
|
11 |
+
)
|
12 |
+
}
|
src/app/interface/panel/bubble.tsx
ADDED
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { ReactNode } from "react"
|
2 |
+
|
3 |
+
import { cn } from "@/lib/utils"
|
4 |
+
|
5 |
+
export function Bubble({
|
6 |
+
children,
|
7 |
+
className
|
8 |
+
}: {
|
9 |
+
children?: ReactNode
|
10 |
+
className?: string
|
11 |
+
}) {
|
12 |
+
|
13 |
+
if (!children) {
|
14 |
+
return null
|
15 |
+
}
|
16 |
+
|
17 |
+
return (
|
18 |
+
<div>
|
19 |
+
<div className={cn(
|
20 |
+
`relative w-[300px] p-6 rounded-[40px]`,
|
21 |
+
`bg-white`,
|
22 |
+
`text-lg leading-6 text-center text-zinc-800`,
|
23 |
+
|
24 |
+
// BEFORE ELEMENT
|
25 |
+
`before:content-[""] before:w-0 before:h-0 before:absolute`,
|
26 |
+
`before:border-l-[24px] before:border-l-white`,
|
27 |
+
`before:border-r-[12px] before:border-r-transparent`,
|
28 |
+
`before:border-t-[12px] before:border-t-white`,
|
29 |
+
`before:border-b-[20px] before:border-b-transparent`,
|
30 |
+
`before:border-solid before:left-8 before:-bottom-6`,
|
31 |
+
// `before:border-radius`,
|
32 |
+
`shadow-lg`,
|
33 |
+
className
|
34 |
+
)}>
|
35 |
+
<div
|
36 |
+
className={cn(
|
37 |
+
``
|
38 |
+
)}
|
39 |
+
>
|
40 |
+
{children}
|
41 |
+
</div>
|
42 |
+
</div>
|
43 |
+
</div>
|
44 |
+
)
|
45 |
+
}
|
src/app/interface/panel/index.tsx
ADDED
@@ -0,0 +1,139 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
|
3 |
+
import { useEffect, useRef, useState, useTransition } from "react"
|
4 |
+
|
5 |
+
import { Preset } from "@/app/engine/presets"
|
6 |
+
|
7 |
+
import { RenderedScene } from "@/types"
|
8 |
+
import { cn } from "@/lib/utils"
|
9 |
+
import { FontName } from "@/lib/fonts"
|
10 |
+
import { getRender, newRender } from "../../engine/render"
|
11 |
+
import { getInitialRenderedScene } from "@/lib/getInitialRenderedScene"
|
12 |
+
// import { Bubble } from "./bubble"
|
13 |
+
|
14 |
+
export default function Panel({
|
15 |
+
prompt = "",
|
16 |
+
font,
|
17 |
+
preset,
|
18 |
+
className = "",
|
19 |
+
width = 1,
|
20 |
+
height = 1,
|
21 |
+
delay = 0,
|
22 |
+
}: {
|
23 |
+
prompt?: string
|
24 |
+
font: FontName
|
25 |
+
preset: Preset
|
26 |
+
className?: string
|
27 |
+
width?: number
|
28 |
+
height?: number
|
29 |
+
delay?: number
|
30 |
+
}) {
|
31 |
+
|
32 |
+
const [_isPending, startTransition] = useTransition()
|
33 |
+
const [isLoading, setLoading] = useState<boolean>(false)
|
34 |
+
const [rendered, setRendered] = useState<RenderedScene>(getInitialRenderedScene())
|
35 |
+
const renderedRef = useRef<RenderedScene>()
|
36 |
+
|
37 |
+
const timeoutRef = useRef<any>(null)
|
38 |
+
|
39 |
+
// since this run in its own loop, we need to use references everywhere
|
40 |
+
// but perhaps this could be refactored
|
41 |
+
useEffect(() => {
|
42 |
+
startTransition(async () => {
|
43 |
+
// console.log("Panel prompt: "+ prompt)
|
44 |
+
if (!prompt?.length) { return }
|
45 |
+
|
46 |
+
console.log("Loading panel..")
|
47 |
+
setLoading(true)
|
48 |
+
|
49 |
+
// console.log("calling:\nconst newRendered = await newRender({ prompt, preset, width, height })")
|
50 |
+
console.log({
|
51 |
+
prompt, preset, width, height
|
52 |
+
})
|
53 |
+
|
54 |
+
const newRendered = await newRender({ prompt, width, height })
|
55 |
+
|
56 |
+
if (newRendered) {
|
57 |
+
// console.log("newRendered:", newRendered)
|
58 |
+
setRendered(renderedRef.current = newRendered)
|
59 |
+
|
60 |
+
// but we are still loading!
|
61 |
+
} else {
|
62 |
+
setRendered(renderedRef.current = {
|
63 |
+
renderId: "",
|
64 |
+
status: "error",
|
65 |
+
assetUrl: "",
|
66 |
+
maskUrl: "",
|
67 |
+
error: "failed to fetch the data",
|
68 |
+
segments: []
|
69 |
+
})
|
70 |
+
|
71 |
+
setLoading(false)
|
72 |
+
return
|
73 |
+
}
|
74 |
+
})
|
75 |
+
}, [prompt, font, preset])
|
76 |
+
|
77 |
+
|
78 |
+
const checkStatus = () => {
|
79 |
+
startTransition(async () => {
|
80 |
+
clearTimeout(timeoutRef.current)
|
81 |
+
|
82 |
+
if (!renderedRef.current?.renderId || renderedRef.current?.status !== "pending") {
|
83 |
+
timeoutRef.current = setTimeout(checkStatus, 1000)
|
84 |
+
return
|
85 |
+
}
|
86 |
+
try {
|
87 |
+
// console.log(`Checking job status API for job ${renderedRef.current?.renderId}`)
|
88 |
+
const newRendered = await getRender(renderedRef.current.renderId)
|
89 |
+
// console.log("got a response!", newRendered)
|
90 |
+
|
91 |
+
if (JSON.stringify(renderedRef.current) !== JSON.stringify(newRendered)) {
|
92 |
+
console.log("updated panel:", newRendered)
|
93 |
+
setRendered(renderedRef.current = newRendered)
|
94 |
+
setLoading(true)
|
95 |
+
}
|
96 |
+
// console.log("status:", newRendered.status)
|
97 |
+
|
98 |
+
if (newRendered.status === "pending") {
|
99 |
+
// console.log("job not finished")
|
100 |
+
timeoutRef.current = setTimeout(checkStatus, 1000)
|
101 |
+
} else {
|
102 |
+
console.log("panel finished!")
|
103 |
+
setLoading(false)
|
104 |
+
}
|
105 |
+
} catch (err) {
|
106 |
+
console.error(err)
|
107 |
+
timeoutRef.current = setTimeout(checkStatus, 1000)
|
108 |
+
}
|
109 |
+
})
|
110 |
+
}
|
111 |
+
|
112 |
+
useEffect(() => {
|
113 |
+
console.log("starting timeout")
|
114 |
+
// normally it should reply in < 1sec, but we could also use an interval
|
115 |
+
timeoutRef.current = setTimeout(checkStatus, delay)
|
116 |
+
|
117 |
+
return () => {
|
118 |
+
clearTimeout(timeoutRef.current)
|
119 |
+
}
|
120 |
+
}, [])
|
121 |
+
|
122 |
+
return (
|
123 |
+
<div className={cn(
|
124 |
+
`w-full h-full`,
|
125 |
+
preset.color === "grayscale" ? "grayscale" : "",
|
126 |
+
className
|
127 |
+
)}>
|
128 |
+
{rendered.assetUrl ? <img
|
129 |
+
src={rendered.assetUrl}
|
130 |
+
className="w-full h-full object-cover"
|
131 |
+
/> : null}
|
132 |
+
|
133 |
+
{/*<Bubble className="absolute top-4 left-4">
|
134 |
+
Hello, world!
|
135 |
+
</Bubble>
|
136 |
+
*/}
|
137 |
+
</div>
|
138 |
+
)
|
139 |
+
}
|
src/app/interface/progress/index.tsx
ADDED
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { useEffect, useRef, useState } from "react"
|
2 |
+
|
3 |
+
import { ProgressBar } from "./progress-bar"
|
4 |
+
import { cn } from "@/lib/utils"
|
5 |
+
|
6 |
+
export function Progress({
|
7 |
+
isLoading,
|
8 |
+
resetKey = "", // when this key change, this will re-spawn the progress bar
|
9 |
+
className = "",
|
10 |
+
}: {
|
11 |
+
isLoading: boolean
|
12 |
+
resetKey: string
|
13 |
+
className?: string
|
14 |
+
}) {
|
15 |
+
const timeoutRef = useRef<any>()
|
16 |
+
const [progressPercent, setProcessPercent] = useState(0)
|
17 |
+
const progressRef = useRef(0)
|
18 |
+
const isLoadingRef = useRef(isLoading)
|
19 |
+
|
20 |
+
const updateProgressBar = () => {
|
21 |
+
const duration = 1000 // 1 sec
|
22 |
+
const frequency = 200 // 200ms
|
23 |
+
const nbUpdatesPerSec = duration / frequency // 5x per second
|
24 |
+
|
25 |
+
// normally it takes 45, and we will try to go below,
|
26 |
+
// but to be safe let's set the counter a 1 min
|
27 |
+
const nbSeconds = 32 // 1 min
|
28 |
+
const amountInPercent = 100 / (nbUpdatesPerSec * nbSeconds) // 0.333
|
29 |
+
|
30 |
+
progressRef.current = Math.min(100, progressRef.current + amountInPercent)
|
31 |
+
setProcessPercent(progressRef.current)
|
32 |
+
}
|
33 |
+
|
34 |
+
useEffect(() => {
|
35 |
+
clearInterval(timeoutRef.current)
|
36 |
+
isLoadingRef.current = isLoading
|
37 |
+
progressRef.current = 0
|
38 |
+
setProcessPercent(0)
|
39 |
+
if (isLoading) {
|
40 |
+
timeoutRef.current = setInterval(updateProgressBar, 200)
|
41 |
+
}
|
42 |
+
}, [isLoading, resetKey])
|
43 |
+
|
44 |
+
return (
|
45 |
+
<div className={cn(
|
46 |
+
`fixed flex w-10 h-10 top-16 right-6 z-50`,
|
47 |
+
`animation-all duration-300 text-md`,
|
48 |
+
isLoading
|
49 |
+
? `scale-100 opacity-100`
|
50 |
+
: `scale-0 opacity-0`,
|
51 |
+
className
|
52 |
+
)}>
|
53 |
+
<ProgressBar progressPercentage={progressPercent} />
|
54 |
+
</div>
|
55 |
+
)
|
56 |
+
}
|
src/app/interface/progress/progress-bar.tsx
ADDED
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
|
3 |
+
import { CircularProgressbar, buildStyles } from "react-circular-progressbar"
|
4 |
+
import "react-circular-progressbar/dist/styles.css"
|
5 |
+
|
6 |
+
export function ProgressBar ({
|
7 |
+
className,
|
8 |
+
progressPercentage,
|
9 |
+
text
|
10 |
+
}: {
|
11 |
+
className?: string
|
12 |
+
progressPercentage?: number
|
13 |
+
text?: string
|
14 |
+
}) {
|
15 |
+
return (
|
16 |
+
<div className={className}>
|
17 |
+
<CircularProgressbar
|
18 |
+
// doc: https://www.npmjs.com/package/react-circular-progressbar
|
19 |
+
|
20 |
+
value={progressPercentage || 0}
|
21 |
+
|
22 |
+
// Text to display inside progressbar. Default: ''.
|
23 |
+
text={text || ""}
|
24 |
+
|
25 |
+
// Width of circular line relative to total width of component, a value from 0-100. Default: 8.
|
26 |
+
strokeWidth={8}
|
27 |
+
|
28 |
+
|
29 |
+
// As a convenience, you can use buildStyles to configure the most common style changes:
|
30 |
+
|
31 |
+
styles={buildStyles({
|
32 |
+
// Rotation of path and trail, in number of turns (0-1)
|
33 |
+
rotation: 0,
|
34 |
+
|
35 |
+
// Whether to use rounded or flat corners on the ends - can use 'butt' or 'round'
|
36 |
+
strokeLinecap: 'round',
|
37 |
+
|
38 |
+
// Text size
|
39 |
+
textSize: '20px',
|
40 |
+
|
41 |
+
// How long animation takes to go from one percentage to another, in seconds
|
42 |
+
pathTransitionDuration: 0.1,
|
43 |
+
|
44 |
+
// Can specify path transition in more detail, or remove it entirely
|
45 |
+
// pathTransition: 'none',
|
46 |
+
|
47 |
+
// Colors
|
48 |
+
// pathColor: `rgba(62, 152, 199, ${percentage / 100})`,
|
49 |
+
textColor: '#f88',
|
50 |
+
trailColor: '#d6d6d6',
|
51 |
+
backgroundColor: '#3e98c7',
|
52 |
+
})}
|
53 |
+
|
54 |
+
/>
|
55 |
+
</div>
|
56 |
+
)
|
57 |
+
}
|
src/app/interface/top-menu/index.tsx
ADDED
@@ -0,0 +1,103 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
|
3 |
+
import {
|
4 |
+
Select,
|
5 |
+
SelectContent,
|
6 |
+
SelectItem,
|
7 |
+
SelectTrigger,
|
8 |
+
SelectValue,
|
9 |
+
} from "@/components/ui/select"
|
10 |
+
import { Label } from "@/components/ui/label"
|
11 |
+
import { cn } from "@/lib/utils"
|
12 |
+
import { FontName, fontList, fonts } from "@/lib/fonts"
|
13 |
+
import { Input } from "@/components/ui/input"
|
14 |
+
import { Preset, PresetName, presets } from "@/app/engine/presets"
|
15 |
+
import { useState } from "react"
|
16 |
+
|
17 |
+
export function TopMenu({
|
18 |
+
defaultPreset,
|
19 |
+
preset,
|
20 |
+
onChangePreset,
|
21 |
+
font,
|
22 |
+
onChangeFont,
|
23 |
+
prompt,
|
24 |
+
onChangePrompt,
|
25 |
+
}: {
|
26 |
+
defaultPreset: PresetName
|
27 |
+
preset: Preset
|
28 |
+
onChangePreset: (newPresetName: PresetName) => void
|
29 |
+
font: FontName
|
30 |
+
onChangeFont: (newFontName: FontName) => void
|
31 |
+
prompt: string
|
32 |
+
onChangePrompt: (newPrompt: string) => void
|
33 |
+
}) {
|
34 |
+
const [draft, setDraft] = useState("")
|
35 |
+
return (
|
36 |
+
<div className={cn(
|
37 |
+
`z-10 fixed top-0 left-0 right-0`,
|
38 |
+
`flex flex-row w-full justify-between items-center`,
|
39 |
+
`backdrop-blur-xl`,
|
40 |
+
`px-2 py-2 border-b-1 border-gray-50 dark:border-gray-50`,
|
41 |
+
`bg-stone-900/70 dark:bg-stone-900/70 text-gray-50 dark:text-gray-50`,
|
42 |
+
`space-x-6`
|
43 |
+
)}>
|
44 |
+
<div className="flex flex-row items-center space-x-3 font-mono">
|
45 |
+
<Label className="flex text-sm">Select a preset:</Label>
|
46 |
+
<Select
|
47 |
+
defaultValue={defaultPreset}
|
48 |
+
onValueChange={(value) => { onChangePreset(value as FontName) }}>
|
49 |
+
<SelectTrigger className="w-[180px]">
|
50 |
+
<SelectValue className="text-sm" placeholder="Type" />
|
51 |
+
</SelectTrigger>
|
52 |
+
<SelectContent>
|
53 |
+
{Object.entries(presets).map(([key, preset]) =>
|
54 |
+
<SelectItem key={key} value={key}>{preset.label}</SelectItem>
|
55 |
+
)}
|
56 |
+
</SelectContent>
|
57 |
+
</Select>
|
58 |
+
</div>
|
59 |
+
<div className="flex flex-row flex-grow items-center space-x-3 font-mono">
|
60 |
+
<Input
|
61 |
+
placeholder="Story"
|
62 |
+
className="w-full bg-neutral-300 text-neutral-800 dark:bg-neutral-300 dark:text-neutral-800"
|
63 |
+
onChange={(e) => {
|
64 |
+
setDraft(e.target.value)
|
65 |
+
}}
|
66 |
+
onBlur={(e) => {
|
67 |
+
if (draft !== prompt) {
|
68 |
+
onChangePrompt(draft)
|
69 |
+
}
|
70 |
+
}}
|
71 |
+
onKeyDown={({ key }) => {
|
72 |
+
if (key === 'Enter') {
|
73 |
+
if (draft.trim() !== prompt.trim()) {
|
74 |
+
onChangePrompt(draft.trim())
|
75 |
+
}
|
76 |
+
}
|
77 |
+
}}
|
78 |
+
value={draft}
|
79 |
+
/>
|
80 |
+
</div>
|
81 |
+
<div className="flex flex-row items-center space-x-3 font-mono">
|
82 |
+
<Label className="flex text-sm">Font:</Label>
|
83 |
+
<Select
|
84 |
+
defaultValue={fontList.includes(preset.font) ? preset.font : "cartoonist"}
|
85 |
+
onValueChange={(value) => { onChangeFont(value as FontName) }}>
|
86 |
+
<SelectTrigger className="w-[144px]">
|
87 |
+
<SelectValue className="text-sm" placeholder="Type" />
|
88 |
+
</SelectTrigger>
|
89 |
+
<SelectContent>
|
90 |
+
{Object.keys(fonts)
|
91 |
+
.map((font) =>
|
92 |
+
<SelectItem
|
93 |
+
key={font}
|
94 |
+
value={font}>{
|
95 |
+
font
|
96 |
+
}</SelectItem>
|
97 |
+
)}
|
98 |
+
</SelectContent>
|
99 |
+
</Select>
|
100 |
+
</div>
|
101 |
+
</div>
|
102 |
+
)
|
103 |
+
}
|
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: 'Comic Factory: generate your own comics!',
|
9 |
+
description: 'Comic Factory: generate your own comits!',
|
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/main.tsx
ADDED
@@ -0,0 +1,112 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
|
3 |
+
import { useState, useTransition } from "react"
|
4 |
+
import { usePathname, useRouter, useSearchParams } from "next/navigation"
|
5 |
+
|
6 |
+
import "react-virtualized/styles.css" // only needs to be imported once
|
7 |
+
|
8 |
+
import { Preset, PresetName, defaultPreset, getPreset } from "@/app/engine/presets"
|
9 |
+
|
10 |
+
import { cn } from "@/lib/utils"
|
11 |
+
import { TopMenu } from "./interface/top-menu"
|
12 |
+
import { FontName } from "@/lib/fonts"
|
13 |
+
import Panel from "./interface/panel"
|
14 |
+
|
15 |
+
export default function Main() {
|
16 |
+
const [_isPending, startTransition] = useTransition()
|
17 |
+
const [isLoading, setLoading] = useState<boolean>()
|
18 |
+
|
19 |
+
const router = useRouter()
|
20 |
+
const pathname = usePathname()
|
21 |
+
const searchParams = useSearchParams()
|
22 |
+
|
23 |
+
const requestedPreset = (searchParams.get('preset') as PresetName) || defaultPreset
|
24 |
+
const [preset, setPreset] = useState<Preset>(getPreset(requestedPreset))
|
25 |
+
|
26 |
+
const [font, setFont] = useState<FontName>("cartoonist")
|
27 |
+
|
28 |
+
const requestedPrompt = (searchParams.get('prompt') as string) || ""
|
29 |
+
const [prompt, setPrompt] = useState<string>(requestedPrompt)
|
30 |
+
const [panelPrompts, setPanelPrompts] = useState<string[]>([])
|
31 |
+
|
32 |
+
const handleChangeFont = (newFont: FontName) => {
|
33 |
+
setFont(newFont)
|
34 |
+
}
|
35 |
+
|
36 |
+
const handleChangePreset = (newPresetName: PresetName) => {
|
37 |
+
setPreset(getPreset(newPresetName))
|
38 |
+
}
|
39 |
+
|
40 |
+
const handleChangePrompt = (newPrompt: string) => {
|
41 |
+
setPrompt(newPrompt)
|
42 |
+
|
43 |
+
// TODO call the LLM here!
|
44 |
+
const prompt = preset.imagePrompt(newPrompt).join(", ")
|
45 |
+
|
46 |
+
setPanelPrompts([
|
47 |
+
prompt,
|
48 |
+
prompt,
|
49 |
+
prompt,
|
50 |
+
prompt
|
51 |
+
])
|
52 |
+
}
|
53 |
+
|
54 |
+
return (
|
55 |
+
<div className={cn(
|
56 |
+
``
|
57 |
+
)}>
|
58 |
+
<TopMenu
|
59 |
+
defaultPreset={defaultPreset}
|
60 |
+
preset={preset}
|
61 |
+
onChangePreset={handleChangePreset}
|
62 |
+
font={font}
|
63 |
+
onChangeFont={handleChangeFont}
|
64 |
+
prompt={prompt}
|
65 |
+
onChangePrompt={handleChangePrompt}
|
66 |
+
/>
|
67 |
+
<div className="flex flex-col items-center w-screen h-screen pt-16 overflow-y-scroll">
|
68 |
+
<div
|
69 |
+
// the "fixed" width ensure our comic keeps a consistent ratio
|
70 |
+
className="grid grid-cols-2 grid-rows-3 gap-4 w-[1160px] h-screen">
|
71 |
+
<div className="bg-stone-100">
|
72 |
+
<Panel
|
73 |
+
prompt={panelPrompts[0]}
|
74 |
+
font={font}
|
75 |
+
preset={preset}
|
76 |
+
width={1024}
|
77 |
+
height={512}
|
78 |
+
/>
|
79 |
+
</div>
|
80 |
+
<div className="bg-zinc-100 row-span-2">
|
81 |
+
<Panel
|
82 |
+
prompt={panelPrompts[1]}
|
83 |
+
font={font}
|
84 |
+
preset={preset}
|
85 |
+
width={1024}
|
86 |
+
height={1024}
|
87 |
+
/>
|
88 |
+
</div>
|
89 |
+
<div className="bg-gray-100 row-span-2 col-span-1">
|
90 |
+
<Panel
|
91 |
+
prompt={panelPrompts[2]}
|
92 |
+
font={font}
|
93 |
+
preset={preset}
|
94 |
+
width={1024}
|
95 |
+
height={1024}
|
96 |
+
/>
|
97 |
+
</div>
|
98 |
+
<div className="bg-slate-100">
|
99 |
+
<Panel
|
100 |
+
prompt={panelPrompts[3]}
|
101 |
+
font={font}
|
102 |
+
preset={preset}
|
103 |
+
width={1024}
|
104 |
+
height={512}
|
105 |
+
/>
|
106 |
+
</div>
|
107 |
+
</div>
|
108 |
+
</div>
|
109 |
+
</div>
|
110 |
+
|
111 |
+
)
|
112 |
+
}
|
src/app/page.tsx
ADDED
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use server"
|
2 |
+
|
3 |
+
import Head from "next/head"
|
4 |
+
|
5 |
+
import Main from "./main"
|
6 |
+
import { TooltipProvider } from "@/components/ui/tooltip"
|
7 |
+
|
8 |
+
// https://nextjs.org/docs/pages/building-your-application/optimizing/fonts
|
9 |
+
|
10 |
+
export default async function IndexPage({ params: { ownerId } }: { params: { ownerId: string }}) {
|
11 |
+
return (
|
12 |
+
<>
|
13 |
+
<Head>
|
14 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
|
15 |
+
<link rel="preconnect" href="https://fonts.googleapis.com" crossOrigin="anonymous" />
|
16 |
+
<meta name="viewport" content="width=device-width, initial-scale=0.86, maximum-scale=5.0, minimum-scale=0.86" />
|
17 |
+
</Head>
|
18 |
+
<main className={
|
19 |
+
`light fixed inset-0 w-screen h-screen flex flex-col items-center
|
20 |
+
bg-zinc-50 text-stone-900 overflow-y-scroll
|
21 |
+
`}>
|
22 |
+
<TooltipProvider delayDuration={100}>
|
23 |
+
<Main />
|
24 |
+
</TooltipProvider>
|
25 |
+
</main>
|
26 |
+
</>
|
27 |
+
)
|
28 |
+
}
|
src/app/queries/getBackground.ts
ADDED
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { createLlamaPrompt } from "@/lib/createLlamaPrompt"
|
2 |
+
|
3 |
+
import { predict } from "./predict"
|
4 |
+
import { Preset } from "../engine/presets"
|
5 |
+
|
6 |
+
export const getBackground = async ({
|
7 |
+
preset,
|
8 |
+
storyPrompt = "",
|
9 |
+
previousPanelPrompt = "",
|
10 |
+
newPanelPrompt = "",
|
11 |
+
}: {
|
12 |
+
preset: Preset;
|
13 |
+
storyPrompt: string;
|
14 |
+
previousPanelPrompt: string;
|
15 |
+
newPanelPrompt: string;
|
16 |
+
}) => {
|
17 |
+
|
18 |
+
const prompt = createLlamaPrompt([
|
19 |
+
{
|
20 |
+
role: "system",
|
21 |
+
content: [
|
22 |
+
`You are a comic book author for a ${preset.llmPrompt}`,
|
23 |
+
`Please write in a single sentence a photo caption for the next plausible page, using a few words for each of those categories: the environment, era, characters, objects, textures, lighting.`,
|
24 |
+
`Separate each of those category descriptions using a comma.`,
|
25 |
+
`Be brief in your caption don't add your own comments. Be straight to the point, and never reply things like "As the player approaches.." or "As the player clicks.." or "the scene shifts to.." (the best is not not mention the player at all)`
|
26 |
+
].filter(item => item).join("\n")
|
27 |
+
},
|
28 |
+
{
|
29 |
+
role: "user",
|
30 |
+
content: storyPrompt
|
31 |
+
}
|
32 |
+
])
|
33 |
+
|
34 |
+
|
35 |
+
let result = ""
|
36 |
+
try {
|
37 |
+
result = await predict(prompt)
|
38 |
+
if (!result.trim().length) {
|
39 |
+
throw new Error("empty result!")
|
40 |
+
}
|
41 |
+
} catch (err) {
|
42 |
+
console.log(`prediction of the background failed, trying again..`)
|
43 |
+
try {
|
44 |
+
result = await predict(prompt+".")
|
45 |
+
if (!result.trim().length) {
|
46 |
+
throw new Error("empty result!")
|
47 |
+
}
|
48 |
+
} catch (err) {
|
49 |
+
console.error(`prediction of the background failed again!`)
|
50 |
+
throw new Error(`failed to generate the background ${err}`)
|
51 |
+
}
|
52 |
+
}
|
53 |
+
|
54 |
+
const tmp = result.split("Caption:").pop() || result
|
55 |
+
return tmp.replaceAll("\n", ", ")
|
56 |
+
}
|
src/app/queries/predict.ts
ADDED
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use server"
|
2 |
+
|
3 |
+
import { HfInference } from "@huggingface/inference"
|
4 |
+
|
5 |
+
const hfi = new HfInference(process.env.HF_API_TOKEN)
|
6 |
+
const hf = hfi.endpoint(`${process.env.HF_INFERENCE_ENDPOINT_URL || ""}`)
|
7 |
+
|
8 |
+
export async function predict(inputs: string) {
|
9 |
+
|
10 |
+
console.log(`predict: `, inputs)
|
11 |
+
|
12 |
+
let instructions = ""
|
13 |
+
try {
|
14 |
+
for await (const output of hf.textGenerationStream({
|
15 |
+
inputs,
|
16 |
+
parameters: {
|
17 |
+
do_sample: true,
|
18 |
+
|
19 |
+
// hard limit for max_new_tokens is 1512
|
20 |
+
max_new_tokens: 200, // 1150,
|
21 |
+
return_full_text: false,
|
22 |
+
}
|
23 |
+
})) {
|
24 |
+
instructions += output.token.text
|
25 |
+
process.stdout.write(output.token.text)
|
26 |
+
if (
|
27 |
+
instructions.includes("</s>") ||
|
28 |
+
instructions.includes("<s>") ||
|
29 |
+
instructions.includes("[INST]") ||
|
30 |
+
instructions.includes("[/INST]") ||
|
31 |
+
instructions.includes("<SYS>") ||
|
32 |
+
instructions.includes("</SYS>") ||
|
33 |
+
instructions.includes("<|end|>") ||
|
34 |
+
instructions.includes("<|assistant|>")
|
35 |
+
) {
|
36 |
+
break
|
37 |
+
}
|
38 |
+
}
|
39 |
+
} catch (err) {
|
40 |
+
console.error(`error during generation: ${err}`)
|
41 |
+
}
|
42 |
+
|
43 |
+
// need to do some cleanup of the garbage the LLM might have gave us
|
44 |
+
return (
|
45 |
+
instructions
|
46 |
+
.replaceAll("<|end|>", "")
|
47 |
+
.replaceAll("<s>", "")
|
48 |
+
.replaceAll("</s>", "")
|
49 |
+
.replaceAll("[INST]", "")
|
50 |
+
.replaceAll("[/INST]", "")
|
51 |
+
.replaceAll("<SYS>", "")
|
52 |
+
.replaceAll("</SYS>", "")
|
53 |
+
.replaceAll("<|assistant|>", "")
|
54 |
+
.replaceAll('""', '"')
|
55 |
+
)
|
56 |
+
}
|
src/components/icons/full-screen.tsx
ADDED
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
export function FullScreenIcon() {
|
2 |
+
return (
|
3 |
+
<svg version="1.1" viewBox="0 0 14 14" width="24px" height="24px" xmlns="http://www.w3.org/2000/svg">
|
4 |
+
<title/>
|
5 |
+
<desc/>
|
6 |
+
<defs/>
|
7 |
+
<g fill="none" fill-rule="evenodd" id="Page-1" stroke="none" stroke-width="1">
|
8 |
+
<g fill="currentColor" id="Core" transform="translate(-215.000000, -257.000000)">
|
9 |
+
<g id="fullscreen" transform="translate(215.000000, 257.000000)">
|
10 |
+
<path d="M2,9 L0,9 L0,14 L5,14 L5,12 L2,12 L2,9 L2,9 Z M0,5 L2,5 L2,2 L5,2 L5,0 L0,0 L0,5 L0,5 Z M12,12 L9,12 L9,14 L14,14 L14,9 L12,9 L12,12 L12,12 Z M9,0 L9,2 L12,2 L12,5 L14,5 L14,0 L9,0 L9,0 Z" id="Shape"/>
|
11 |
+
</g>
|
12 |
+
</g>
|
13 |
+
</g>
|
14 |
+
</svg>
|
15 |
+
)
|
16 |
+
}
|
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/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/label.tsx
ADDED
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
|
3 |
+
import * as React from "react"
|
4 |
+
import * as LabelPrimitive from "@radix-ui/react-label"
|
5 |
+
import { cva, type VariantProps } from "class-variance-authority"
|
6 |
+
|
7 |
+
import { cn } from "@/lib/utils"
|
8 |
+
|
9 |
+
const labelVariants = cva(
|
10 |
+
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
11 |
+
)
|
12 |
+
|
13 |
+
const Label = React.forwardRef<
|
14 |
+
React.ElementRef<typeof LabelPrimitive.Root>,
|
15 |
+
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
|
16 |
+
VariantProps<typeof labelVariants>
|
17 |
+
>(({ className, ...props }, ref) => (
|
18 |
+
<LabelPrimitive.Root
|
19 |
+
ref={ref}
|
20 |
+
className={cn(labelVariants(), className)}
|
21 |
+
{...props}
|
22 |
+
/>
|
23 |
+
))
|
24 |
+
Label.displayName = LabelPrimitive.Root.displayName
|
25 |
+
|
26 |
+
export { Label }
|
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 }
|
src/components/ui/select.tsx
ADDED
@@ -0,0 +1,121 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
|
3 |
+
import * as React from "react"
|
4 |
+
import * as SelectPrimitive from "@radix-ui/react-select"
|
5 |
+
import { Check, ChevronDown } from "lucide-react"
|
6 |
+
|
7 |
+
import { cn } from "@/lib/utils"
|
8 |
+
|
9 |
+
const Select = SelectPrimitive.Root
|
10 |
+
|
11 |
+
const SelectGroup = SelectPrimitive.Group
|
12 |
+
|
13 |
+
const SelectValue = SelectPrimitive.Value
|
14 |
+
|
15 |
+
const SelectTrigger = React.forwardRef<
|
16 |
+
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
17 |
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
18 |
+
>(({ className, children, ...props }, ref) => (
|
19 |
+
<SelectPrimitive.Trigger
|
20 |
+
ref={ref}
|
21 |
+
className={cn(
|
22 |
+
"flex h-10 w-full items-center justify-between rounded-md border border-stone-200 border-stone-200 bg-transparent px-3 py-2 text-sm ring-offset-white placeholder:text-stone-500 focus:outline-none focus:ring-2 focus:ring-stone-400 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-stone-800 dark:border-stone-800 dark:ring-offset-stone-950 dark:placeholder:text-stone-400 dark:focus:ring-stone-800",
|
23 |
+
className
|
24 |
+
)}
|
25 |
+
{...props}
|
26 |
+
>
|
27 |
+
{children}
|
28 |
+
<SelectPrimitive.Icon asChild>
|
29 |
+
<ChevronDown className="h-4 w-4 opacity-50" />
|
30 |
+
</SelectPrimitive.Icon>
|
31 |
+
</SelectPrimitive.Trigger>
|
32 |
+
))
|
33 |
+
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
|
34 |
+
|
35 |
+
const SelectContent = React.forwardRef<
|
36 |
+
React.ElementRef<typeof SelectPrimitive.Content>,
|
37 |
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
38 |
+
>(({ className, children, position = "popper", ...props }, ref) => (
|
39 |
+
<SelectPrimitive.Portal>
|
40 |
+
<SelectPrimitive.Content
|
41 |
+
ref={ref}
|
42 |
+
className={cn(
|
43 |
+
"relative z-50 min-w-[8rem] overflow-hidden rounded-md border border-stone-200 bg-white 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",
|
44 |
+
position === "popper" &&
|
45 |
+
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
46 |
+
className
|
47 |
+
)}
|
48 |
+
position={position}
|
49 |
+
{...props}
|
50 |
+
>
|
51 |
+
<SelectPrimitive.Viewport
|
52 |
+
className={cn(
|
53 |
+
"p-1",
|
54 |
+
position === "popper" &&
|
55 |
+
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
|
56 |
+
)}
|
57 |
+
>
|
58 |
+
{children}
|
59 |
+
</SelectPrimitive.Viewport>
|
60 |
+
</SelectPrimitive.Content>
|
61 |
+
</SelectPrimitive.Portal>
|
62 |
+
))
|
63 |
+
SelectContent.displayName = SelectPrimitive.Content.displayName
|
64 |
+
|
65 |
+
const SelectLabel = React.forwardRef<
|
66 |
+
React.ElementRef<typeof SelectPrimitive.Label>,
|
67 |
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
68 |
+
>(({ className, ...props }, ref) => (
|
69 |
+
<SelectPrimitive.Label
|
70 |
+
ref={ref}
|
71 |
+
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
|
72 |
+
{...props}
|
73 |
+
/>
|
74 |
+
))
|
75 |
+
SelectLabel.displayName = SelectPrimitive.Label.displayName
|
76 |
+
|
77 |
+
const SelectItem = React.forwardRef<
|
78 |
+
React.ElementRef<typeof SelectPrimitive.Item>,
|
79 |
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
80 |
+
>(({ className, children, ...props }, ref) => (
|
81 |
+
<SelectPrimitive.Item
|
82 |
+
ref={ref}
|
83 |
+
className={cn(
|
84 |
+
"relative flex w-full 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",
|
85 |
+
className
|
86 |
+
)}
|
87 |
+
{...props}
|
88 |
+
>
|
89 |
+
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
90 |
+
<SelectPrimitive.ItemIndicator>
|
91 |
+
<Check className="h-4 w-4" />
|
92 |
+
</SelectPrimitive.ItemIndicator>
|
93 |
+
</span>
|
94 |
+
|
95 |
+
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
96 |
+
</SelectPrimitive.Item>
|
97 |
+
))
|
98 |
+
SelectItem.displayName = SelectPrimitive.Item.displayName
|
99 |
+
|
100 |
+
const SelectSeparator = React.forwardRef<
|
101 |
+
React.ElementRef<typeof SelectPrimitive.Separator>,
|
102 |
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
103 |
+
>(({ className, ...props }, ref) => (
|
104 |
+
<SelectPrimitive.Separator
|
105 |
+
ref={ref}
|
106 |
+
className={cn("-mx-1 my-1 h-px bg-stone-100 dark:bg-stone-800", className)}
|
107 |
+
{...props}
|
108 |
+
/>
|
109 |
+
))
|
110 |
+
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
|
111 |
+
|
112 |
+
export {
|
113 |
+
Select,
|
114 |
+
SelectGroup,
|
115 |
+
SelectValue,
|
116 |
+
SelectTrigger,
|
117 |
+
SelectContent,
|
118 |
+
SelectLabel,
|
119 |
+
SelectItem,
|
120 |
+
SelectSeparator,
|
121 |
+
}
|
src/components/ui/separator.tsx
ADDED
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
|
3 |
+
import * as React from "react"
|
4 |
+
import * as SeparatorPrimitive from "@radix-ui/react-separator"
|
5 |
+
|
6 |
+
import { cn } from "@/lib/utils"
|
7 |
+
|
8 |
+
const Separator = React.forwardRef<
|
9 |
+
React.ElementRef<typeof SeparatorPrimitive.Root>,
|
10 |
+
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
|
11 |
+
>(
|
12 |
+
(
|
13 |
+
{ className, orientation = "horizontal", decorative = true, ...props },
|
14 |
+
ref
|
15 |
+
) => (
|
16 |
+
<SeparatorPrimitive.Root
|
17 |
+
ref={ref}
|
18 |
+
decorative={decorative}
|
19 |
+
orientation={orientation}
|
20 |
+
className={cn(
|
21 |
+
"shrink-0 bg-stone-200 dark:bg-stone-800",
|
22 |
+
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
|
23 |
+
className
|
24 |
+
)}
|
25 |
+
{...props}
|
26 |
+
/>
|
27 |
+
)
|
28 |
+
)
|
29 |
+
Separator.displayName = SeparatorPrimitive.Root.displayName
|
30 |
+
|
31 |
+
export { Separator }
|
src/components/ui/switch.tsx
ADDED
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
|
3 |
+
import * as React from "react"
|
4 |
+
import * as SwitchPrimitives from "@radix-ui/react-switch"
|
5 |
+
|
6 |
+
import { cn } from "@/lib/utils"
|
7 |
+
|
8 |
+
const Switch = React.forwardRef<
|
9 |
+
React.ElementRef<typeof SwitchPrimitives.Root>,
|
10 |
+
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
|
11 |
+
>(({ className, ...props }, ref) => (
|
12 |
+
<SwitchPrimitives.Root
|
13 |
+
className={cn(
|
14 |
+
"peer inline-flex h-[24px] w-[44px] shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-stone-400 focus-visible:ring-offset-2 focus-visible:ring-offset-white disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-stone-900 data-[state=unchecked]:bg-stone-200 dark:focus-visible:ring-stone-800 dark:focus-visible:ring-offset-stone-950 dark:data-[state=checked]:bg-stone-50 dark:data-[state=unchecked]:bg-stone-800",
|
15 |
+
className
|
16 |
+
)}
|
17 |
+
{...props}
|
18 |
+
ref={ref}
|
19 |
+
>
|
20 |
+
<SwitchPrimitives.Thumb
|
21 |
+
className={cn(
|
22 |
+
"pointer-events-none block h-5 w-5 rounded-full bg-white shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0 dark:bg-stone-950"
|
23 |
+
)}
|
24 |
+
/>
|
25 |
+
</SwitchPrimitives.Root>
|
26 |
+
))
|
27 |
+
Switch.displayName = SwitchPrimitives.Root.displayName
|
28 |
+
|
29 |
+
export { Switch }
|