jbilcke-hf HF Staff commited on
Commit
624088c
·
0 Parent(s):

initial commit

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .env +3 -0
  2. .eslintrc.json +3 -0
  3. .gitignore +35 -0
  4. .nvmrc +1 -0
  5. Dockerfile +65 -0
  6. README.md +11 -0
  7. components.json +16 -0
  8. next.config.js +10 -0
  9. package-lock.json +0 -0
  10. package.json +66 -0
  11. postcss.config.js +6 -0
  12. public/next.svg +1 -0
  13. public/vercel.svg +1 -0
  14. scripts/test.js +23 -0
  15. src/app/engine/censorship.ts +4 -0
  16. src/app/engine/forbidden.ts +6 -0
  17. src/app/engine/presets.ts +136 -0
  18. src/app/engine/render.ts +132 -0
  19. src/app/favicon.ico +0 -0
  20. src/app/globals.css +27 -0
  21. src/app/interface/display/index.tsx +12 -0
  22. src/app/interface/panel/bubble.tsx +45 -0
  23. src/app/interface/panel/index.tsx +139 -0
  24. src/app/interface/progress/index.tsx +56 -0
  25. src/app/interface/progress/progress-bar.tsx +57 -0
  26. src/app/interface/top-menu/index.tsx +103 -0
  27. src/app/layout.tsx +24 -0
  28. src/app/main.tsx +112 -0
  29. src/app/page.tsx +28 -0
  30. src/app/queries/getBackground.ts +56 -0
  31. src/app/queries/predict.ts +56 -0
  32. src/components/icons/full-screen.tsx +16 -0
  33. src/components/ui/accordion.tsx +60 -0
  34. src/components/ui/alert.tsx +59 -0
  35. src/components/ui/avatar.tsx +50 -0
  36. src/components/ui/badge.tsx +36 -0
  37. src/components/ui/button.tsx +56 -0
  38. src/components/ui/card.tsx +79 -0
  39. src/components/ui/checkbox.tsx +30 -0
  40. src/components/ui/collapsible.tsx +11 -0
  41. src/components/ui/command.tsx +155 -0
  42. src/components/ui/dialog.tsx +123 -0
  43. src/components/ui/dropdown-menu.tsx +200 -0
  44. src/components/ui/input.tsx +25 -0
  45. src/components/ui/label.tsx +26 -0
  46. src/components/ui/menubar.tsx +236 -0
  47. src/components/ui/popover.tsx +31 -0
  48. src/components/ui/select.tsx +121 -0
  49. src/components/ui/separator.tsx +31 -0
  50. 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 }