Spaces:
Runtime error
Runtime error
add ui api
Browse files- .env.example +1 -0
- bun.lockb +0 -0
- components.json +17 -0
- package.json +11 -1
- src/app/globals.css +69 -20
- src/app/layout.tsx +2 -0
- src/app/page.tsx +90 -104
- src/components/LoadingIcon.tsx +8 -0
- src/components/ui/button.tsx +56 -0
- src/components/ui/card.tsx +79 -0
- src/components/ui/form.tsx +176 -0
- src/components/ui/input.tsx +25 -0
- src/components/ui/label.tsx +26 -0
- src/components/ui/skeleton.tsx +15 -0
- src/lib/comfy-deploy.ts +99 -0
- src/lib/utils.ts +6 -0
- src/server/generate.tsx +20 -0
- tailwind.config.ts +73 -13
.env.example
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
COMFY_API_TOKEN=
|
bun.lockb
CHANGED
Binary files a/bun.lockb and b/bun.lockb differ
|
|
components.json
ADDED
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"$schema": "https://ui.shadcn.com/schema.json",
|
3 |
+
"style": "default",
|
4 |
+
"rsc": true,
|
5 |
+
"tsx": true,
|
6 |
+
"tailwind": {
|
7 |
+
"config": "tailwind.config.ts",
|
8 |
+
"css": "src/app/globals.css",
|
9 |
+
"baseColor": "zinc",
|
10 |
+
"cssVariables": true,
|
11 |
+
"prefix": ""
|
12 |
+
},
|
13 |
+
"aliases": {
|
14 |
+
"components": "@/components",
|
15 |
+
"utils": "@/lib/utils"
|
16 |
+
}
|
17 |
+
}
|
package.json
CHANGED
@@ -9,9 +9,19 @@
|
|
9 |
"lint": "next lint"
|
10 |
},
|
11 |
"dependencies": {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
12 |
"react": "^18",
|
13 |
"react-dom": "^18",
|
14 |
-
"
|
|
|
|
|
|
|
15 |
},
|
16 |
"devDependencies": {
|
17 |
"typescript": "^5",
|
|
|
9 |
"lint": "next lint"
|
10 |
},
|
11 |
"dependencies": {
|
12 |
+
"@hookform/resolvers": "^3.3.4",
|
13 |
+
"@radix-ui/react-label": "^2.0.2",
|
14 |
+
"@radix-ui/react-slot": "^1.0.2",
|
15 |
+
"class-variance-authority": "^0.7.0",
|
16 |
+
"clsx": "^2.1.0",
|
17 |
+
"lucide-react": "^0.309.0",
|
18 |
+
"next": "14.0.3",
|
19 |
"react": "^18",
|
20 |
"react-dom": "^18",
|
21 |
+
"react-hook-form": "^7.49.3",
|
22 |
+
"tailwind-merge": "^2.2.0",
|
23 |
+
"tailwindcss-animate": "^1.0.7",
|
24 |
+
"zod": "^3.22.4"
|
25 |
},
|
26 |
"devDependencies": {
|
27 |
"typescript": "^5",
|
src/app/globals.css
CHANGED
@@ -1,27 +1,76 @@
|
|
1 |
@tailwind base;
|
2 |
@tailwind components;
|
3 |
@tailwind utilities;
|
|
|
|
|
|
|
|
|
|
|
4 |
|
5 |
-
:
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
10 |
|
11 |
-
|
12 |
-
|
13 |
-
--
|
14 |
-
|
15 |
-
--
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
16 |
}
|
17 |
}
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
}
|
|
|
1 |
@tailwind base;
|
2 |
@tailwind components;
|
3 |
@tailwind utilities;
|
4 |
+
|
5 |
+
@layer base {
|
6 |
+
:root {
|
7 |
+
--background: 0 0% 100%;
|
8 |
+
--foreground: 240 10% 3.9%;
|
9 |
|
10 |
+
--card: 0 0% 100%;
|
11 |
+
--card-foreground: 240 10% 3.9%;
|
12 |
+
|
13 |
+
--popover: 0 0% 100%;
|
14 |
+
--popover-foreground: 240 10% 3.9%;
|
15 |
+
|
16 |
+
--primary: 240 5.9% 10%;
|
17 |
+
--primary-foreground: 0 0% 98%;
|
18 |
+
|
19 |
+
--secondary: 240 4.8% 95.9%;
|
20 |
+
--secondary-foreground: 240 5.9% 10%;
|
21 |
+
|
22 |
+
--muted: 240 4.8% 95.9%;
|
23 |
+
--muted-foreground: 240 3.8% 46.1%;
|
24 |
+
|
25 |
+
--accent: 240 4.8% 95.9%;
|
26 |
+
--accent-foreground: 240 5.9% 10%;
|
27 |
+
|
28 |
+
--destructive: 0 84.2% 60.2%;
|
29 |
+
--destructive-foreground: 0 0% 98%;
|
30 |
|
31 |
+
--border: 240 5.9% 90%;
|
32 |
+
--input: 240 5.9% 90%;
|
33 |
+
--ring: 240 10% 3.9%;
|
34 |
+
|
35 |
+
--radius: 0.5rem;
|
36 |
+
}
|
37 |
+
|
38 |
+
.dark {
|
39 |
+
--background: 240 10% 3.9%;
|
40 |
+
--foreground: 0 0% 98%;
|
41 |
+
|
42 |
+
--card: 240 10% 3.9%;
|
43 |
+
--card-foreground: 0 0% 98%;
|
44 |
+
|
45 |
+
--popover: 240 10% 3.9%;
|
46 |
+
--popover-foreground: 0 0% 98%;
|
47 |
+
|
48 |
+
--primary: 0 0% 98%;
|
49 |
+
--primary-foreground: 240 5.9% 10%;
|
50 |
+
|
51 |
+
--secondary: 240 3.7% 15.9%;
|
52 |
+
--secondary-foreground: 0 0% 98%;
|
53 |
+
|
54 |
+
--muted: 240 3.7% 15.9%;
|
55 |
+
--muted-foreground: 240 5% 64.9%;
|
56 |
+
|
57 |
+
--accent: 240 3.7% 15.9%;
|
58 |
+
--accent-foreground: 0 0% 98%;
|
59 |
+
|
60 |
+
--destructive: 0 62.8% 30.6%;
|
61 |
+
--destructive-foreground: 0 0% 98%;
|
62 |
+
|
63 |
+
--border: 240 3.7% 15.9%;
|
64 |
+
--input: 240 3.7% 15.9%;
|
65 |
+
--ring: 240 4.9% 83.9%;
|
66 |
}
|
67 |
}
|
68 |
+
|
69 |
+
@layer base {
|
70 |
+
* {
|
71 |
+
@apply border-border;
|
72 |
+
}
|
73 |
+
body {
|
74 |
+
@apply bg-background text-foreground;
|
75 |
+
}
|
76 |
+
}
|
|
src/app/layout.tsx
CHANGED
@@ -4,6 +4,8 @@ import './globals.css'
|
|
4 |
|
5 |
const inter = Inter({ subsets: ['latin'] })
|
6 |
|
|
|
|
|
7 |
export const metadata: Metadata = {
|
8 |
title: 'Create Next App',
|
9 |
description: 'Generated by create next app',
|
|
|
4 |
|
5 |
const inter = Inter({ subsets: ['latin'] })
|
6 |
|
7 |
+
export const runtime = 'edge'
|
8 |
+
|
9 |
export const metadata: Metadata = {
|
10 |
title: 'Create Next App',
|
11 |
description: 'Generated by create next app',
|
src/app/page.tsx
CHANGED
@@ -1,113 +1,99 @@
|
|
1 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2 |
|
3 |
export default function Home() {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
4 |
return (
|
5 |
<main className="flex min-h-screen flex-col items-center justify-between p-24">
|
6 |
-
<
|
7 |
-
<
|
8 |
-
|
9 |
-
<
|
10 |
-
|
11 |
-
|
12 |
-
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
17 |
>
|
18 |
-
|
19 |
-
<
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
height={24}
|
25 |
-
priority
|
26 |
/>
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
src="/next.svg"
|
35 |
-
alt="Next.js Logo"
|
36 |
-
width={180}
|
37 |
-
height={37}
|
38 |
-
priority
|
39 |
-
/>
|
40 |
-
</div>
|
41 |
-
|
42 |
-
<div className="mb-32 grid text-center lg:max-w-5xl lg:w-full lg:mb-0 lg:grid-cols-4 lg:text-left">
|
43 |
-
<a
|
44 |
-
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
|
45 |
-
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
|
46 |
-
target="_blank"
|
47 |
-
rel="noopener noreferrer"
|
48 |
-
>
|
49 |
-
<h2 className={`mb-3 text-2xl font-semibold`}>
|
50 |
-
Docs{' '}
|
51 |
-
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
|
52 |
-
->
|
53 |
-
</span>
|
54 |
-
</h2>
|
55 |
-
<p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
|
56 |
-
Find in-depth information about Next.js features and API.
|
57 |
-
</p>
|
58 |
-
</a>
|
59 |
-
|
60 |
-
<a
|
61 |
-
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
62 |
-
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
|
63 |
-
target="_blank"
|
64 |
-
rel="noopener noreferrer"
|
65 |
-
>
|
66 |
-
<h2 className={`mb-3 text-2xl font-semibold`}>
|
67 |
-
Learn{' '}
|
68 |
-
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
|
69 |
-
->
|
70 |
-
</span>
|
71 |
-
</h2>
|
72 |
-
<p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
|
73 |
-
Learn about Next.js in an interactive course with quizzes!
|
74 |
-
</p>
|
75 |
-
</a>
|
76 |
-
|
77 |
-
<a
|
78 |
-
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
|
79 |
-
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
|
80 |
-
target="_blank"
|
81 |
-
rel="noopener noreferrer"
|
82 |
-
>
|
83 |
-
<h2 className={`mb-3 text-2xl font-semibold`}>
|
84 |
-
Templates{' '}
|
85 |
-
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
|
86 |
-
->
|
87 |
-
</span>
|
88 |
-
</h2>
|
89 |
-
<p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
|
90 |
-
Explore starter templates for Next.js.
|
91 |
-
</p>
|
92 |
-
</a>
|
93 |
|
94 |
-
|
95 |
-
|
96 |
-
|
97 |
-
|
98 |
-
|
99 |
-
|
100 |
-
|
101 |
-
|
102 |
-
|
103 |
-
|
104 |
-
|
105 |
-
|
106 |
-
|
107 |
-
|
108 |
-
|
109 |
-
|
110 |
-
|
|
|
111 |
</main>
|
112 |
-
)
|
113 |
}
|
|
|
1 |
+
"use client";
|
2 |
+
|
3 |
+
import { LoadingIcon } from "@/components/LoadingIcon";
|
4 |
+
import { Button } from "@/components/ui/button";
|
5 |
+
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
6 |
+
import { Input } from "@/components/ui/input";
|
7 |
+
import { Label } from "@/components/ui/label";
|
8 |
+
import { Skeleton } from "@/components/ui/skeleton";
|
9 |
+
import { checkStatus, generate } from "@/server/generate";
|
10 |
+
import { useEffect, useState } from "react";
|
11 |
|
12 |
export default function Home() {
|
13 |
+
const [prompt, setPrompt] = useState("");
|
14 |
+
const [image, setImage] = useState("");
|
15 |
+
const [loading, setLoading] = useState(false);
|
16 |
+
const [runId, setRunId] = useState("");
|
17 |
+
const [status, setStatus] = useState("preparing");
|
18 |
+
|
19 |
+
// Polling in frontend to check for the
|
20 |
+
useEffect(() => {
|
21 |
+
if (!runId) return;
|
22 |
+
const interval = setInterval(() => {
|
23 |
+
checkStatus(runId).then((res) => {
|
24 |
+
if (res) setStatus(res.status);
|
25 |
+
if (res && res.status === "success") {
|
26 |
+
console.log(res.outputs[0].data);
|
27 |
+
setImage(res.outputs[0].data.images[0].url);
|
28 |
+
setLoading(false);
|
29 |
+
clearInterval(interval);
|
30 |
+
}
|
31 |
+
});
|
32 |
+
}, 2000);
|
33 |
+
return () => clearInterval(interval);
|
34 |
+
}, [runId]);
|
35 |
+
|
36 |
return (
|
37 |
<main className="flex min-h-screen flex-col items-center justify-between p-24">
|
38 |
+
<Card className="w-full max-w-[500px]">
|
39 |
+
<CardHeader>
|
40 |
+
Comfy Deploy - Vector Line Art Tool
|
41 |
+
<div className="text-xs text-foreground opacity-50">
|
42 |
+
Lora -{" "}
|
43 |
+
<a href="https://civitai.com/models/256144/stick-line-vector-illustration">
|
44 |
+
stick-line-vector-illustration
|
45 |
+
</a>
|
46 |
+
</div>
|
47 |
+
</CardHeader>
|
48 |
+
<CardContent>
|
49 |
+
<form
|
50 |
+
className="grid w-full items-center gap-1.5"
|
51 |
+
onSubmit={(e) => {
|
52 |
+
if (loading) return;
|
53 |
+
|
54 |
+
e.preventDefault();
|
55 |
+
setLoading(true);
|
56 |
+
generate(prompt).then((res) => {
|
57 |
+
console.log(res);
|
58 |
+
if (!res) return;
|
59 |
+
setRunId(res.run_id);
|
60 |
+
});
|
61 |
+
setStatus("preparing");
|
62 |
+
}}
|
63 |
>
|
64 |
+
<Label htmlFor="picture">Image prompt</Label>
|
65 |
+
<Input
|
66 |
+
id="picture"
|
67 |
+
type="text"
|
68 |
+
value={prompt}
|
69 |
+
onChange={(e) => setPrompt(e.target.value)}
|
|
|
|
|
70 |
/>
|
71 |
+
<Button
|
72 |
+
type="submit"
|
73 |
+
className="flex gap-2"
|
74 |
+
disabled={loading}
|
75 |
+
>
|
76 |
+
Generate {loading && <LoadingIcon />}
|
77 |
+
</Button>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
78 |
|
79 |
+
<div className="border border-gray-200 w-full square h-[400px] rounded-lg relative">
|
80 |
+
{!loading && image && (
|
81 |
+
<img
|
82 |
+
className="w-full h-full object-contain"
|
83 |
+
src={image}
|
84 |
+
alt="Generated image"
|
85 |
+
></img>
|
86 |
+
)}
|
87 |
+
{loading && (
|
88 |
+
<div className="absolute top-0 left-0 w-full h-full flex items-center justify-center gap-2">
|
89 |
+
{status} <LoadingIcon />
|
90 |
+
</div>
|
91 |
+
)}
|
92 |
+
{loading && <Skeleton className="w-full h-full" />}
|
93 |
+
</div>
|
94 |
+
</form>
|
95 |
+
</CardContent>
|
96 |
+
</Card>
|
97 |
</main>
|
98 |
+
);
|
99 |
}
|
src/components/LoadingIcon.tsx
ADDED
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client";
|
2 |
+
|
3 |
+
import { LoaderIcon } from "lucide-react";
|
4 |
+
import * as React from "react";
|
5 |
+
|
6 |
+
export function LoadingIcon() {
|
7 |
+
return <LoaderIcon size={14} className="animate-spin" />;
|
8 |
+
}
|
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 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
9 |
+
{
|
10 |
+
variants: {
|
11 |
+
variant: {
|
12 |
+
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
13 |
+
destructive:
|
14 |
+
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
15 |
+
outline:
|
16 |
+
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
17 |
+
secondary:
|
18 |
+
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
19 |
+
ghost: "hover:bg-accent hover:text-accent-foreground",
|
20 |
+
link: "text-primary underline-offset-4 hover:underline",
|
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 bg-card text-card-foreground shadow-sm",
|
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-muted-foreground", 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/form.tsx
ADDED
@@ -0,0 +1,176 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import * as React from "react"
|
2 |
+
import * as LabelPrimitive from "@radix-ui/react-label"
|
3 |
+
import { Slot } from "@radix-ui/react-slot"
|
4 |
+
import {
|
5 |
+
Controller,
|
6 |
+
ControllerProps,
|
7 |
+
FieldPath,
|
8 |
+
FieldValues,
|
9 |
+
FormProvider,
|
10 |
+
useFormContext,
|
11 |
+
} from "react-hook-form"
|
12 |
+
|
13 |
+
import { cn } from "@/lib/utils"
|
14 |
+
import { Label } from "@/components/ui/label"
|
15 |
+
|
16 |
+
const Form = FormProvider
|
17 |
+
|
18 |
+
type FormFieldContextValue<
|
19 |
+
TFieldValues extends FieldValues = FieldValues,
|
20 |
+
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
|
21 |
+
> = {
|
22 |
+
name: TName
|
23 |
+
}
|
24 |
+
|
25 |
+
const FormFieldContext = React.createContext<FormFieldContextValue>(
|
26 |
+
{} as FormFieldContextValue
|
27 |
+
)
|
28 |
+
|
29 |
+
const FormField = <
|
30 |
+
TFieldValues extends FieldValues = FieldValues,
|
31 |
+
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
|
32 |
+
>({
|
33 |
+
...props
|
34 |
+
}: ControllerProps<TFieldValues, TName>) => {
|
35 |
+
return (
|
36 |
+
<FormFieldContext.Provider value={{ name: props.name }}>
|
37 |
+
<Controller {...props} />
|
38 |
+
</FormFieldContext.Provider>
|
39 |
+
)
|
40 |
+
}
|
41 |
+
|
42 |
+
const useFormField = () => {
|
43 |
+
const fieldContext = React.useContext(FormFieldContext)
|
44 |
+
const itemContext = React.useContext(FormItemContext)
|
45 |
+
const { getFieldState, formState } = useFormContext()
|
46 |
+
|
47 |
+
const fieldState = getFieldState(fieldContext.name, formState)
|
48 |
+
|
49 |
+
if (!fieldContext) {
|
50 |
+
throw new Error("useFormField should be used within <FormField>")
|
51 |
+
}
|
52 |
+
|
53 |
+
const { id } = itemContext
|
54 |
+
|
55 |
+
return {
|
56 |
+
id,
|
57 |
+
name: fieldContext.name,
|
58 |
+
formItemId: `${id}-form-item`,
|
59 |
+
formDescriptionId: `${id}-form-item-description`,
|
60 |
+
formMessageId: `${id}-form-item-message`,
|
61 |
+
...fieldState,
|
62 |
+
}
|
63 |
+
}
|
64 |
+
|
65 |
+
type FormItemContextValue = {
|
66 |
+
id: string
|
67 |
+
}
|
68 |
+
|
69 |
+
const FormItemContext = React.createContext<FormItemContextValue>(
|
70 |
+
{} as FormItemContextValue
|
71 |
+
)
|
72 |
+
|
73 |
+
const FormItem = React.forwardRef<
|
74 |
+
HTMLDivElement,
|
75 |
+
React.HTMLAttributes<HTMLDivElement>
|
76 |
+
>(({ className, ...props }, ref) => {
|
77 |
+
const id = React.useId()
|
78 |
+
|
79 |
+
return (
|
80 |
+
<FormItemContext.Provider value={{ id }}>
|
81 |
+
<div ref={ref} className={cn("space-y-2", className)} {...props} />
|
82 |
+
</FormItemContext.Provider>
|
83 |
+
)
|
84 |
+
})
|
85 |
+
FormItem.displayName = "FormItem"
|
86 |
+
|
87 |
+
const FormLabel = React.forwardRef<
|
88 |
+
React.ElementRef<typeof LabelPrimitive.Root>,
|
89 |
+
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
|
90 |
+
>(({ className, ...props }, ref) => {
|
91 |
+
const { error, formItemId } = useFormField()
|
92 |
+
|
93 |
+
return (
|
94 |
+
<Label
|
95 |
+
ref={ref}
|
96 |
+
className={cn(error && "text-destructive", className)}
|
97 |
+
htmlFor={formItemId}
|
98 |
+
{...props}
|
99 |
+
/>
|
100 |
+
)
|
101 |
+
})
|
102 |
+
FormLabel.displayName = "FormLabel"
|
103 |
+
|
104 |
+
const FormControl = React.forwardRef<
|
105 |
+
React.ElementRef<typeof Slot>,
|
106 |
+
React.ComponentPropsWithoutRef<typeof Slot>
|
107 |
+
>(({ ...props }, ref) => {
|
108 |
+
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
|
109 |
+
|
110 |
+
return (
|
111 |
+
<Slot
|
112 |
+
ref={ref}
|
113 |
+
id={formItemId}
|
114 |
+
aria-describedby={
|
115 |
+
!error
|
116 |
+
? `${formDescriptionId}`
|
117 |
+
: `${formDescriptionId} ${formMessageId}`
|
118 |
+
}
|
119 |
+
aria-invalid={!!error}
|
120 |
+
{...props}
|
121 |
+
/>
|
122 |
+
)
|
123 |
+
})
|
124 |
+
FormControl.displayName = "FormControl"
|
125 |
+
|
126 |
+
const FormDescription = React.forwardRef<
|
127 |
+
HTMLParagraphElement,
|
128 |
+
React.HTMLAttributes<HTMLParagraphElement>
|
129 |
+
>(({ className, ...props }, ref) => {
|
130 |
+
const { formDescriptionId } = useFormField()
|
131 |
+
|
132 |
+
return (
|
133 |
+
<p
|
134 |
+
ref={ref}
|
135 |
+
id={formDescriptionId}
|
136 |
+
className={cn("text-sm text-muted-foreground", className)}
|
137 |
+
{...props}
|
138 |
+
/>
|
139 |
+
)
|
140 |
+
})
|
141 |
+
FormDescription.displayName = "FormDescription"
|
142 |
+
|
143 |
+
const FormMessage = React.forwardRef<
|
144 |
+
HTMLParagraphElement,
|
145 |
+
React.HTMLAttributes<HTMLParagraphElement>
|
146 |
+
>(({ className, children, ...props }, ref) => {
|
147 |
+
const { error, formMessageId } = useFormField()
|
148 |
+
const body = error ? String(error?.message) : children
|
149 |
+
|
150 |
+
if (!body) {
|
151 |
+
return null
|
152 |
+
}
|
153 |
+
|
154 |
+
return (
|
155 |
+
<p
|
156 |
+
ref={ref}
|
157 |
+
id={formMessageId}
|
158 |
+
className={cn("text-sm font-medium text-destructive", className)}
|
159 |
+
{...props}
|
160 |
+
>
|
161 |
+
{body}
|
162 |
+
</p>
|
163 |
+
)
|
164 |
+
})
|
165 |
+
FormMessage.displayName = "FormMessage"
|
166 |
+
|
167 |
+
export {
|
168 |
+
useFormField,
|
169 |
+
Form,
|
170 |
+
FormItem,
|
171 |
+
FormLabel,
|
172 |
+
FormControl,
|
173 |
+
FormDescription,
|
174 |
+
FormMessage,
|
175 |
+
FormField,
|
176 |
+
}
|
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-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
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/skeleton.tsx
ADDED
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { cn } from "@/lib/utils"
|
2 |
+
|
3 |
+
function Skeleton({
|
4 |
+
className,
|
5 |
+
...props
|
6 |
+
}: React.HTMLAttributes<HTMLDivElement>) {
|
7 |
+
return (
|
8 |
+
<div
|
9 |
+
className={cn("animate-pulse rounded-md bg-muted", className)}
|
10 |
+
{...props}
|
11 |
+
/>
|
12 |
+
)
|
13 |
+
}
|
14 |
+
|
15 |
+
export { Skeleton }
|
src/lib/comfy-deploy.ts
ADDED
@@ -0,0 +1,99 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { z } from "zod";
|
2 |
+
|
3 |
+
const runTypes = z.object({
|
4 |
+
run_id: z.string(),
|
5 |
+
});
|
6 |
+
|
7 |
+
const runOutputTypes = z.object({
|
8 |
+
id: z.string(),
|
9 |
+
status: z.enum(["success", "failed", "running", "uploading"]),
|
10 |
+
outputs: z.array(
|
11 |
+
z.object({
|
12 |
+
data: z.any(),
|
13 |
+
})
|
14 |
+
),
|
15 |
+
});
|
16 |
+
|
17 |
+
export class ComfyDeployClient {
|
18 |
+
apiBase: string = "https://www.comfydeploy.com/api";
|
19 |
+
apiToken: string;
|
20 |
+
|
21 |
+
constructor({ apiBase, apiToken }: { apiBase?: string; apiToken: string }) {
|
22 |
+
if (apiBase)
|
23 |
+
this.apiBase = `${apiBase}/api`;
|
24 |
+
this.apiToken = apiToken;
|
25 |
+
}
|
26 |
+
|
27 |
+
async run({
|
28 |
+
deployment_id,
|
29 |
+
inputs,
|
30 |
+
}: {
|
31 |
+
deployment_id: string;
|
32 |
+
inputs?: Record<string, string>;
|
33 |
+
}) {
|
34 |
+
return fetch(`${this.apiBase}/run`, {
|
35 |
+
method: "POST",
|
36 |
+
headers: {
|
37 |
+
"Content-Type": "application/json",
|
38 |
+
authorization: `Bearer ${this.apiToken}`,
|
39 |
+
},
|
40 |
+
body: JSON.stringify({
|
41 |
+
deployment_id: deployment_id,
|
42 |
+
inputs: inputs,
|
43 |
+
}),
|
44 |
+
cache: "no-store"
|
45 |
+
})
|
46 |
+
.then((response) => response.json())
|
47 |
+
.then((json) => runTypes.parse(json))
|
48 |
+
.catch((err) => {
|
49 |
+
console.error(err);
|
50 |
+
return null;
|
51 |
+
});
|
52 |
+
}
|
53 |
+
|
54 |
+
async getRun(run_id: string) {
|
55 |
+
return await fetch(`${this.apiBase}/run?run_id=${run_id}`, {
|
56 |
+
method: "GET",
|
57 |
+
headers: {
|
58 |
+
"Content-Type": "application/json",
|
59 |
+
authorization: `Bearer ${this.apiToken}`,
|
60 |
+
},
|
61 |
+
cache: "no-store"
|
62 |
+
})
|
63 |
+
.then((response) => response.json())
|
64 |
+
.then((json) => runOutputTypes.parse(json))
|
65 |
+
.catch((err) => {
|
66 |
+
console.error(err);
|
67 |
+
return null;
|
68 |
+
});
|
69 |
+
}
|
70 |
+
|
71 |
+
async runSync(props: {
|
72 |
+
deployment_id: string;
|
73 |
+
inputs?: Record<string, string>;
|
74 |
+
}) {
|
75 |
+
const runResult = await this.run(props);
|
76 |
+
if (!runResult) return null;
|
77 |
+
|
78 |
+
// 5 minutes
|
79 |
+
const timeout = 60 * 5;
|
80 |
+
const interval = 1000;
|
81 |
+
|
82 |
+
let run: Awaited<ReturnType<typeof this.getRun>> = null;
|
83 |
+
for (let i = 0; i < timeout; i++) {
|
84 |
+
run = await this.getRun(runResult.run_id);
|
85 |
+
if (run && run.status == "succuss") {
|
86 |
+
break;
|
87 |
+
}
|
88 |
+
await new Promise((resolve) => setTimeout(resolve, interval));
|
89 |
+
}
|
90 |
+
|
91 |
+
if (!run) {
|
92 |
+
return {
|
93 |
+
id: runResult.run_id,
|
94 |
+
};
|
95 |
+
}
|
96 |
+
|
97 |
+
return run;
|
98 |
+
}
|
99 |
+
}
|
src/lib/utils.ts
ADDED
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { type ClassValue, clsx } from "clsx"
|
2 |
+
import { twMerge } from "tailwind-merge"
|
3 |
+
|
4 |
+
export function cn(...inputs: ClassValue[]) {
|
5 |
+
return twMerge(clsx(inputs))
|
6 |
+
}
|
src/server/generate.tsx
ADDED
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use server"
|
2 |
+
|
3 |
+
import { ComfyDeployClient } from "@/lib/comfy-deploy"
|
4 |
+
|
5 |
+
const client = new ComfyDeployClient({
|
6 |
+
apiToken: process.env.COMFY_API_TOKEN!,
|
7 |
+
})
|
8 |
+
|
9 |
+
export async function generate(prompt: string){
|
10 |
+
return await client.run({
|
11 |
+
deployment_id: "8dc98937-9842-425b-a562-970329d07639",
|
12 |
+
inputs: {
|
13 |
+
"input_text": prompt
|
14 |
+
}
|
15 |
+
})
|
16 |
+
}
|
17 |
+
|
18 |
+
export async function checkStatus(run_id: string){
|
19 |
+
return await client.getRun(run_id)
|
20 |
+
}
|
tailwind.config.ts
CHANGED
@@ -1,20 +1,80 @@
|
|
1 |
-
import type { Config } from
|
2 |
|
3 |
-
const config
|
|
|
4 |
content: [
|
5 |
-
'./
|
6 |
-
'./
|
7 |
-
'./
|
8 |
-
|
|
|
|
|
9 |
theme: {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
10 |
extend: {
|
11 |
-
|
12 |
-
|
13 |
-
|
14 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
15 |
},
|
16 |
},
|
17 |
},
|
18 |
-
plugins: [],
|
19 |
-
}
|
20 |
-
|
|
|
|
1 |
+
import type { Config } from "tailwindcss"
|
2 |
|
3 |
+
const config = {
|
4 |
+
darkMode: ["class"],
|
5 |
content: [
|
6 |
+
'./pages/**/*.{ts,tsx}',
|
7 |
+
'./components/**/*.{ts,tsx}',
|
8 |
+
'./app/**/*.{ts,tsx}',
|
9 |
+
'./src/**/*.{ts,tsx}',
|
10 |
+
],
|
11 |
+
prefix: "",
|
12 |
theme: {
|
13 |
+
container: {
|
14 |
+
center: true,
|
15 |
+
padding: "2rem",
|
16 |
+
screens: {
|
17 |
+
"2xl": "1400px",
|
18 |
+
},
|
19 |
+
},
|
20 |
extend: {
|
21 |
+
colors: {
|
22 |
+
border: "hsl(var(--border))",
|
23 |
+
input: "hsl(var(--input))",
|
24 |
+
ring: "hsl(var(--ring))",
|
25 |
+
background: "hsl(var(--background))",
|
26 |
+
foreground: "hsl(var(--foreground))",
|
27 |
+
primary: {
|
28 |
+
DEFAULT: "hsl(var(--primary))",
|
29 |
+
foreground: "hsl(var(--primary-foreground))",
|
30 |
+
},
|
31 |
+
secondary: {
|
32 |
+
DEFAULT: "hsl(var(--secondary))",
|
33 |
+
foreground: "hsl(var(--secondary-foreground))",
|
34 |
+
},
|
35 |
+
destructive: {
|
36 |
+
DEFAULT: "hsl(var(--destructive))",
|
37 |
+
foreground: "hsl(var(--destructive-foreground))",
|
38 |
+
},
|
39 |
+
muted: {
|
40 |
+
DEFAULT: "hsl(var(--muted))",
|
41 |
+
foreground: "hsl(var(--muted-foreground))",
|
42 |
+
},
|
43 |
+
accent: {
|
44 |
+
DEFAULT: "hsl(var(--accent))",
|
45 |
+
foreground: "hsl(var(--accent-foreground))",
|
46 |
+
},
|
47 |
+
popover: {
|
48 |
+
DEFAULT: "hsl(var(--popover))",
|
49 |
+
foreground: "hsl(var(--popover-foreground))",
|
50 |
+
},
|
51 |
+
card: {
|
52 |
+
DEFAULT: "hsl(var(--card))",
|
53 |
+
foreground: "hsl(var(--card-foreground))",
|
54 |
+
},
|
55 |
+
},
|
56 |
+
borderRadius: {
|
57 |
+
lg: "var(--radius)",
|
58 |
+
md: "calc(var(--radius) - 2px)",
|
59 |
+
sm: "calc(var(--radius) - 4px)",
|
60 |
+
},
|
61 |
+
keyframes: {
|
62 |
+
"accordion-down": {
|
63 |
+
from: { height: "0" },
|
64 |
+
to: { height: "var(--radix-accordion-content-height)" },
|
65 |
+
},
|
66 |
+
"accordion-up": {
|
67 |
+
from: { height: "var(--radix-accordion-content-height)" },
|
68 |
+
to: { height: "0" },
|
69 |
+
},
|
70 |
+
},
|
71 |
+
animation: {
|
72 |
+
"accordion-down": "accordion-down 0.2s ease-out",
|
73 |
+
"accordion-up": "accordion-up 0.2s ease-out",
|
74 |
},
|
75 |
},
|
76 |
},
|
77 |
+
plugins: [require("tailwindcss-animate")],
|
78 |
+
} satisfies Config
|
79 |
+
|
80 |
+
export default config
|