Commit
·
59b210a
0
Parent(s):
fiat lux
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .gitignore +41 -0
- README.md +99 -0
- app/api/messages/route.ts +92 -0
- app/globals.css +94 -0
- app/layout.tsx +14 -0
- app/page.tsx +156 -0
- components.json +21 -0
- components/ChatArea.tsx +121 -0
- components/CookieConsent.tsx +56 -0
- components/Footer.tsx +32 -0
- components/Header.tsx +29 -0
- components/InputArea.tsx +154 -0
- components/SharePopover.tsx +44 -0
- components/theme-provider.tsx +10 -0
- components/ui/accordion.tsx +58 -0
- components/ui/alert-dialog.tsx +141 -0
- components/ui/alert.tsx +59 -0
- components/ui/aspect-ratio.tsx +7 -0
- components/ui/avatar.tsx +50 -0
- components/ui/badge.tsx +36 -0
- components/ui/breadcrumb.tsx +115 -0
- components/ui/button.tsx +56 -0
- components/ui/calendar.tsx +66 -0
- components/ui/card.tsx +79 -0
- components/ui/carousel.tsx +262 -0
- components/ui/chart.tsx +365 -0
- components/ui/checkbox.tsx +30 -0
- components/ui/collapsible.tsx +11 -0
- components/ui/command.tsx +153 -0
- components/ui/context-menu.tsx +200 -0
- components/ui/dialog.tsx +122 -0
- components/ui/drawer.tsx +118 -0
- components/ui/dropdown-menu.tsx +200 -0
- components/ui/form.tsx +178 -0
- components/ui/hover-card.tsx +29 -0
- components/ui/input-otp.tsx +71 -0
- components/ui/input.tsx +22 -0
- components/ui/label.tsx +26 -0
- components/ui/menubar.tsx +236 -0
- components/ui/navigation-menu.tsx +128 -0
- components/ui/pagination.tsx +117 -0
- components/ui/popover.tsx +31 -0
- components/ui/progress.tsx +28 -0
- components/ui/radio-group.tsx +44 -0
- components/ui/resizable.tsx +45 -0
- components/ui/scroll-area.tsx +48 -0
- components/ui/select.tsx +160 -0
- components/ui/separator.tsx +31 -0
- components/ui/sheet.tsx +140 -0
- components/ui/sidebar.tsx +763 -0
.gitignore
ADDED
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
2 |
+
|
3 |
+
# dependencies
|
4 |
+
/node_modules
|
5 |
+
/.pnp
|
6 |
+
.pnp.*
|
7 |
+
.yarn/*
|
8 |
+
!.yarn/patches
|
9 |
+
!.yarn/plugins
|
10 |
+
!.yarn/releases
|
11 |
+
!.yarn/versions
|
12 |
+
|
13 |
+
# testing
|
14 |
+
/coverage
|
15 |
+
|
16 |
+
# next.js
|
17 |
+
/.next/
|
18 |
+
/out/
|
19 |
+
|
20 |
+
# production
|
21 |
+
/build
|
22 |
+
|
23 |
+
# misc
|
24 |
+
.DS_Store
|
25 |
+
*.pem
|
26 |
+
|
27 |
+
# debug
|
28 |
+
npm-debug.log*
|
29 |
+
yarn-debug.log*
|
30 |
+
yarn-error.log*
|
31 |
+
.pnpm-debug.log*
|
32 |
+
|
33 |
+
# env files (can opt-in for committing if needed)
|
34 |
+
.env*
|
35 |
+
|
36 |
+
# vercel
|
37 |
+
.vercel
|
38 |
+
|
39 |
+
# typescript
|
40 |
+
*.tsbuildinfo
|
41 |
+
next-env.d.ts
|
README.md
ADDED
@@ -0,0 +1,99 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Pollinations Chatbox - Karma.yt Experiment
|
2 |
+
|
3 |
+
An experimental chat interface powered by Karma.yt, utilizing the advanced AI and image generation capabilities of Pollinations. Explore various AI models for interactive text and image generation, with local storage for seamless conversation continuity. This project is an innovative integration of Pollinations technologies to create a unique and flexible chat experience.
|
4 |
+
|
5 |
+
## 📧 Contact
|
6 |
+
|
7 |
+
If you have any questions or suggestions, feel free to reach out to us via email at: **[email protected]**
|
8 |
+
|
9 |
+
## 🌍 Social Media
|
10 |
+
|
11 |
+
- **Discord**: [Pollinations Discord](https://discord.gg/k9F7SyTgqn)
|
12 |
+
- **WhatsApp**: [WhatsApp Group](https://chat.whatsapp.com/JxQEn2FKDny0DdwkDuzoQR)
|
13 |
+
|
14 |
+
## 🛠️ Requirements
|
15 |
+
|
16 |
+
Before running the project locally, make sure you have the following:
|
17 |
+
|
18 |
+
- **Node.js**: Version 16.x or above
|
19 |
+
- **Next.js**: Used for building and rendering the website.
|
20 |
+
- **pnpm** (recommended), **npm**, or **bun** for package management.
|
21 |
+
|
22 |
+
### Installation
|
23 |
+
|
24 |
+
1. Clone the repository:
|
25 |
+
|
26 |
+
```bash
|
27 |
+
git clone https://github.com/diogo-karma/pollinations-chatbox.git
|
28 |
+
cd pollinations-chatbox
|
29 |
+
```
|
30 |
+
|
31 |
+
2. Install dependencies:
|
32 |
+
|
33 |
+
If using **pnpm** (recommended):
|
34 |
+
|
35 |
+
```bash
|
36 |
+
pnpm install
|
37 |
+
```
|
38 |
+
|
39 |
+
Or using **npm**:
|
40 |
+
|
41 |
+
```bash
|
42 |
+
npm install
|
43 |
+
```
|
44 |
+
|
45 |
+
Or **bun**:
|
46 |
+
|
47 |
+
```bash
|
48 |
+
bun install
|
49 |
+
```
|
50 |
+
|
51 |
+
3. Start the development server:
|
52 |
+
|
53 |
+
```bash
|
54 |
+
pnpm dev
|
55 |
+
```
|
56 |
+
|
57 |
+
Or using **npm**:
|
58 |
+
|
59 |
+
```bash
|
60 |
+
npm run dev
|
61 |
+
```
|
62 |
+
|
63 |
+
Or **bun**:
|
64 |
+
|
65 |
+
```bash
|
66 |
+
bun dev
|
67 |
+
```
|
68 |
+
|
69 |
+
4. Open the app in your browser at [http://localhost:3000](http://localhost:3000).
|
70 |
+
|
71 |
+
## 🛠️ Folder Structure
|
72 |
+
|
73 |
+
The project follows the standard Next.js conventions with additional folders for organization:
|
74 |
+
|
75 |
+
```
|
76 |
+
/app # App logic and components, including pages
|
77 |
+
/api # API routes for backend interaction
|
78 |
+
/components # Reusable UI components
|
79 |
+
/ui # Custom UI components built with ShadCN and Tailwind
|
80 |
+
/hooks # Custom hooks for additional functionality
|
81 |
+
/lib # Helper functions and external libraries
|
82 |
+
/public # Public assets like images and fonts
|
83 |
+
/styles # Global styles, including Tailwind configuration
|
84 |
+
```
|
85 |
+
|
86 |
+
## 🎨 Design and UX
|
87 |
+
|
88 |
+
We use **Tailwind CSS** for styling the interface, ensuring a responsive design, while **ShadCN UI** is used for creating a modern and fluid user experience (UX).
|
89 |
+
|
90 |
+
## 📚 Full Documentation
|
91 |
+
|
92 |
+
For more details on how the Pollinations technology works, visit the official documentation at [Pollinations.ai](https://pollinations.ai).
|
93 |
+
|
94 |
+
---
|
95 |
+
|
96 |
+
Thank you for exploring the **Pollinations (chatbox) Experiment**! Stay tuned for updates and improvements as we continue the development of this application. If you'd like to contribute or discuss new ideas, join us on **Discord** or the **WhatsApp group**!
|
97 |
+
|
98 |
+
[Pollinations Discord](https://discord.gg/k9F7SyTgqn)
|
99 |
+
[WhatsApp Group](https://chat.whatsapp.com/JxQEn2FKDny0DdwkDuzoQR)
|
app/api/messages/route.ts
ADDED
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { NextRequest, NextResponse } from "next/server";
|
2 |
+
|
3 |
+
export async function POST(req: NextRequest) {
|
4 |
+
const formData = await req.formData();
|
5 |
+
console.dir(formData);
|
6 |
+
const message = formData.get("message") as string;
|
7 |
+
const type = formData.get("type") as string;
|
8 |
+
const model = JSON.parse(formData.get("model") as string);
|
9 |
+
const image = formData.get("image") as string;
|
10 |
+
|
11 |
+
let content = ``;
|
12 |
+
let imageUrl = null;
|
13 |
+
let visionDescription = null;
|
14 |
+
const seed = Math.floor(Math.random() * 1337);
|
15 |
+
|
16 |
+
|
17 |
+
if (image) {
|
18 |
+
// const visionPayload = {
|
19 |
+
// messages: [
|
20 |
+
// {
|
21 |
+
// role: "user",
|
22 |
+
// content: [
|
23 |
+
// { type: "text", text: message },
|
24 |
+
// {
|
25 |
+
// type: "image_url",
|
26 |
+
// image_url: {
|
27 |
+
// url: image,
|
28 |
+
// },
|
29 |
+
// },
|
30 |
+
// ],
|
31 |
+
// },
|
32 |
+
// ],
|
33 |
+
// };
|
34 |
+
|
35 |
+
// const visionResponse = await fetch(process.env.AZURE_API_URL, {
|
36 |
+
// method: "POST",
|
37 |
+
// headers: {
|
38 |
+
// "Content-Type": "application/json",
|
39 |
+
// "api-key": process.env.AZURE_API_KEY,
|
40 |
+
// },
|
41 |
+
// body: JSON.stringify(visionPayload),
|
42 |
+
// });
|
43 |
+
|
44 |
+
// console.log(
|
45 |
+
// process.env.AZURE_API_URL,
|
46 |
+
// process.env.AZURE_API_KEY,
|
47 |
+
// JSON.stringify(visionPayload)
|
48 |
+
// );
|
49 |
+
// const visionData = await visionResponse.json();
|
50 |
+
// console.log(visionData);
|
51 |
+
// visionDescription =
|
52 |
+
// visionData?.choices[0]?.message?.content || "No description available.";
|
53 |
+
|
54 |
+
// if (type === "text") {
|
55 |
+
// const influencedPrompt = `${message} influenced by ${visionDescription}`;
|
56 |
+
// const prompt = encodeURIComponent(influencedPrompt);
|
57 |
+
// content = await fetch(
|
58 |
+
// `https://text.pollinations.ai/${prompt}?seed=${seed}&model=${model.name}`
|
59 |
+
// )
|
60 |
+
// .then((res) => res.text())
|
61 |
+
// .catch((err) => `Error fetching text: ${err.message}`);
|
62 |
+
// } else if (type === "image") {
|
63 |
+
// const combinedPrompt = `${message} ${visionDescription}`;
|
64 |
+
// const prompt = encodeURIComponent(combinedPrompt);
|
65 |
+
// imageUrl = `https://image.pollinations.ai/prompt/${prompt}?width=1024&height=1024&seed=${seed}&model=${model.name}`;
|
66 |
+
// }
|
67 |
+
} else if (type === "text") {
|
68 |
+
const prompt = encodeURIComponent(message);
|
69 |
+
content = await fetch(
|
70 |
+
`https://text.pollinations.ai/${prompt}?seed=${seed}&model=${model.name}`
|
71 |
+
)
|
72 |
+
.then((res) => res.text())
|
73 |
+
.catch((err) => `Error fetching text: ${err.message}`);
|
74 |
+
} else if (type === "image") {
|
75 |
+
const prompt = encodeURIComponent(message);
|
76 |
+
imageUrl = `https://image.pollinations.ai/prompt/${prompt}?width=1024&height=1024&seed=${seed}&model=${model.name}&nologo=true`;
|
77 |
+
} else {
|
78 |
+
return NextResponse.json(
|
79 |
+
{ error: "Invalid type. Use 'image' or 'text'." },
|
80 |
+
{ status: 400 }
|
81 |
+
);
|
82 |
+
}
|
83 |
+
|
84 |
+
return NextResponse.json({
|
85 |
+
type,
|
86 |
+
model,
|
87 |
+
seed,
|
88 |
+
imageUrl,
|
89 |
+
content,
|
90 |
+
visionDescription,
|
91 |
+
});
|
92 |
+
}
|
app/globals.css
ADDED
@@ -0,0 +1,94 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
@tailwind base;
|
2 |
+
@tailwind components;
|
3 |
+
@tailwind utilities;
|
4 |
+
|
5 |
+
body {
|
6 |
+
font-family: Arial, Helvetica, sans-serif;
|
7 |
+
}
|
8 |
+
|
9 |
+
@layer utilities {
|
10 |
+
.text-balance {
|
11 |
+
text-wrap: balance;
|
12 |
+
}
|
13 |
+
}
|
14 |
+
|
15 |
+
@layer base {
|
16 |
+
:root {
|
17 |
+
--background: 0 0% 100%;
|
18 |
+
--foreground: 0 0% 3.9%;
|
19 |
+
--card: 0 0% 100%;
|
20 |
+
--card-foreground: 0 0% 3.9%;
|
21 |
+
--popover: 0 0% 100%;
|
22 |
+
--popover-foreground: 0 0% 3.9%;
|
23 |
+
--primary: 0 0% 9%;
|
24 |
+
--primary-foreground: 0 0% 98%;
|
25 |
+
--secondary: 0 0% 96.1%;
|
26 |
+
--secondary-foreground: 0 0% 9%;
|
27 |
+
--muted: 0 0% 96.1%;
|
28 |
+
--muted-foreground: 0 0% 45.1%;
|
29 |
+
--accent: 0 0% 96.1%;
|
30 |
+
--accent-foreground: 0 0% 9%;
|
31 |
+
--destructive: 0 84.2% 60.2%;
|
32 |
+
--destructive-foreground: 0 0% 98%;
|
33 |
+
--border: 0 0% 89.8%;
|
34 |
+
--input: 0 0% 89.8%;
|
35 |
+
--ring: 0 0% 3.9%;
|
36 |
+
--chart-1: 12 76% 61%;
|
37 |
+
--chart-2: 173 58% 39%;
|
38 |
+
--chart-3: 197 37% 24%;
|
39 |
+
--chart-4: 43 74% 66%;
|
40 |
+
--chart-5: 27 87% 67%;
|
41 |
+
--radius: 0.5rem;
|
42 |
+
--sidebar-background: 0 0% 98%;
|
43 |
+
--sidebar-foreground: 240 5.3% 26.1%;
|
44 |
+
--sidebar-primary: 240 5.9% 10%;
|
45 |
+
--sidebar-primary-foreground: 0 0% 98%;
|
46 |
+
--sidebar-accent: 240 4.8% 95.9%;
|
47 |
+
--sidebar-accent-foreground: 240 5.9% 10%;
|
48 |
+
--sidebar-border: 220 13% 91%;
|
49 |
+
--sidebar-ring: 217.2 91.2% 59.8%;
|
50 |
+
}
|
51 |
+
.dark {
|
52 |
+
--background: 0 0% 3.9%;
|
53 |
+
--foreground: 0 0% 98%;
|
54 |
+
--card: 0 0% 3.9%;
|
55 |
+
--card-foreground: 0 0% 98%;
|
56 |
+
--popover: 0 0% 3.9%;
|
57 |
+
--popover-foreground: 0 0% 98%;
|
58 |
+
--primary: 0 0% 98%;
|
59 |
+
--primary-foreground: 0 0% 9%;
|
60 |
+
--secondary: 0 0% 14.9%;
|
61 |
+
--secondary-foreground: 0 0% 98%;
|
62 |
+
--muted: 0 0% 14.9%;
|
63 |
+
--muted-foreground: 0 0% 63.9%;
|
64 |
+
--accent: 0 0% 14.9%;
|
65 |
+
--accent-foreground: 0 0% 98%;
|
66 |
+
--destructive: 0 62.8% 30.6%;
|
67 |
+
--destructive-foreground: 0 0% 98%;
|
68 |
+
--border: 0 0% 14.9%;
|
69 |
+
--input: 0 0% 14.9%;
|
70 |
+
--ring: 0 0% 83.1%;
|
71 |
+
--chart-1: 220 70% 50%;
|
72 |
+
--chart-2: 160 60% 45%;
|
73 |
+
--chart-3: 30 80% 55%;
|
74 |
+
--chart-4: 280 65% 60%;
|
75 |
+
--chart-5: 340 75% 55%;
|
76 |
+
--sidebar-background: 240 5.9% 10%;
|
77 |
+
--sidebar-foreground: 240 4.8% 95.9%;
|
78 |
+
--sidebar-primary: 224.3 76.3% 48%;
|
79 |
+
--sidebar-primary-foreground: 0 0% 100%;
|
80 |
+
--sidebar-accent: 240 3.7% 15.9%;
|
81 |
+
--sidebar-accent-foreground: 240 4.8% 95.9%;
|
82 |
+
--sidebar-border: 240 3.7% 15.9%;
|
83 |
+
--sidebar-ring: 217.2 91.2% 59.8%;
|
84 |
+
}
|
85 |
+
}
|
86 |
+
|
87 |
+
@layer base {
|
88 |
+
* {
|
89 |
+
@apply border-border;
|
90 |
+
}
|
91 |
+
body {
|
92 |
+
@apply bg-background text-foreground;
|
93 |
+
}
|
94 |
+
}
|
app/layout.tsx
ADDED
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { ThemeProvider } from "@/components/theme-provider"
|
2 |
+
import './globals.css'
|
3 |
+
export default function RootLayout({ children }) {
|
4 |
+
return (
|
5 |
+
<html lang="en" suppressHydrationWarning>
|
6 |
+
<body>
|
7 |
+
<ThemeProvider attribute="class" defaultTheme="dark">
|
8 |
+
{children}
|
9 |
+
</ThemeProvider>
|
10 |
+
</body>
|
11 |
+
</html>
|
12 |
+
)
|
13 |
+
}
|
14 |
+
|
app/page.tsx
ADDED
@@ -0,0 +1,156 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
'use client'
|
2 |
+
|
3 |
+
import { useState, useEffect, useRef } from 'react'
|
4 |
+
import { useTheme } from 'next-themes'
|
5 |
+
import Header from '@/components/Header'
|
6 |
+
import ChatArea from '@/components/ChatArea'
|
7 |
+
import InputArea from '@/components/InputArea'
|
8 |
+
import Footer from '@/components/Footer'
|
9 |
+
import CookieConsent from '@/components/CookieConsent'
|
10 |
+
import { v4 as uuidv4 } from 'uuid'
|
11 |
+
|
12 |
+
export default function KarmaPollinations() {
|
13 |
+
const [messages, setMessages] = useState([])
|
14 |
+
const [textModels, setTextModels] = useState([])
|
15 |
+
const [imageModels, setImageModels] = useState([])
|
16 |
+
const { setTheme } = useTheme()
|
17 |
+
const chatAreaRef = useRef(null)
|
18 |
+
const [showCookieConsent, setShowCookieConsent] = useState(true)
|
19 |
+
|
20 |
+
useEffect(() => {
|
21 |
+
setTheme('dark')
|
22 |
+
fetchModels()
|
23 |
+
loadMessagesFromLocalStorage()
|
24 |
+
const consent = localStorage.getItem('cookieConsent')
|
25 |
+
if (consent === 'accepted') {
|
26 |
+
setShowCookieConsent(false)
|
27 |
+
}
|
28 |
+
}, [setTheme])
|
29 |
+
|
30 |
+
useEffect(() => {
|
31 |
+
fetchModels()
|
32 |
+
}, [])
|
33 |
+
|
34 |
+
useEffect(() => {
|
35 |
+
scrollToBottom()
|
36 |
+
}, [messages])
|
37 |
+
|
38 |
+
const fetchModels = async () => {
|
39 |
+
try {
|
40 |
+
const textResponse = await fetch('https://text.pollinations.ai/models')
|
41 |
+
const textData = await textResponse.json()
|
42 |
+
setTextModels(textData.map(model => ({ ...model, id: `text-${model.name}` })))
|
43 |
+
|
44 |
+
const imageResponse = await fetch('https://image.pollinations.ai/models')
|
45 |
+
const imageData = await imageResponse.json()
|
46 |
+
setImageModels(imageData.map((model, index) => ({ id: `image-${model}-${index}`, name: model, description: model })))
|
47 |
+
} catch (error) {
|
48 |
+
console.error('Error fetching models:', error)
|
49 |
+
}
|
50 |
+
}
|
51 |
+
|
52 |
+
const handleSendMessage = async (message, type, model, file) => {
|
53 |
+
const newMessage = {
|
54 |
+
id: uuidv4(),
|
55 |
+
content: message,
|
56 |
+
sender: 'user',
|
57 |
+
timestamp: new Date().toISOString(),
|
58 |
+
type,
|
59 |
+
model,
|
60 |
+
}
|
61 |
+
const updatedMessages = [...messages, newMessage]
|
62 |
+
setMessages(updatedMessages)
|
63 |
+
saveMessagesToLocalStorage(updatedMessages)
|
64 |
+
|
65 |
+
try {
|
66 |
+
const formData = new FormData()
|
67 |
+
formData.append('message', message)
|
68 |
+
formData.append('type', type)
|
69 |
+
formData.append('model', JSON.stringify(model))
|
70 |
+
if (file) {
|
71 |
+
formData.append('image', file)
|
72 |
+
}
|
73 |
+
|
74 |
+
const response = await fetch('/api/messages', {
|
75 |
+
method: 'POST',
|
76 |
+
body: formData,
|
77 |
+
})
|
78 |
+
|
79 |
+
if (!response.ok) {
|
80 |
+
throw new Error('Failed to process message')
|
81 |
+
}
|
82 |
+
|
83 |
+
const data = await response.json()
|
84 |
+
|
85 |
+
const assistantMessage = {
|
86 |
+
id: uuidv4(),
|
87 |
+
content: data.content,
|
88 |
+
sender: 'assistant',
|
89 |
+
timestamp: new Date().toISOString(),
|
90 |
+
type: data.type,
|
91 |
+
model: data.model,
|
92 |
+
}
|
93 |
+
|
94 |
+
if (data.imageUrl) {
|
95 |
+
assistantMessage.imageUrl = data.imageUrl
|
96 |
+
assistantMessage.imageText = data.imageText
|
97 |
+
}
|
98 |
+
|
99 |
+
const finalMessages = [...updatedMessages, assistantMessage]
|
100 |
+
setMessages(finalMessages)
|
101 |
+
saveMessagesToLocalStorage(finalMessages)
|
102 |
+
} catch (error) {
|
103 |
+
console.error('Error processing message:', error)
|
104 |
+
}
|
105 |
+
}
|
106 |
+
|
107 |
+
const scrollToBottom = () => {
|
108 |
+
if (chatAreaRef.current) {
|
109 |
+
chatAreaRef.current.scrollTo({
|
110 |
+
top: chatAreaRef.current.scrollHeight,
|
111 |
+
behavior: 'smooth',
|
112 |
+
})
|
113 |
+
}
|
114 |
+
}
|
115 |
+
|
116 |
+
const handleReset = () => {
|
117 |
+
localStorage.removeItem('chatMessages')
|
118 |
+
setMessages([])
|
119 |
+
}
|
120 |
+
|
121 |
+
const loadMessagesFromLocalStorage = () => {
|
122 |
+
const storedMessages = localStorage.getItem('chatMessages')
|
123 |
+
if (storedMessages) {
|
124 |
+
setMessages(JSON.parse(storedMessages))
|
125 |
+
}
|
126 |
+
}
|
127 |
+
|
128 |
+
const saveMessagesToLocalStorage = (messages) => {
|
129 |
+
localStorage.setItem('chatMessages', JSON.stringify(messages))
|
130 |
+
}
|
131 |
+
|
132 |
+
const handleAcceptCookies = () => {
|
133 |
+
setShowCookieConsent(false)
|
134 |
+
localStorage.setItem('cookieConsent', 'accepted')
|
135 |
+
}
|
136 |
+
|
137 |
+
return (
|
138 |
+
<div className="flex flex-col h-screen bg-background text-foreground">
|
139 |
+
{/* Fixed Header */}
|
140 |
+
<Header onReset={handleReset} className="fixed top-0 w-full z-10 shadow-md bg-primary text-white" />
|
141 |
+
|
142 |
+
{/* Main Content with Scrollable Center */}
|
143 |
+
<div className="flex-1 overflow-y-auto pt-16 pb-16">
|
144 |
+
<ChatArea messages={messages} chatAreaRef={chatAreaRef} />
|
145 |
+
</div>
|
146 |
+
|
147 |
+
{/* Fixed Footer */}
|
148 |
+
<InputArea
|
149 |
+
onSendMessage={handleSendMessage}
|
150 |
+
textModels={textModels}
|
151 |
+
imageModels={imageModels}
|
152 |
+
className="fixed bottom-0 w-full z-10 shadow-md bg-secondary text-white"
|
153 |
+
/>
|
154 |
+
</div>
|
155 |
+
)
|
156 |
+
}
|
components.json
ADDED
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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": "app/globals.css",
|
9 |
+
"baseColor": "neutral",
|
10 |
+
"cssVariables": true,
|
11 |
+
"prefix": ""
|
12 |
+
},
|
13 |
+
"aliases": {
|
14 |
+
"components": "@/components",
|
15 |
+
"utils": "@/lib/utils",
|
16 |
+
"ui": "@/components/ui",
|
17 |
+
"lib": "@/lib",
|
18 |
+
"hooks": "@/hooks"
|
19 |
+
},
|
20 |
+
"iconLibrary": "lucide"
|
21 |
+
}
|
components/ChatArea.tsx
ADDED
@@ -0,0 +1,121 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
'use client'
|
2 |
+
|
3 |
+
import { useState, useEffect, useRef } from 'react'
|
4 |
+
import { Copy, User } from 'lucide-react'
|
5 |
+
import { Button } from '@/components/ui/button'
|
6 |
+
import { motion } from 'framer-motion'
|
7 |
+
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
8 |
+
import ReactMarkdown from 'react-markdown' // For rendering markdown content
|
9 |
+
import remarkGfm from 'remark-gfm' // To support GitHub-flavored markdown (e.g., tables, strikethrough, etc.)
|
10 |
+
|
11 |
+
export default function ChatArea({ messages, chatAreaRef }) {
|
12 |
+
const [copiedId, setCopiedId] = useState(null) // State to track the copied message
|
13 |
+
const bottomRef = useRef(null) // Ref for auto-scroll to bottom
|
14 |
+
|
15 |
+
// Function to copy message content to the clipboard
|
16 |
+
const handleCopy = (content, id) => {
|
17 |
+
navigator.clipboard.writeText(content)
|
18 |
+
setCopiedId(id)
|
19 |
+
setTimeout(() => setCopiedId(null), 2000) // Reset copied state after 2 seconds
|
20 |
+
}
|
21 |
+
|
22 |
+
// Automatically scroll to the bottom whenever new messages arrive
|
23 |
+
useEffect(() => {
|
24 |
+
bottomRef.current?.scrollIntoView({ behavior: 'smooth' })
|
25 |
+
}, [messages])
|
26 |
+
|
27 |
+
return (
|
28 |
+
<div
|
29 |
+
ref={chatAreaRef}
|
30 |
+
className="flex-1 overflow-y-auto overflow-x-auto p-4 space-y-4" // Allows both horizontal and vertical scrolling
|
31 |
+
style={{ scrollBehavior: 'smooth' }} // Smooth scrolling for better UX
|
32 |
+
>
|
33 |
+
{messages.map((message) => (
|
34 |
+
<motion.div
|
35 |
+
key={message.id}
|
36 |
+
initial={{ opacity: 0, y: 50 }}
|
37 |
+
animate={{ opacity: 1, y: 0 }}
|
38 |
+
transition={{ duration: 0.5 }}
|
39 |
+
className={`flex ${message.sender === 'user' ? 'justify-end' : 'justify-start'}`}
|
40 |
+
>
|
41 |
+
<div
|
42 |
+
className={`flex items-start space-x-2 max-w-3xl ${message.sender === 'user' ? 'flex-row-reverse' : ''}`}
|
43 |
+
>
|
44 |
+
{/* Avatar for user or bot */}
|
45 |
+
<Avatar className="w-14 h-14">
|
46 |
+
{message.sender === 'user' ? (
|
47 |
+
<AvatarImage src="/yy.png" alt="User" />
|
48 |
+
) : (
|
49 |
+
<AvatarImage src="/kk.png" alt="Karma Bot" />
|
50 |
+
)}
|
51 |
+
<AvatarFallback>{message.sender === 'user' ? <User /> : 'K'}</AvatarFallback>
|
52 |
+
</Avatar>
|
53 |
+
|
54 |
+
{/* Message container */}
|
55 |
+
<div
|
56 |
+
className={`p-4 rounded-lg transition-all duration-300 ease-in-out hover:shadow-lg ${message.sender === 'user' ? 'bg-primary text-primary-foreground' : 'bg-secondary'
|
57 |
+
}`}
|
58 |
+
>
|
59 |
+
<div className="flex justify-between items-start mb-2">
|
60 |
+
<span className="font-bold">{message.sender === 'user' ? 'You' : 'Karma'}</span>
|
61 |
+
{/* Copy Button */}
|
62 |
+
|
63 |
+
{/* Copy confirmation */}
|
64 |
+
{copiedId === message.id ? (
|
65 |
+
<span className="text-xs text-green-500 mt-1">Copied ✅</span>
|
66 |
+
) : (
|
67 |
+
<Button
|
68 |
+
variant="ghost"
|
69 |
+
size="icon"
|
70 |
+
onClick={() => handleCopy(message.imageUrl ?? message.content, message.id)}
|
71 |
+
>
|
72 |
+
<Copy className="h-4 w-4" />
|
73 |
+
</Button>)}
|
74 |
+
</div>
|
75 |
+
|
76 |
+
|
77 |
+
{/* Image rendering with link */}
|
78 |
+
{message.imageUrl && (
|
79 |
+
<div className="mt-2">
|
80 |
+
<a
|
81 |
+
href={message.imageUrl}
|
82 |
+
target="_blank"
|
83 |
+
rel="noopener noreferrer"
|
84 |
+
title={message.content || 'Generated image'}
|
85 |
+
>
|
86 |
+
<img
|
87 |
+
src={message.imageUrl}
|
88 |
+
alt="Generated or uploaded image"
|
89 |
+
className="max-w-full h-auto rounded-lg"
|
90 |
+
/>
|
91 |
+
</a>
|
92 |
+
{message.imageText && (
|
93 |
+
<p className="mt-1 text-sm text-muted-foreground">{message.imageText}</p>
|
94 |
+
)}
|
95 |
+
</div>
|
96 |
+
)}
|
97 |
+
|
98 |
+
{/* Markdown rendering for message content */}
|
99 |
+
{message.content && (
|
100 |
+
<div className="mt-2 prose prose-sm text-muted-foreground">
|
101 |
+
<ReactMarkdown remarkPlugins={[remarkGfm]}>
|
102 |
+
{message.content}
|
103 |
+
</ReactMarkdown>
|
104 |
+
</div>
|
105 |
+
)}
|
106 |
+
|
107 |
+
{/* Metadata (model and timestamp) */}
|
108 |
+
<div className="mt-2 text-xs text-muted-foreground">
|
109 |
+
<p>model: {message.model?.description || 'N/A'} [{new Date(message.timestamp).toLocaleString()}] - {message.seed}</p>
|
110 |
+
</div>
|
111 |
+
|
112 |
+
|
113 |
+
</div>
|
114 |
+
</div>
|
115 |
+
</motion.div>
|
116 |
+
))}
|
117 |
+
{/* Ref for auto-scroll to bottom */}
|
118 |
+
<div ref={bottomRef} />
|
119 |
+
</div>
|
120 |
+
)
|
121 |
+
}
|
components/CookieConsent.tsx
ADDED
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
'use client'
|
2 |
+
|
3 |
+
import { useState } from 'react'
|
4 |
+
import { Button } from '@/components/ui/button'
|
5 |
+
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
6 |
+
|
7 |
+
export default function CookieConsent({ onAccept }) {
|
8 |
+
const [showPrivacyPolicy, setShowPrivacyPolicy] = useState(false)
|
9 |
+
|
10 |
+
return (
|
11 |
+
<Dialog open={true}>
|
12 |
+
<DialogContent>
|
13 |
+
<DialogHeader>
|
14 |
+
<DialogTitle>Cookie Consent</DialogTitle>
|
15 |
+
<DialogDescription>
|
16 |
+
We use cookies to enhance your browsing experience and store chat messages locally. By clicking "Accept", you agree to our use of cookies.
|
17 |
+
</DialogDescription>
|
18 |
+
</DialogHeader>
|
19 |
+
<DialogFooter>
|
20 |
+
<Button onClick={() => setShowPrivacyPolicy(true)}>Privacy Policy</Button>
|
21 |
+
<Button onClick={onAccept}>Accept</Button>
|
22 |
+
</DialogFooter>
|
23 |
+
</DialogContent>
|
24 |
+
{showPrivacyPolicy && (
|
25 |
+
<Dialog open={showPrivacyPolicy} onOpenChange={setShowPrivacyPolicy}>
|
26 |
+
<DialogContent>
|
27 |
+
<DialogHeader>
|
28 |
+
<DialogTitle>Privacy Policy</DialogTitle>
|
29 |
+
</DialogHeader>
|
30 |
+
<DialogDescription>
|
31 |
+
<p>At Karma-Pollinations, we respect your privacy and are committed to protecting your personal data. This privacy policy will inform you about how we collect, use, and protect your information when you use our Karma-Pollinations chatbox.
|
32 |
+
|
33 |
+
1. Data Collection: We collect and store your chat messages locally on your device using browser localStorage. This allows us to provide a seamless experience and maintain your conversation history.
|
34 |
+
|
35 |
+
2. Cookie Usage: We use cookies to remember your preferences and consent choices. These cookies are essential for the proper functioning of our service.
|
36 |
+
|
37 |
+
3. Data Usage: The information we collect is used solely to improve your experience with our chatbox. We do not sell or share your personal data with third parties.
|
38 |
+
|
39 |
+
4. Data Protection: We implement appropriate technical and organizational measures to ensure the security of your personal data.
|
40 |
+
|
41 |
+
5. User Rights: You have the right to access, rectify, or erase your personal data. You can clear your chat history at any time using the reset function.
|
42 |
+
|
43 |
+
6. Changes to Policy: We may update this privacy policy from time to time. We will notify you of any changes by posting the new policy on this page.
|
44 |
+
|
45 |
+
By using our Karma-Pollinations chatbox, you agree to the collection and use of information in accordance with this policy. If you have any questions about this privacy policy, please contact us.
|
46 |
+
</DialogDescription>
|
47 |
+
<DialogFooter>
|
48 |
+
<Button onClick={() => setShowPrivacyPolicy(false)}>Close</Button>
|
49 |
+
</DialogFooter>
|
50 |
+
</DialogContent>
|
51 |
+
</Dialog>
|
52 |
+
)}
|
53 |
+
</Dialog>
|
54 |
+
)
|
55 |
+
}
|
56 |
+
|
components/Footer.tsx
ADDED
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { FaDiscord, FaWhatsapp, FaInstagram, FaTiktok, FaGithub } from 'react-icons/fa'
|
2 |
+
|
3 |
+
export default function Footer() {
|
4 |
+
return (
|
5 |
+
<footer className="mt-3">
|
6 |
+
<div className="flex flex-col sm:flex-row justify-center items-center text-center sm:text-left">
|
7 |
+
<p className="text-sm text-muted-foreground mb-2 sm:mb-0">
|
8 |
+
Coded by <a href="https://karma.yt/?pollinations">Karma.yt</a> with <a href="https://pollinations.ai/#karma">Pollinations.ai</a>
|
9 |
+
</p>
|
10 |
+
</div>
|
11 |
+
<div className="flex justify-center space-x-4 mt-2">
|
12 |
+
<a href="https://discord.gg/BZWmGNK44Q" className="text-muted-foreground hover:text-foreground">
|
13 |
+
<FaDiscord />
|
14 |
+
</a>
|
15 |
+
<a href="https://chat.whatsapp.com/JxQEn2FKDny0DdwkDuzoQR" className="text-muted-foreground hover:text-foreground">
|
16 |
+
<FaWhatsapp />
|
17 |
+
</a>
|
18 |
+
<a href="https://www.github.com/pollinations/pollinations" className="text-muted-foreground hover:text-foreground">
|
19 |
+
<FaGithub />
|
20 |
+
</a>
|
21 |
+
<a href="https://instagram.com/pollinations_ai" className="text-muted-foreground hover:text-foreground">
|
22 |
+
<FaInstagram />
|
23 |
+
</a>
|
24 |
+
<a href="https://tiktok.com/@pollinations.ai" className="text-muted-foreground hover:text-foreground">
|
25 |
+
<FaTiktok />
|
26 |
+
</a>
|
27 |
+
</div>
|
28 |
+
</footer>
|
29 |
+
|
30 |
+
)
|
31 |
+
}
|
32 |
+
|
components/Header.tsx
ADDED
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
'use client'
|
2 |
+
|
3 |
+
import { Moon, Sun, RefreshCw } from 'lucide-react'
|
4 |
+
import { Button } from '@/components/ui/button'
|
5 |
+
import { useTheme } from 'next-themes'
|
6 |
+
import { SharePopover } from './SharePopover'
|
7 |
+
|
8 |
+
export default function Header({ onReset }) {
|
9 |
+
const { theme, setTheme } = useTheme()
|
10 |
+
|
11 |
+
return (
|
12 |
+
<header className="flex items-center justify-between p-4 border-b">
|
13 |
+
<div className="flex items-center">
|
14 |
+
<img src="/karma.png" alt="Karma-Pollinations Logo" className="w-16 h-12 mr-2" />
|
15 |
+
</div>
|
16 |
+
<h1 className="text-xl font-bold">karma.pollinations.ai</h1>
|
17 |
+
<div className="flex items-center space-x-2">
|
18 |
+
{/* <Button variant="ghost" size="icon" onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
|
19 |
+
{theme === 'dark' ? <Sun className="h-5 w-5" /> : <Moon className="h-5 w-5" />}
|
20 |
+
</Button> */}
|
21 |
+
<Button variant="ghost" size="icon" onClick={onReset}>
|
22 |
+
<RefreshCw className="h-5 w-5" />
|
23 |
+
</Button>
|
24 |
+
<SharePopover />
|
25 |
+
</div>
|
26 |
+
</header>
|
27 |
+
)
|
28 |
+
}
|
29 |
+
|
components/InputArea.tsx
ADDED
@@ -0,0 +1,154 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
'use client'
|
2 |
+
|
3 |
+
import { useState, useEffect, ChangeEvent, FormEvent } from 'react'
|
4 |
+
import { Send, Paperclip } from 'lucide-react'
|
5 |
+
import { Button } from '@/components/ui/button'
|
6 |
+
import { Input } from '@/components/ui/input'
|
7 |
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
8 |
+
import { motion } from 'framer-motion'
|
9 |
+
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
|
10 |
+
import Footer from './Footer'
|
11 |
+
|
12 |
+
// Tipos para os modelos de texto e imagem
|
13 |
+
interface Model {
|
14 |
+
id: string
|
15 |
+
description: string
|
16 |
+
}
|
17 |
+
|
18 |
+
interface FileData {
|
19 |
+
base64: string | ArrayBuffer | null
|
20 |
+
type: string
|
21 |
+
name: string
|
22 |
+
}
|
23 |
+
|
24 |
+
interface InputAreaProps {
|
25 |
+
onSendMessage: (message: string, type: 'text' | 'image', model: Model | undefined, fileBase64: string | null) => void
|
26 |
+
textModels: Model[]
|
27 |
+
imageModels: Model[]
|
28 |
+
}
|
29 |
+
|
30 |
+
export default function InputArea({ onSendMessage, textModels, imageModels }: InputAreaProps) {
|
31 |
+
const [message, setMessage] = useState<string>('')
|
32 |
+
const [type, setType] = useState<'text' | 'image'>('text')
|
33 |
+
const [selectedModel, setSelectedModel] = useState<string>(textModels[0]?.id || '')
|
34 |
+
const [file, setFile] = useState<FileData | null>(null)
|
35 |
+
|
36 |
+
useEffect(() => {
|
37 |
+
if (textModels.length > 0 && type === 'text') {
|
38 |
+
setSelectedModel(textModels[0].id)
|
39 |
+
} else if (imageModels.length > 0 && type === 'image') {
|
40 |
+
setSelectedModel(imageModels[0].id)
|
41 |
+
}
|
42 |
+
}, [type, textModels, imageModels])
|
43 |
+
|
44 |
+
const handleSubmit = (e: FormEvent) => {
|
45 |
+
e.preventDefault()
|
46 |
+
if (message.trim() || file) {
|
47 |
+
const model = type === 'text'
|
48 |
+
? textModels.find(m => m.id === selectedModel)
|
49 |
+
: imageModels.find(m => m.id === selectedModel)
|
50 |
+
|
51 |
+
onSendMessage(message, type, model, file ? file.base64 : null)
|
52 |
+
setMessage('')
|
53 |
+
setFile(null)
|
54 |
+
}
|
55 |
+
}
|
56 |
+
|
57 |
+
const handleFileChange = (e: ChangeEvent<HTMLInputElement>) => {
|
58 |
+
const selectedFile = e.target.files?.[0]
|
59 |
+
if (selectedFile) {
|
60 |
+
// Verifica o tamanho do arquivo (4MB)
|
61 |
+
if (selectedFile.size > 4 * 1024 * 1024) {
|
62 |
+
alert('File size exceeds 4MB limit.')
|
63 |
+
return
|
64 |
+
}
|
65 |
+
|
66 |
+
// Cria o Base64 do arquivo com mimetype
|
67 |
+
const reader = new FileReader()
|
68 |
+
reader.onloadend = () => {
|
69 |
+
setFile({
|
70 |
+
base64: reader.result,
|
71 |
+
type: selectedFile.type,
|
72 |
+
name: selectedFile.name,
|
73 |
+
})
|
74 |
+
}
|
75 |
+
reader.readAsDataURL(selectedFile)
|
76 |
+
}
|
77 |
+
}
|
78 |
+
|
79 |
+
const handleTypeChange = (newType: 'text' | 'image') => {
|
80 |
+
setType(newType)
|
81 |
+
setSelectedModel(newType === 'text' ? textModels[0]?.id : imageModels[0]?.id)
|
82 |
+
}
|
83 |
+
|
84 |
+
return (
|
85 |
+
<form onSubmit={handleSubmit} className="p-4 border-t">
|
86 |
+
<div className="flex items-center space-x-2 mb-2">
|
87 |
+
<Input
|
88 |
+
type="text"
|
89 |
+
value={message}
|
90 |
+
onChange={(e) => setMessage(e.target.value)}
|
91 |
+
placeholder="Type your message here..."
|
92 |
+
className="flex-grow"
|
93 |
+
/>
|
94 |
+
<Select value={type} onValueChange={handleTypeChange}>
|
95 |
+
<SelectTrigger className="w-[100px]">
|
96 |
+
<SelectValue placeholder="Type" />
|
97 |
+
</SelectTrigger>
|
98 |
+
<SelectContent>
|
99 |
+
<SelectItem value="text">Text</SelectItem>
|
100 |
+
<SelectItem value="image">Image</SelectItem>
|
101 |
+
</SelectContent>
|
102 |
+
</Select>
|
103 |
+
<Select value={selectedModel} onValueChange={setSelectedModel}>
|
104 |
+
<SelectTrigger className="w-[200px]">
|
105 |
+
<SelectValue placeholder="Select model" />
|
106 |
+
</SelectTrigger>
|
107 |
+
<SelectContent>
|
108 |
+
{(type === 'text' ? textModels : imageModels).map((model, i) => (
|
109 |
+
<SelectItem key={`${type}-${model.id}-${i}`} value={model.id}>
|
110 |
+
{model.description}
|
111 |
+
</SelectItem>
|
112 |
+
))}
|
113 |
+
</SelectContent>
|
114 |
+
</Select>
|
115 |
+
<motion.div
|
116 |
+
whileHover={{ scale: 1.05 }}
|
117 |
+
whileTap={{ scale: 0.95 }}
|
118 |
+
>
|
119 |
+
<Button type="submit">
|
120 |
+
<Send className="h-4 w-4" />
|
121 |
+
</Button>
|
122 |
+
</motion.div>
|
123 |
+
</div>
|
124 |
+
<div className="flex items-center justify-between hidden">
|
125 |
+
<div className="flex items-center space-x-2">
|
126 |
+
<Input
|
127 |
+
type="file"
|
128 |
+
id="file-upload"
|
129 |
+
className="hidden"
|
130 |
+
onChange={handleFileChange}
|
131 |
+
accept="image/*"
|
132 |
+
/>
|
133 |
+
<TooltipProvider>
|
134 |
+
<Tooltip>
|
135 |
+
<TooltipTrigger asChild>
|
136 |
+
<Button variant="outline" size="sm" asChild>
|
137 |
+
<label htmlFor="file-upload" className="cursor-pointer">
|
138 |
+
<Paperclip className="h-4 w-4 mr-2" />
|
139 |
+
Attach Image
|
140 |
+
</label>
|
141 |
+
</Button>
|
142 |
+
</TooltipTrigger>
|
143 |
+
<TooltipContent>
|
144 |
+
<p>Attached images will influence text and model interpretation</p>
|
145 |
+
</TooltipContent>
|
146 |
+
</Tooltip>
|
147 |
+
</TooltipProvider>
|
148 |
+
{file && <span className="text-sm text-muted-foreground">{file.name}</span>}
|
149 |
+
</div>
|
150 |
+
</div>
|
151 |
+
<Footer />
|
152 |
+
</form>
|
153 |
+
)
|
154 |
+
}
|
components/SharePopover.tsx
ADDED
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
'use client'
|
2 |
+
import { useState } from 'react'
|
3 |
+
import { Share, Copy } from 'lucide-react'
|
4 |
+
import { Button } from '@/components/ui/button'
|
5 |
+
import { Input } from '@/components/ui/input'
|
6 |
+
import {
|
7 |
+
Popover,
|
8 |
+
PopoverContent,
|
9 |
+
PopoverTrigger,
|
10 |
+
} from '@/components/ui/popover'
|
11 |
+
|
12 |
+
export function SharePopover() {
|
13 |
+
const [copied, setCopied] = useState(false)
|
14 |
+
const url = typeof window !== 'undefined' ? window.location.href : ''
|
15 |
+
|
16 |
+
const handleCopy = () => {
|
17 |
+
navigator.clipboard.writeText(url)
|
18 |
+
setCopied(true)
|
19 |
+
setTimeout(() => setCopied(false), 2000)
|
20 |
+
}
|
21 |
+
|
22 |
+
return (
|
23 |
+
<Popover>
|
24 |
+
<PopoverTrigger asChild>
|
25 |
+
<Button variant="ghost" size="icon">
|
26 |
+
<Share className="h-5 w-5" />
|
27 |
+
</Button>
|
28 |
+
</PopoverTrigger>
|
29 |
+
<PopoverContent className="w-80">
|
30 |
+
<div className="space-y-2">
|
31 |
+
<h3 className="font-medium">Share this conversation</h3>
|
32 |
+
<div className="flex space-x-2">
|
33 |
+
<Input value={url} readOnly />
|
34 |
+
<Button size="icon" onClick={handleCopy}>
|
35 |
+
<Copy className="h-4 w-4" />
|
36 |
+
</Button>
|
37 |
+
</div>
|
38 |
+
{copied && <p className="text-sm text-green-500">Copied to clipboard!</p>}
|
39 |
+
</div>
|
40 |
+
</PopoverContent>
|
41 |
+
</Popover>
|
42 |
+
)
|
43 |
+
}
|
44 |
+
|
components/theme-provider.tsx
ADDED
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
'use client'
|
2 |
+
|
3 |
+
import * as React from 'react'
|
4 |
+
import { ThemeProvider as NextThemesProvider } from 'next-themes'
|
5 |
+
import { type ThemeProviderProps } from 'next-themes'
|
6 |
+
|
7 |
+
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
8 |
+
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
|
9 |
+
}
|
10 |
+
|
components/ui/accordion.tsx
ADDED
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
|
50 |
+
{...props}
|
51 |
+
>
|
52 |
+
<div className={cn("pb-4 pt-0", className)}>{children}</div>
|
53 |
+
</AccordionPrimitive.Content>
|
54 |
+
))
|
55 |
+
|
56 |
+
AccordionContent.displayName = AccordionPrimitive.Content.displayName
|
57 |
+
|
58 |
+
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
|
components/ui/alert-dialog.tsx
ADDED
@@ -0,0 +1,141 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
|
3 |
+
import * as React from "react"
|
4 |
+
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
|
5 |
+
|
6 |
+
import { cn } from "@/lib/utils"
|
7 |
+
import { buttonVariants } from "@/components/ui/button"
|
8 |
+
|
9 |
+
const AlertDialog = AlertDialogPrimitive.Root
|
10 |
+
|
11 |
+
const AlertDialogTrigger = AlertDialogPrimitive.Trigger
|
12 |
+
|
13 |
+
const AlertDialogPortal = AlertDialogPrimitive.Portal
|
14 |
+
|
15 |
+
const AlertDialogOverlay = React.forwardRef<
|
16 |
+
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
|
17 |
+
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
|
18 |
+
>(({ className, ...props }, ref) => (
|
19 |
+
<AlertDialogPrimitive.Overlay
|
20 |
+
className={cn(
|
21 |
+
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
22 |
+
className
|
23 |
+
)}
|
24 |
+
{...props}
|
25 |
+
ref={ref}
|
26 |
+
/>
|
27 |
+
))
|
28 |
+
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
|
29 |
+
|
30 |
+
const AlertDialogContent = React.forwardRef<
|
31 |
+
React.ElementRef<typeof AlertDialogPrimitive.Content>,
|
32 |
+
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
|
33 |
+
>(({ className, ...props }, ref) => (
|
34 |
+
<AlertDialogPortal>
|
35 |
+
<AlertDialogOverlay />
|
36 |
+
<AlertDialogPrimitive.Content
|
37 |
+
ref={ref}
|
38 |
+
className={cn(
|
39 |
+
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background 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",
|
40 |
+
className
|
41 |
+
)}
|
42 |
+
{...props}
|
43 |
+
/>
|
44 |
+
</AlertDialogPortal>
|
45 |
+
))
|
46 |
+
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
|
47 |
+
|
48 |
+
const AlertDialogHeader = ({
|
49 |
+
className,
|
50 |
+
...props
|
51 |
+
}: React.HTMLAttributes<HTMLDivElement>) => (
|
52 |
+
<div
|
53 |
+
className={cn(
|
54 |
+
"flex flex-col space-y-2 text-center sm:text-left",
|
55 |
+
className
|
56 |
+
)}
|
57 |
+
{...props}
|
58 |
+
/>
|
59 |
+
)
|
60 |
+
AlertDialogHeader.displayName = "AlertDialogHeader"
|
61 |
+
|
62 |
+
const AlertDialogFooter = ({
|
63 |
+
className,
|
64 |
+
...props
|
65 |
+
}: React.HTMLAttributes<HTMLDivElement>) => (
|
66 |
+
<div
|
67 |
+
className={cn(
|
68 |
+
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
69 |
+
className
|
70 |
+
)}
|
71 |
+
{...props}
|
72 |
+
/>
|
73 |
+
)
|
74 |
+
AlertDialogFooter.displayName = "AlertDialogFooter"
|
75 |
+
|
76 |
+
const AlertDialogTitle = React.forwardRef<
|
77 |
+
React.ElementRef<typeof AlertDialogPrimitive.Title>,
|
78 |
+
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
|
79 |
+
>(({ className, ...props }, ref) => (
|
80 |
+
<AlertDialogPrimitive.Title
|
81 |
+
ref={ref}
|
82 |
+
className={cn("text-lg font-semibold", className)}
|
83 |
+
{...props}
|
84 |
+
/>
|
85 |
+
))
|
86 |
+
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
|
87 |
+
|
88 |
+
const AlertDialogDescription = React.forwardRef<
|
89 |
+
React.ElementRef<typeof AlertDialogPrimitive.Description>,
|
90 |
+
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
|
91 |
+
>(({ className, ...props }, ref) => (
|
92 |
+
<AlertDialogPrimitive.Description
|
93 |
+
ref={ref}
|
94 |
+
className={cn("text-sm text-muted-foreground", className)}
|
95 |
+
{...props}
|
96 |
+
/>
|
97 |
+
))
|
98 |
+
AlertDialogDescription.displayName =
|
99 |
+
AlertDialogPrimitive.Description.displayName
|
100 |
+
|
101 |
+
const AlertDialogAction = React.forwardRef<
|
102 |
+
React.ElementRef<typeof AlertDialogPrimitive.Action>,
|
103 |
+
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
|
104 |
+
>(({ className, ...props }, ref) => (
|
105 |
+
<AlertDialogPrimitive.Action
|
106 |
+
ref={ref}
|
107 |
+
className={cn(buttonVariants(), className)}
|
108 |
+
{...props}
|
109 |
+
/>
|
110 |
+
))
|
111 |
+
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
|
112 |
+
|
113 |
+
const AlertDialogCancel = React.forwardRef<
|
114 |
+
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
|
115 |
+
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
|
116 |
+
>(({ className, ...props }, ref) => (
|
117 |
+
<AlertDialogPrimitive.Cancel
|
118 |
+
ref={ref}
|
119 |
+
className={cn(
|
120 |
+
buttonVariants({ variant: "outline" }),
|
121 |
+
"mt-2 sm:mt-0",
|
122 |
+
className
|
123 |
+
)}
|
124 |
+
{...props}
|
125 |
+
/>
|
126 |
+
))
|
127 |
+
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
|
128 |
+
|
129 |
+
export {
|
130 |
+
AlertDialog,
|
131 |
+
AlertDialogPortal,
|
132 |
+
AlertDialogOverlay,
|
133 |
+
AlertDialogTrigger,
|
134 |
+
AlertDialogContent,
|
135 |
+
AlertDialogHeader,
|
136 |
+
AlertDialogFooter,
|
137 |
+
AlertDialogTitle,
|
138 |
+
AlertDialogDescription,
|
139 |
+
AlertDialogAction,
|
140 |
+
AlertDialogCancel,
|
141 |
+
}
|
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 p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
|
8 |
+
{
|
9 |
+
variants: {
|
10 |
+
variant: {
|
11 |
+
default: "bg-background text-foreground",
|
12 |
+
destructive:
|
13 |
+
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
|
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 }
|
components/ui/aspect-ratio.tsx
ADDED
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
|
3 |
+
import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
|
4 |
+
|
5 |
+
const AspectRatio = AspectRatioPrimitive.Root
|
6 |
+
|
7 |
+
export { AspectRatio }
|
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-muted",
|
43 |
+
className
|
44 |
+
)}
|
45 |
+
{...props}
|
46 |
+
/>
|
47 |
+
))
|
48 |
+
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
|
49 |
+
|
50 |
+
export { Avatar, AvatarImage, AvatarFallback }
|
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 px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
8 |
+
{
|
9 |
+
variants: {
|
10 |
+
variant: {
|
11 |
+
default:
|
12 |
+
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
|
13 |
+
secondary:
|
14 |
+
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
15 |
+
destructive:
|
16 |
+
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
|
17 |
+
outline: "text-foreground",
|
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 }
|
components/ui/breadcrumb.tsx
ADDED
@@ -0,0 +1,115 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import * as React from "react"
|
2 |
+
import { Slot } from "@radix-ui/react-slot"
|
3 |
+
import { ChevronRight, MoreHorizontal } from "lucide-react"
|
4 |
+
|
5 |
+
import { cn } from "@/lib/utils"
|
6 |
+
|
7 |
+
const Breadcrumb = React.forwardRef<
|
8 |
+
HTMLElement,
|
9 |
+
React.ComponentPropsWithoutRef<"nav"> & {
|
10 |
+
separator?: React.ReactNode
|
11 |
+
}
|
12 |
+
>(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />)
|
13 |
+
Breadcrumb.displayName = "Breadcrumb"
|
14 |
+
|
15 |
+
const BreadcrumbList = React.forwardRef<
|
16 |
+
HTMLOListElement,
|
17 |
+
React.ComponentPropsWithoutRef<"ol">
|
18 |
+
>(({ className, ...props }, ref) => (
|
19 |
+
<ol
|
20 |
+
ref={ref}
|
21 |
+
className={cn(
|
22 |
+
"flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5",
|
23 |
+
className
|
24 |
+
)}
|
25 |
+
{...props}
|
26 |
+
/>
|
27 |
+
))
|
28 |
+
BreadcrumbList.displayName = "BreadcrumbList"
|
29 |
+
|
30 |
+
const BreadcrumbItem = React.forwardRef<
|
31 |
+
HTMLLIElement,
|
32 |
+
React.ComponentPropsWithoutRef<"li">
|
33 |
+
>(({ className, ...props }, ref) => (
|
34 |
+
<li
|
35 |
+
ref={ref}
|
36 |
+
className={cn("inline-flex items-center gap-1.5", className)}
|
37 |
+
{...props}
|
38 |
+
/>
|
39 |
+
))
|
40 |
+
BreadcrumbItem.displayName = "BreadcrumbItem"
|
41 |
+
|
42 |
+
const BreadcrumbLink = React.forwardRef<
|
43 |
+
HTMLAnchorElement,
|
44 |
+
React.ComponentPropsWithoutRef<"a"> & {
|
45 |
+
asChild?: boolean
|
46 |
+
}
|
47 |
+
>(({ asChild, className, ...props }, ref) => {
|
48 |
+
const Comp = asChild ? Slot : "a"
|
49 |
+
|
50 |
+
return (
|
51 |
+
<Comp
|
52 |
+
ref={ref}
|
53 |
+
className={cn("transition-colors hover:text-foreground", className)}
|
54 |
+
{...props}
|
55 |
+
/>
|
56 |
+
)
|
57 |
+
})
|
58 |
+
BreadcrumbLink.displayName = "BreadcrumbLink"
|
59 |
+
|
60 |
+
const BreadcrumbPage = React.forwardRef<
|
61 |
+
HTMLSpanElement,
|
62 |
+
React.ComponentPropsWithoutRef<"span">
|
63 |
+
>(({ className, ...props }, ref) => (
|
64 |
+
<span
|
65 |
+
ref={ref}
|
66 |
+
role="link"
|
67 |
+
aria-disabled="true"
|
68 |
+
aria-current="page"
|
69 |
+
className={cn("font-normal text-foreground", className)}
|
70 |
+
{...props}
|
71 |
+
/>
|
72 |
+
))
|
73 |
+
BreadcrumbPage.displayName = "BreadcrumbPage"
|
74 |
+
|
75 |
+
const BreadcrumbSeparator = ({
|
76 |
+
children,
|
77 |
+
className,
|
78 |
+
...props
|
79 |
+
}: React.ComponentProps<"li">) => (
|
80 |
+
<li
|
81 |
+
role="presentation"
|
82 |
+
aria-hidden="true"
|
83 |
+
className={cn("[&>svg]:w-3.5 [&>svg]:h-3.5", className)}
|
84 |
+
{...props}
|
85 |
+
>
|
86 |
+
{children ?? <ChevronRight />}
|
87 |
+
</li>
|
88 |
+
)
|
89 |
+
BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
|
90 |
+
|
91 |
+
const BreadcrumbEllipsis = ({
|
92 |
+
className,
|
93 |
+
...props
|
94 |
+
}: React.ComponentProps<"span">) => (
|
95 |
+
<span
|
96 |
+
role="presentation"
|
97 |
+
aria-hidden="true"
|
98 |
+
className={cn("flex h-9 w-9 items-center justify-center", className)}
|
99 |
+
{...props}
|
100 |
+
>
|
101 |
+
<MoreHorizontal className="h-4 w-4" />
|
102 |
+
<span className="sr-only">More</span>
|
103 |
+
</span>
|
104 |
+
)
|
105 |
+
BreadcrumbEllipsis.displayName = "BreadcrumbElipssis"
|
106 |
+
|
107 |
+
export {
|
108 |
+
Breadcrumb,
|
109 |
+
BreadcrumbList,
|
110 |
+
BreadcrumbItem,
|
111 |
+
BreadcrumbLink,
|
112 |
+
BreadcrumbPage,
|
113 |
+
BreadcrumbSeparator,
|
114 |
+
BreadcrumbEllipsis,
|
115 |
+
}
|
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 gap-2 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 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
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 }
|
components/ui/calendar.tsx
ADDED
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
|
3 |
+
import * as React from "react"
|
4 |
+
import { ChevronLeft, ChevronRight } from "lucide-react"
|
5 |
+
import { DayPicker } from "react-day-picker"
|
6 |
+
|
7 |
+
import { cn } from "@/lib/utils"
|
8 |
+
import { buttonVariants } from "@/components/ui/button"
|
9 |
+
|
10 |
+
export type CalendarProps = React.ComponentProps<typeof DayPicker>
|
11 |
+
|
12 |
+
function Calendar({
|
13 |
+
className,
|
14 |
+
classNames,
|
15 |
+
showOutsideDays = true,
|
16 |
+
...props
|
17 |
+
}: CalendarProps) {
|
18 |
+
return (
|
19 |
+
<DayPicker
|
20 |
+
showOutsideDays={showOutsideDays}
|
21 |
+
className={cn("p-3", className)}
|
22 |
+
classNames={{
|
23 |
+
months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
|
24 |
+
month: "space-y-4",
|
25 |
+
caption: "flex justify-center pt-1 relative items-center",
|
26 |
+
caption_label: "text-sm font-medium",
|
27 |
+
nav: "space-x-1 flex items-center",
|
28 |
+
nav_button: cn(
|
29 |
+
buttonVariants({ variant: "outline" }),
|
30 |
+
"h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100"
|
31 |
+
),
|
32 |
+
nav_button_previous: "absolute left-1",
|
33 |
+
nav_button_next: "absolute right-1",
|
34 |
+
table: "w-full border-collapse space-y-1",
|
35 |
+
head_row: "flex",
|
36 |
+
head_cell:
|
37 |
+
"text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]",
|
38 |
+
row: "flex w-full mt-2",
|
39 |
+
cell: "h-9 w-9 text-center text-sm p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20",
|
40 |
+
day: cn(
|
41 |
+
buttonVariants({ variant: "ghost" }),
|
42 |
+
"h-9 w-9 p-0 font-normal aria-selected:opacity-100"
|
43 |
+
),
|
44 |
+
day_range_end: "day-range-end",
|
45 |
+
day_selected:
|
46 |
+
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
|
47 |
+
day_today: "bg-accent text-accent-foreground",
|
48 |
+
day_outside:
|
49 |
+
"day-outside text-muted-foreground aria-selected:bg-accent/50 aria-selected:text-muted-foreground",
|
50 |
+
day_disabled: "text-muted-foreground opacity-50",
|
51 |
+
day_range_middle:
|
52 |
+
"aria-selected:bg-accent aria-selected:text-accent-foreground",
|
53 |
+
day_hidden: "invisible",
|
54 |
+
...classNames,
|
55 |
+
}}
|
56 |
+
components={{
|
57 |
+
IconLeft: ({ ...props }) => <ChevronLeft className="h-4 w-4" />,
|
58 |
+
IconRight: ({ ...props }) => <ChevronRight className="h-4 w-4" />,
|
59 |
+
}}
|
60 |
+
{...props}
|
61 |
+
/>
|
62 |
+
)
|
63 |
+
}
|
64 |
+
Calendar.displayName = "Calendar"
|
65 |
+
|
66 |
+
export { Calendar }
|
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 |
+
HTMLDivElement,
|
34 |
+
React.HTMLAttributes<HTMLDivElement>
|
35 |
+
>(({ className, ...props }, ref) => (
|
36 |
+
<div
|
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 |
+
HTMLDivElement,
|
49 |
+
React.HTMLAttributes<HTMLDivElement>
|
50 |
+
>(({ className, ...props }, ref) => (
|
51 |
+
<div
|
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 }
|
components/ui/carousel.tsx
ADDED
@@ -0,0 +1,262 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
|
3 |
+
import * as React from "react"
|
4 |
+
import useEmblaCarousel, {
|
5 |
+
type UseEmblaCarouselType,
|
6 |
+
} from "embla-carousel-react"
|
7 |
+
import { ArrowLeft, ArrowRight } from "lucide-react"
|
8 |
+
|
9 |
+
import { cn } from "@/lib/utils"
|
10 |
+
import { Button } from "@/components/ui/button"
|
11 |
+
|
12 |
+
type CarouselApi = UseEmblaCarouselType[1]
|
13 |
+
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>
|
14 |
+
type CarouselOptions = UseCarouselParameters[0]
|
15 |
+
type CarouselPlugin = UseCarouselParameters[1]
|
16 |
+
|
17 |
+
type CarouselProps = {
|
18 |
+
opts?: CarouselOptions
|
19 |
+
plugins?: CarouselPlugin
|
20 |
+
orientation?: "horizontal" | "vertical"
|
21 |
+
setApi?: (api: CarouselApi) => void
|
22 |
+
}
|
23 |
+
|
24 |
+
type CarouselContextProps = {
|
25 |
+
carouselRef: ReturnType<typeof useEmblaCarousel>[0]
|
26 |
+
api: ReturnType<typeof useEmblaCarousel>[1]
|
27 |
+
scrollPrev: () => void
|
28 |
+
scrollNext: () => void
|
29 |
+
canScrollPrev: boolean
|
30 |
+
canScrollNext: boolean
|
31 |
+
} & CarouselProps
|
32 |
+
|
33 |
+
const CarouselContext = React.createContext<CarouselContextProps | null>(null)
|
34 |
+
|
35 |
+
function useCarousel() {
|
36 |
+
const context = React.useContext(CarouselContext)
|
37 |
+
|
38 |
+
if (!context) {
|
39 |
+
throw new Error("useCarousel must be used within a <Carousel />")
|
40 |
+
}
|
41 |
+
|
42 |
+
return context
|
43 |
+
}
|
44 |
+
|
45 |
+
const Carousel = React.forwardRef<
|
46 |
+
HTMLDivElement,
|
47 |
+
React.HTMLAttributes<HTMLDivElement> & CarouselProps
|
48 |
+
>(
|
49 |
+
(
|
50 |
+
{
|
51 |
+
orientation = "horizontal",
|
52 |
+
opts,
|
53 |
+
setApi,
|
54 |
+
plugins,
|
55 |
+
className,
|
56 |
+
children,
|
57 |
+
...props
|
58 |
+
},
|
59 |
+
ref
|
60 |
+
) => {
|
61 |
+
const [carouselRef, api] = useEmblaCarousel(
|
62 |
+
{
|
63 |
+
...opts,
|
64 |
+
axis: orientation === "horizontal" ? "x" : "y",
|
65 |
+
},
|
66 |
+
plugins
|
67 |
+
)
|
68 |
+
const [canScrollPrev, setCanScrollPrev] = React.useState(false)
|
69 |
+
const [canScrollNext, setCanScrollNext] = React.useState(false)
|
70 |
+
|
71 |
+
const onSelect = React.useCallback((api: CarouselApi) => {
|
72 |
+
if (!api) {
|
73 |
+
return
|
74 |
+
}
|
75 |
+
|
76 |
+
setCanScrollPrev(api.canScrollPrev())
|
77 |
+
setCanScrollNext(api.canScrollNext())
|
78 |
+
}, [])
|
79 |
+
|
80 |
+
const scrollPrev = React.useCallback(() => {
|
81 |
+
api?.scrollPrev()
|
82 |
+
}, [api])
|
83 |
+
|
84 |
+
const scrollNext = React.useCallback(() => {
|
85 |
+
api?.scrollNext()
|
86 |
+
}, [api])
|
87 |
+
|
88 |
+
const handleKeyDown = React.useCallback(
|
89 |
+
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
90 |
+
if (event.key === "ArrowLeft") {
|
91 |
+
event.preventDefault()
|
92 |
+
scrollPrev()
|
93 |
+
} else if (event.key === "ArrowRight") {
|
94 |
+
event.preventDefault()
|
95 |
+
scrollNext()
|
96 |
+
}
|
97 |
+
},
|
98 |
+
[scrollPrev, scrollNext]
|
99 |
+
)
|
100 |
+
|
101 |
+
React.useEffect(() => {
|
102 |
+
if (!api || !setApi) {
|
103 |
+
return
|
104 |
+
}
|
105 |
+
|
106 |
+
setApi(api)
|
107 |
+
}, [api, setApi])
|
108 |
+
|
109 |
+
React.useEffect(() => {
|
110 |
+
if (!api) {
|
111 |
+
return
|
112 |
+
}
|
113 |
+
|
114 |
+
onSelect(api)
|
115 |
+
api.on("reInit", onSelect)
|
116 |
+
api.on("select", onSelect)
|
117 |
+
|
118 |
+
return () => {
|
119 |
+
api?.off("select", onSelect)
|
120 |
+
}
|
121 |
+
}, [api, onSelect])
|
122 |
+
|
123 |
+
return (
|
124 |
+
<CarouselContext.Provider
|
125 |
+
value={{
|
126 |
+
carouselRef,
|
127 |
+
api: api,
|
128 |
+
opts,
|
129 |
+
orientation:
|
130 |
+
orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
|
131 |
+
scrollPrev,
|
132 |
+
scrollNext,
|
133 |
+
canScrollPrev,
|
134 |
+
canScrollNext,
|
135 |
+
}}
|
136 |
+
>
|
137 |
+
<div
|
138 |
+
ref={ref}
|
139 |
+
onKeyDownCapture={handleKeyDown}
|
140 |
+
className={cn("relative", className)}
|
141 |
+
role="region"
|
142 |
+
aria-roledescription="carousel"
|
143 |
+
{...props}
|
144 |
+
>
|
145 |
+
{children}
|
146 |
+
</div>
|
147 |
+
</CarouselContext.Provider>
|
148 |
+
)
|
149 |
+
}
|
150 |
+
)
|
151 |
+
Carousel.displayName = "Carousel"
|
152 |
+
|
153 |
+
const CarouselContent = React.forwardRef<
|
154 |
+
HTMLDivElement,
|
155 |
+
React.HTMLAttributes<HTMLDivElement>
|
156 |
+
>(({ className, ...props }, ref) => {
|
157 |
+
const { carouselRef, orientation } = useCarousel()
|
158 |
+
|
159 |
+
return (
|
160 |
+
<div ref={carouselRef} className="overflow-hidden">
|
161 |
+
<div
|
162 |
+
ref={ref}
|
163 |
+
className={cn(
|
164 |
+
"flex",
|
165 |
+
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
|
166 |
+
className
|
167 |
+
)}
|
168 |
+
{...props}
|
169 |
+
/>
|
170 |
+
</div>
|
171 |
+
)
|
172 |
+
})
|
173 |
+
CarouselContent.displayName = "CarouselContent"
|
174 |
+
|
175 |
+
const CarouselItem = React.forwardRef<
|
176 |
+
HTMLDivElement,
|
177 |
+
React.HTMLAttributes<HTMLDivElement>
|
178 |
+
>(({ className, ...props }, ref) => {
|
179 |
+
const { orientation } = useCarousel()
|
180 |
+
|
181 |
+
return (
|
182 |
+
<div
|
183 |
+
ref={ref}
|
184 |
+
role="group"
|
185 |
+
aria-roledescription="slide"
|
186 |
+
className={cn(
|
187 |
+
"min-w-0 shrink-0 grow-0 basis-full",
|
188 |
+
orientation === "horizontal" ? "pl-4" : "pt-4",
|
189 |
+
className
|
190 |
+
)}
|
191 |
+
{...props}
|
192 |
+
/>
|
193 |
+
)
|
194 |
+
})
|
195 |
+
CarouselItem.displayName = "CarouselItem"
|
196 |
+
|
197 |
+
const CarouselPrevious = React.forwardRef<
|
198 |
+
HTMLButtonElement,
|
199 |
+
React.ComponentProps<typeof Button>
|
200 |
+
>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
|
201 |
+
const { orientation, scrollPrev, canScrollPrev } = useCarousel()
|
202 |
+
|
203 |
+
return (
|
204 |
+
<Button
|
205 |
+
ref={ref}
|
206 |
+
variant={variant}
|
207 |
+
size={size}
|
208 |
+
className={cn(
|
209 |
+
"absolute h-8 w-8 rounded-full",
|
210 |
+
orientation === "horizontal"
|
211 |
+
? "-left-12 top-1/2 -translate-y-1/2"
|
212 |
+
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
|
213 |
+
className
|
214 |
+
)}
|
215 |
+
disabled={!canScrollPrev}
|
216 |
+
onClick={scrollPrev}
|
217 |
+
{...props}
|
218 |
+
>
|
219 |
+
<ArrowLeft className="h-4 w-4" />
|
220 |
+
<span className="sr-only">Previous slide</span>
|
221 |
+
</Button>
|
222 |
+
)
|
223 |
+
})
|
224 |
+
CarouselPrevious.displayName = "CarouselPrevious"
|
225 |
+
|
226 |
+
const CarouselNext = React.forwardRef<
|
227 |
+
HTMLButtonElement,
|
228 |
+
React.ComponentProps<typeof Button>
|
229 |
+
>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
|
230 |
+
const { orientation, scrollNext, canScrollNext } = useCarousel()
|
231 |
+
|
232 |
+
return (
|
233 |
+
<Button
|
234 |
+
ref={ref}
|
235 |
+
variant={variant}
|
236 |
+
size={size}
|
237 |
+
className={cn(
|
238 |
+
"absolute h-8 w-8 rounded-full",
|
239 |
+
orientation === "horizontal"
|
240 |
+
? "-right-12 top-1/2 -translate-y-1/2"
|
241 |
+
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
|
242 |
+
className
|
243 |
+
)}
|
244 |
+
disabled={!canScrollNext}
|
245 |
+
onClick={scrollNext}
|
246 |
+
{...props}
|
247 |
+
>
|
248 |
+
<ArrowRight className="h-4 w-4" />
|
249 |
+
<span className="sr-only">Next slide</span>
|
250 |
+
</Button>
|
251 |
+
)
|
252 |
+
})
|
253 |
+
CarouselNext.displayName = "CarouselNext"
|
254 |
+
|
255 |
+
export {
|
256 |
+
type CarouselApi,
|
257 |
+
Carousel,
|
258 |
+
CarouselContent,
|
259 |
+
CarouselItem,
|
260 |
+
CarouselPrevious,
|
261 |
+
CarouselNext,
|
262 |
+
}
|
components/ui/chart.tsx
ADDED
@@ -0,0 +1,365 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
|
3 |
+
import * as React from "react"
|
4 |
+
import * as RechartsPrimitive from "recharts"
|
5 |
+
|
6 |
+
import { cn } from "@/lib/utils"
|
7 |
+
|
8 |
+
// Format: { THEME_NAME: CSS_SELECTOR }
|
9 |
+
const THEMES = { light: "", dark: ".dark" } as const
|
10 |
+
|
11 |
+
export type ChartConfig = {
|
12 |
+
[k in string]: {
|
13 |
+
label?: React.ReactNode
|
14 |
+
icon?: React.ComponentType
|
15 |
+
} & (
|
16 |
+
| { color?: string; theme?: never }
|
17 |
+
| { color?: never; theme: Record<keyof typeof THEMES, string> }
|
18 |
+
)
|
19 |
+
}
|
20 |
+
|
21 |
+
type ChartContextProps = {
|
22 |
+
config: ChartConfig
|
23 |
+
}
|
24 |
+
|
25 |
+
const ChartContext = React.createContext<ChartContextProps | null>(null)
|
26 |
+
|
27 |
+
function useChart() {
|
28 |
+
const context = React.useContext(ChartContext)
|
29 |
+
|
30 |
+
if (!context) {
|
31 |
+
throw new Error("useChart must be used within a <ChartContainer />")
|
32 |
+
}
|
33 |
+
|
34 |
+
return context
|
35 |
+
}
|
36 |
+
|
37 |
+
const ChartContainer = React.forwardRef<
|
38 |
+
HTMLDivElement,
|
39 |
+
React.ComponentProps<"div"> & {
|
40 |
+
config: ChartConfig
|
41 |
+
children: React.ComponentProps<
|
42 |
+
typeof RechartsPrimitive.ResponsiveContainer
|
43 |
+
>["children"]
|
44 |
+
}
|
45 |
+
>(({ id, className, children, config, ...props }, ref) => {
|
46 |
+
const uniqueId = React.useId()
|
47 |
+
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
|
48 |
+
|
49 |
+
return (
|
50 |
+
<ChartContext.Provider value={{ config }}>
|
51 |
+
<div
|
52 |
+
data-chart={chartId}
|
53 |
+
ref={ref}
|
54 |
+
className={cn(
|
55 |
+
"flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none",
|
56 |
+
className
|
57 |
+
)}
|
58 |
+
{...props}
|
59 |
+
>
|
60 |
+
<ChartStyle id={chartId} config={config} />
|
61 |
+
<RechartsPrimitive.ResponsiveContainer>
|
62 |
+
{children}
|
63 |
+
</RechartsPrimitive.ResponsiveContainer>
|
64 |
+
</div>
|
65 |
+
</ChartContext.Provider>
|
66 |
+
)
|
67 |
+
})
|
68 |
+
ChartContainer.displayName = "Chart"
|
69 |
+
|
70 |
+
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
|
71 |
+
const colorConfig = Object.entries(config).filter(
|
72 |
+
([_, config]) => config.theme || config.color
|
73 |
+
)
|
74 |
+
|
75 |
+
if (!colorConfig.length) {
|
76 |
+
return null
|
77 |
+
}
|
78 |
+
|
79 |
+
return (
|
80 |
+
<style
|
81 |
+
dangerouslySetInnerHTML={{
|
82 |
+
__html: Object.entries(THEMES)
|
83 |
+
.map(
|
84 |
+
([theme, prefix]) => `
|
85 |
+
${prefix} [data-chart=${id}] {
|
86 |
+
${colorConfig
|
87 |
+
.map(([key, itemConfig]) => {
|
88 |
+
const color =
|
89 |
+
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
|
90 |
+
itemConfig.color
|
91 |
+
return color ? ` --color-${key}: ${color};` : null
|
92 |
+
})
|
93 |
+
.join("\n")}
|
94 |
+
}
|
95 |
+
`
|
96 |
+
)
|
97 |
+
.join("\n"),
|
98 |
+
}}
|
99 |
+
/>
|
100 |
+
)
|
101 |
+
}
|
102 |
+
|
103 |
+
const ChartTooltip = RechartsPrimitive.Tooltip
|
104 |
+
|
105 |
+
const ChartTooltipContent = React.forwardRef<
|
106 |
+
HTMLDivElement,
|
107 |
+
React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
|
108 |
+
React.ComponentProps<"div"> & {
|
109 |
+
hideLabel?: boolean
|
110 |
+
hideIndicator?: boolean
|
111 |
+
indicator?: "line" | "dot" | "dashed"
|
112 |
+
nameKey?: string
|
113 |
+
labelKey?: string
|
114 |
+
}
|
115 |
+
>(
|
116 |
+
(
|
117 |
+
{
|
118 |
+
active,
|
119 |
+
payload,
|
120 |
+
className,
|
121 |
+
indicator = "dot",
|
122 |
+
hideLabel = false,
|
123 |
+
hideIndicator = false,
|
124 |
+
label,
|
125 |
+
labelFormatter,
|
126 |
+
labelClassName,
|
127 |
+
formatter,
|
128 |
+
color,
|
129 |
+
nameKey,
|
130 |
+
labelKey,
|
131 |
+
},
|
132 |
+
ref
|
133 |
+
) => {
|
134 |
+
const { config } = useChart()
|
135 |
+
|
136 |
+
const tooltipLabel = React.useMemo(() => {
|
137 |
+
if (hideLabel || !payload?.length) {
|
138 |
+
return null
|
139 |
+
}
|
140 |
+
|
141 |
+
const [item] = payload
|
142 |
+
const key = `${labelKey || item.dataKey || item.name || "value"}`
|
143 |
+
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
144 |
+
const value =
|
145 |
+
!labelKey && typeof label === "string"
|
146 |
+
? config[label as keyof typeof config]?.label || label
|
147 |
+
: itemConfig?.label
|
148 |
+
|
149 |
+
if (labelFormatter) {
|
150 |
+
return (
|
151 |
+
<div className={cn("font-medium", labelClassName)}>
|
152 |
+
{labelFormatter(value, payload)}
|
153 |
+
</div>
|
154 |
+
)
|
155 |
+
}
|
156 |
+
|
157 |
+
if (!value) {
|
158 |
+
return null
|
159 |
+
}
|
160 |
+
|
161 |
+
return <div className={cn("font-medium", labelClassName)}>{value}</div>
|
162 |
+
}, [
|
163 |
+
label,
|
164 |
+
labelFormatter,
|
165 |
+
payload,
|
166 |
+
hideLabel,
|
167 |
+
labelClassName,
|
168 |
+
config,
|
169 |
+
labelKey,
|
170 |
+
])
|
171 |
+
|
172 |
+
if (!active || !payload?.length) {
|
173 |
+
return null
|
174 |
+
}
|
175 |
+
|
176 |
+
const nestLabel = payload.length === 1 && indicator !== "dot"
|
177 |
+
|
178 |
+
return (
|
179 |
+
<div
|
180 |
+
ref={ref}
|
181 |
+
className={cn(
|
182 |
+
"grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl",
|
183 |
+
className
|
184 |
+
)}
|
185 |
+
>
|
186 |
+
{!nestLabel ? tooltipLabel : null}
|
187 |
+
<div className="grid gap-1.5">
|
188 |
+
{payload.map((item, index) => {
|
189 |
+
const key = `${nameKey || item.name || item.dataKey || "value"}`
|
190 |
+
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
191 |
+
const indicatorColor = color || item.payload.fill || item.color
|
192 |
+
|
193 |
+
return (
|
194 |
+
<div
|
195 |
+
key={item.dataKey}
|
196 |
+
className={cn(
|
197 |
+
"flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
|
198 |
+
indicator === "dot" && "items-center"
|
199 |
+
)}
|
200 |
+
>
|
201 |
+
{formatter && item?.value !== undefined && item.name ? (
|
202 |
+
formatter(item.value, item.name, item, index, item.payload)
|
203 |
+
) : (
|
204 |
+
<>
|
205 |
+
{itemConfig?.icon ? (
|
206 |
+
<itemConfig.icon />
|
207 |
+
) : (
|
208 |
+
!hideIndicator && (
|
209 |
+
<div
|
210 |
+
className={cn(
|
211 |
+
"shrink-0 rounded-[2px] border-[--color-border] bg-[--color-bg]",
|
212 |
+
{
|
213 |
+
"h-2.5 w-2.5": indicator === "dot",
|
214 |
+
"w-1": indicator === "line",
|
215 |
+
"w-0 border-[1.5px] border-dashed bg-transparent":
|
216 |
+
indicator === "dashed",
|
217 |
+
"my-0.5": nestLabel && indicator === "dashed",
|
218 |
+
}
|
219 |
+
)}
|
220 |
+
style={
|
221 |
+
{
|
222 |
+
"--color-bg": indicatorColor,
|
223 |
+
"--color-border": indicatorColor,
|
224 |
+
} as React.CSSProperties
|
225 |
+
}
|
226 |
+
/>
|
227 |
+
)
|
228 |
+
)}
|
229 |
+
<div
|
230 |
+
className={cn(
|
231 |
+
"flex flex-1 justify-between leading-none",
|
232 |
+
nestLabel ? "items-end" : "items-center"
|
233 |
+
)}
|
234 |
+
>
|
235 |
+
<div className="grid gap-1.5">
|
236 |
+
{nestLabel ? tooltipLabel : null}
|
237 |
+
<span className="text-muted-foreground">
|
238 |
+
{itemConfig?.label || item.name}
|
239 |
+
</span>
|
240 |
+
</div>
|
241 |
+
{item.value && (
|
242 |
+
<span className="font-mono font-medium tabular-nums text-foreground">
|
243 |
+
{item.value.toLocaleString()}
|
244 |
+
</span>
|
245 |
+
)}
|
246 |
+
</div>
|
247 |
+
</>
|
248 |
+
)}
|
249 |
+
</div>
|
250 |
+
)
|
251 |
+
})}
|
252 |
+
</div>
|
253 |
+
</div>
|
254 |
+
)
|
255 |
+
}
|
256 |
+
)
|
257 |
+
ChartTooltipContent.displayName = "ChartTooltip"
|
258 |
+
|
259 |
+
const ChartLegend = RechartsPrimitive.Legend
|
260 |
+
|
261 |
+
const ChartLegendContent = React.forwardRef<
|
262 |
+
HTMLDivElement,
|
263 |
+
React.ComponentProps<"div"> &
|
264 |
+
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
|
265 |
+
hideIcon?: boolean
|
266 |
+
nameKey?: string
|
267 |
+
}
|
268 |
+
>(
|
269 |
+
(
|
270 |
+
{ className, hideIcon = false, payload, verticalAlign = "bottom", nameKey },
|
271 |
+
ref
|
272 |
+
) => {
|
273 |
+
const { config } = useChart()
|
274 |
+
|
275 |
+
if (!payload?.length) {
|
276 |
+
return null
|
277 |
+
}
|
278 |
+
|
279 |
+
return (
|
280 |
+
<div
|
281 |
+
ref={ref}
|
282 |
+
className={cn(
|
283 |
+
"flex items-center justify-center gap-4",
|
284 |
+
verticalAlign === "top" ? "pb-3" : "pt-3",
|
285 |
+
className
|
286 |
+
)}
|
287 |
+
>
|
288 |
+
{payload.map((item) => {
|
289 |
+
const key = `${nameKey || item.dataKey || "value"}`
|
290 |
+
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
291 |
+
|
292 |
+
return (
|
293 |
+
<div
|
294 |
+
key={item.value}
|
295 |
+
className={cn(
|
296 |
+
"flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground"
|
297 |
+
)}
|
298 |
+
>
|
299 |
+
{itemConfig?.icon && !hideIcon ? (
|
300 |
+
<itemConfig.icon />
|
301 |
+
) : (
|
302 |
+
<div
|
303 |
+
className="h-2 w-2 shrink-0 rounded-[2px]"
|
304 |
+
style={{
|
305 |
+
backgroundColor: item.color,
|
306 |
+
}}
|
307 |
+
/>
|
308 |
+
)}
|
309 |
+
{itemConfig?.label}
|
310 |
+
</div>
|
311 |
+
)
|
312 |
+
})}
|
313 |
+
</div>
|
314 |
+
)
|
315 |
+
}
|
316 |
+
)
|
317 |
+
ChartLegendContent.displayName = "ChartLegend"
|
318 |
+
|
319 |
+
// Helper to extract item config from a payload.
|
320 |
+
function getPayloadConfigFromPayload(
|
321 |
+
config: ChartConfig,
|
322 |
+
payload: unknown,
|
323 |
+
key: string
|
324 |
+
) {
|
325 |
+
if (typeof payload !== "object" || payload === null) {
|
326 |
+
return undefined
|
327 |
+
}
|
328 |
+
|
329 |
+
const payloadPayload =
|
330 |
+
"payload" in payload &&
|
331 |
+
typeof payload.payload === "object" &&
|
332 |
+
payload.payload !== null
|
333 |
+
? payload.payload
|
334 |
+
: undefined
|
335 |
+
|
336 |
+
let configLabelKey: string = key
|
337 |
+
|
338 |
+
if (
|
339 |
+
key in payload &&
|
340 |
+
typeof payload[key as keyof typeof payload] === "string"
|
341 |
+
) {
|
342 |
+
configLabelKey = payload[key as keyof typeof payload] as string
|
343 |
+
} else if (
|
344 |
+
payloadPayload &&
|
345 |
+
key in payloadPayload &&
|
346 |
+
typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
|
347 |
+
) {
|
348 |
+
configLabelKey = payloadPayload[
|
349 |
+
key as keyof typeof payloadPayload
|
350 |
+
] as string
|
351 |
+
}
|
352 |
+
|
353 |
+
return configLabelKey in config
|
354 |
+
? config[configLabelKey]
|
355 |
+
: config[key as keyof typeof config]
|
356 |
+
}
|
357 |
+
|
358 |
+
export {
|
359 |
+
ChartContainer,
|
360 |
+
ChartTooltip,
|
361 |
+
ChartTooltipContent,
|
362 |
+
ChartLegend,
|
363 |
+
ChartLegendContent,
|
364 |
+
ChartStyle,
|
365 |
+
}
|
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-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
|
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 }
|
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 }
|
components/ui/command.tsx
ADDED
@@ -0,0 +1,153 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
|
3 |
+
import * as React from "react"
|
4 |
+
import { type 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-popover text-popover-foreground",
|
19 |
+
className
|
20 |
+
)}
|
21 |
+
{...props}
|
22 |
+
/>
|
23 |
+
))
|
24 |
+
Command.displayName = CommandPrimitive.displayName
|
25 |
+
|
26 |
+
const CommandDialog = ({ children, ...props }: DialogProps) => {
|
27 |
+
return (
|
28 |
+
<Dialog {...props}>
|
29 |
+
<DialogContent className="overflow-hidden p-0 shadow-lg">
|
30 |
+
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[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">
|
31 |
+
{children}
|
32 |
+
</Command>
|
33 |
+
</DialogContent>
|
34 |
+
</Dialog>
|
35 |
+
)
|
36 |
+
}
|
37 |
+
|
38 |
+
const CommandInput = React.forwardRef<
|
39 |
+
React.ElementRef<typeof CommandPrimitive.Input>,
|
40 |
+
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
|
41 |
+
>(({ className, ...props }, ref) => (
|
42 |
+
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
|
43 |
+
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
44 |
+
<CommandPrimitive.Input
|
45 |
+
ref={ref}
|
46 |
+
className={cn(
|
47 |
+
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
|
48 |
+
className
|
49 |
+
)}
|
50 |
+
{...props}
|
51 |
+
/>
|
52 |
+
</div>
|
53 |
+
))
|
54 |
+
|
55 |
+
CommandInput.displayName = CommandPrimitive.Input.displayName
|
56 |
+
|
57 |
+
const CommandList = React.forwardRef<
|
58 |
+
React.ElementRef<typeof CommandPrimitive.List>,
|
59 |
+
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
|
60 |
+
>(({ className, ...props }, ref) => (
|
61 |
+
<CommandPrimitive.List
|
62 |
+
ref={ref}
|
63 |
+
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
|
64 |
+
{...props}
|
65 |
+
/>
|
66 |
+
))
|
67 |
+
|
68 |
+
CommandList.displayName = CommandPrimitive.List.displayName
|
69 |
+
|
70 |
+
const CommandEmpty = React.forwardRef<
|
71 |
+
React.ElementRef<typeof CommandPrimitive.Empty>,
|
72 |
+
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
|
73 |
+
>((props, ref) => (
|
74 |
+
<CommandPrimitive.Empty
|
75 |
+
ref={ref}
|
76 |
+
className="py-6 text-center text-sm"
|
77 |
+
{...props}
|
78 |
+
/>
|
79 |
+
))
|
80 |
+
|
81 |
+
CommandEmpty.displayName = CommandPrimitive.Empty.displayName
|
82 |
+
|
83 |
+
const CommandGroup = React.forwardRef<
|
84 |
+
React.ElementRef<typeof CommandPrimitive.Group>,
|
85 |
+
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
|
86 |
+
>(({ className, ...props }, ref) => (
|
87 |
+
<CommandPrimitive.Group
|
88 |
+
ref={ref}
|
89 |
+
className={cn(
|
90 |
+
"overflow-hidden p-1 text-foreground [&_[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-muted-foreground",
|
91 |
+
className
|
92 |
+
)}
|
93 |
+
{...props}
|
94 |
+
/>
|
95 |
+
))
|
96 |
+
|
97 |
+
CommandGroup.displayName = CommandPrimitive.Group.displayName
|
98 |
+
|
99 |
+
const CommandSeparator = React.forwardRef<
|
100 |
+
React.ElementRef<typeof CommandPrimitive.Separator>,
|
101 |
+
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
|
102 |
+
>(({ className, ...props }, ref) => (
|
103 |
+
<CommandPrimitive.Separator
|
104 |
+
ref={ref}
|
105 |
+
className={cn("-mx-1 h-px bg-border", className)}
|
106 |
+
{...props}
|
107 |
+
/>
|
108 |
+
))
|
109 |
+
CommandSeparator.displayName = CommandPrimitive.Separator.displayName
|
110 |
+
|
111 |
+
const CommandItem = React.forwardRef<
|
112 |
+
React.ElementRef<typeof CommandPrimitive.Item>,
|
113 |
+
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
|
114 |
+
>(({ className, ...props }, ref) => (
|
115 |
+
<CommandPrimitive.Item
|
116 |
+
ref={ref}
|
117 |
+
className={cn(
|
118 |
+
"relative flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected='true']:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
119 |
+
className
|
120 |
+
)}
|
121 |
+
{...props}
|
122 |
+
/>
|
123 |
+
))
|
124 |
+
|
125 |
+
CommandItem.displayName = CommandPrimitive.Item.displayName
|
126 |
+
|
127 |
+
const CommandShortcut = ({
|
128 |
+
className,
|
129 |
+
...props
|
130 |
+
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
131 |
+
return (
|
132 |
+
<span
|
133 |
+
className={cn(
|
134 |
+
"ml-auto text-xs tracking-widest text-muted-foreground",
|
135 |
+
className
|
136 |
+
)}
|
137 |
+
{...props}
|
138 |
+
/>
|
139 |
+
)
|
140 |
+
}
|
141 |
+
CommandShortcut.displayName = "CommandShortcut"
|
142 |
+
|
143 |
+
export {
|
144 |
+
Command,
|
145 |
+
CommandDialog,
|
146 |
+
CommandInput,
|
147 |
+
CommandList,
|
148 |
+
CommandEmpty,
|
149 |
+
CommandGroup,
|
150 |
+
CommandItem,
|
151 |
+
CommandShortcut,
|
152 |
+
CommandSeparator,
|
153 |
+
}
|
components/ui/context-menu.tsx
ADDED
@@ -0,0 +1,200 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
|
3 |
+
import * as React from "react"
|
4 |
+
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"
|
5 |
+
import { Check, ChevronRight, Circle } from "lucide-react"
|
6 |
+
|
7 |
+
import { cn } from "@/lib/utils"
|
8 |
+
|
9 |
+
const ContextMenu = ContextMenuPrimitive.Root
|
10 |
+
|
11 |
+
const ContextMenuTrigger = ContextMenuPrimitive.Trigger
|
12 |
+
|
13 |
+
const ContextMenuGroup = ContextMenuPrimitive.Group
|
14 |
+
|
15 |
+
const ContextMenuPortal = ContextMenuPrimitive.Portal
|
16 |
+
|
17 |
+
const ContextMenuSub = ContextMenuPrimitive.Sub
|
18 |
+
|
19 |
+
const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup
|
20 |
+
|
21 |
+
const ContextMenuSubTrigger = React.forwardRef<
|
22 |
+
React.ElementRef<typeof ContextMenuPrimitive.SubTrigger>,
|
23 |
+
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger> & {
|
24 |
+
inset?: boolean
|
25 |
+
}
|
26 |
+
>(({ className, inset, children, ...props }, ref) => (
|
27 |
+
<ContextMenuPrimitive.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-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
|
31 |
+
inset && "pl-8",
|
32 |
+
className
|
33 |
+
)}
|
34 |
+
{...props}
|
35 |
+
>
|
36 |
+
{children}
|
37 |
+
<ChevronRight className="ml-auto h-4 w-4" />
|
38 |
+
</ContextMenuPrimitive.SubTrigger>
|
39 |
+
))
|
40 |
+
ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName
|
41 |
+
|
42 |
+
const ContextMenuSubContent = React.forwardRef<
|
43 |
+
React.ElementRef<typeof ContextMenuPrimitive.SubContent>,
|
44 |
+
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubContent>
|
45 |
+
>(({ className, ...props }, ref) => (
|
46 |
+
<ContextMenuPrimitive.SubContent
|
47 |
+
ref={ref}
|
48 |
+
className={cn(
|
49 |
+
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground 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",
|
50 |
+
className
|
51 |
+
)}
|
52 |
+
{...props}
|
53 |
+
/>
|
54 |
+
))
|
55 |
+
ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName
|
56 |
+
|
57 |
+
const ContextMenuContent = React.forwardRef<
|
58 |
+
React.ElementRef<typeof ContextMenuPrimitive.Content>,
|
59 |
+
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content>
|
60 |
+
>(({ className, ...props }, ref) => (
|
61 |
+
<ContextMenuPrimitive.Portal>
|
62 |
+
<ContextMenuPrimitive.Content
|
63 |
+
ref={ref}
|
64 |
+
className={cn(
|
65 |
+
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in fade-in-80 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",
|
66 |
+
className
|
67 |
+
)}
|
68 |
+
{...props}
|
69 |
+
/>
|
70 |
+
</ContextMenuPrimitive.Portal>
|
71 |
+
))
|
72 |
+
ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName
|
73 |
+
|
74 |
+
const ContextMenuItem = React.forwardRef<
|
75 |
+
React.ElementRef<typeof ContextMenuPrimitive.Item>,
|
76 |
+
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & {
|
77 |
+
inset?: boolean
|
78 |
+
}
|
79 |
+
>(({ className, inset, ...props }, ref) => (
|
80 |
+
<ContextMenuPrimitive.Item
|
81 |
+
ref={ref}
|
82 |
+
className={cn(
|
83 |
+
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
84 |
+
inset && "pl-8",
|
85 |
+
className
|
86 |
+
)}
|
87 |
+
{...props}
|
88 |
+
/>
|
89 |
+
))
|
90 |
+
ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName
|
91 |
+
|
92 |
+
const ContextMenuCheckboxItem = React.forwardRef<
|
93 |
+
React.ElementRef<typeof ContextMenuPrimitive.CheckboxItem>,
|
94 |
+
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.CheckboxItem>
|
95 |
+
>(({ className, children, checked, ...props }, ref) => (
|
96 |
+
<ContextMenuPrimitive.CheckboxItem
|
97 |
+
ref={ref}
|
98 |
+
className={cn(
|
99 |
+
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
100 |
+
className
|
101 |
+
)}
|
102 |
+
checked={checked}
|
103 |
+
{...props}
|
104 |
+
>
|
105 |
+
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
106 |
+
<ContextMenuPrimitive.ItemIndicator>
|
107 |
+
<Check className="h-4 w-4" />
|
108 |
+
</ContextMenuPrimitive.ItemIndicator>
|
109 |
+
</span>
|
110 |
+
{children}
|
111 |
+
</ContextMenuPrimitive.CheckboxItem>
|
112 |
+
))
|
113 |
+
ContextMenuCheckboxItem.displayName =
|
114 |
+
ContextMenuPrimitive.CheckboxItem.displayName
|
115 |
+
|
116 |
+
const ContextMenuRadioItem = React.forwardRef<
|
117 |
+
React.ElementRef<typeof ContextMenuPrimitive.RadioItem>,
|
118 |
+
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.RadioItem>
|
119 |
+
>(({ className, children, ...props }, ref) => (
|
120 |
+
<ContextMenuPrimitive.RadioItem
|
121 |
+
ref={ref}
|
122 |
+
className={cn(
|
123 |
+
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
124 |
+
className
|
125 |
+
)}
|
126 |
+
{...props}
|
127 |
+
>
|
128 |
+
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
129 |
+
<ContextMenuPrimitive.ItemIndicator>
|
130 |
+
<Circle className="h-2 w-2 fill-current" />
|
131 |
+
</ContextMenuPrimitive.ItemIndicator>
|
132 |
+
</span>
|
133 |
+
{children}
|
134 |
+
</ContextMenuPrimitive.RadioItem>
|
135 |
+
))
|
136 |
+
ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName
|
137 |
+
|
138 |
+
const ContextMenuLabel = React.forwardRef<
|
139 |
+
React.ElementRef<typeof ContextMenuPrimitive.Label>,
|
140 |
+
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label> & {
|
141 |
+
inset?: boolean
|
142 |
+
}
|
143 |
+
>(({ className, inset, ...props }, ref) => (
|
144 |
+
<ContextMenuPrimitive.Label
|
145 |
+
ref={ref}
|
146 |
+
className={cn(
|
147 |
+
"px-2 py-1.5 text-sm font-semibold text-foreground",
|
148 |
+
inset && "pl-8",
|
149 |
+
className
|
150 |
+
)}
|
151 |
+
{...props}
|
152 |
+
/>
|
153 |
+
))
|
154 |
+
ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName
|
155 |
+
|
156 |
+
const ContextMenuSeparator = React.forwardRef<
|
157 |
+
React.ElementRef<typeof ContextMenuPrimitive.Separator>,
|
158 |
+
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Separator>
|
159 |
+
>(({ className, ...props }, ref) => (
|
160 |
+
<ContextMenuPrimitive.Separator
|
161 |
+
ref={ref}
|
162 |
+
className={cn("-mx-1 my-1 h-px bg-border", className)}
|
163 |
+
{...props}
|
164 |
+
/>
|
165 |
+
))
|
166 |
+
ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName
|
167 |
+
|
168 |
+
const ContextMenuShortcut = ({
|
169 |
+
className,
|
170 |
+
...props
|
171 |
+
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
172 |
+
return (
|
173 |
+
<span
|
174 |
+
className={cn(
|
175 |
+
"ml-auto text-xs tracking-widest text-muted-foreground",
|
176 |
+
className
|
177 |
+
)}
|
178 |
+
{...props}
|
179 |
+
/>
|
180 |
+
)
|
181 |
+
}
|
182 |
+
ContextMenuShortcut.displayName = "ContextMenuShortcut"
|
183 |
+
|
184 |
+
export {
|
185 |
+
ContextMenu,
|
186 |
+
ContextMenuTrigger,
|
187 |
+
ContextMenuContent,
|
188 |
+
ContextMenuItem,
|
189 |
+
ContextMenuCheckboxItem,
|
190 |
+
ContextMenuRadioItem,
|
191 |
+
ContextMenuLabel,
|
192 |
+
ContextMenuSeparator,
|
193 |
+
ContextMenuShortcut,
|
194 |
+
ContextMenuGroup,
|
195 |
+
ContextMenuPortal,
|
196 |
+
ContextMenuSub,
|
197 |
+
ContextMenuSubContent,
|
198 |
+
ContextMenuSubTrigger,
|
199 |
+
ContextMenuRadioGroup,
|
200 |
+
}
|
components/ui/dialog.tsx
ADDED
@@ -0,0 +1,122 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 = DialogPrimitive.Portal
|
14 |
+
|
15 |
+
const DialogClose = DialogPrimitive.Close
|
16 |
+
|
17 |
+
const DialogOverlay = React.forwardRef<
|
18 |
+
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
19 |
+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
20 |
+
>(({ className, ...props }, ref) => (
|
21 |
+
<DialogPrimitive.Overlay
|
22 |
+
ref={ref}
|
23 |
+
className={cn(
|
24 |
+
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
25 |
+
className
|
26 |
+
)}
|
27 |
+
{...props}
|
28 |
+
/>
|
29 |
+
))
|
30 |
+
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
|
31 |
+
|
32 |
+
const DialogContent = React.forwardRef<
|
33 |
+
React.ElementRef<typeof DialogPrimitive.Content>,
|
34 |
+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
35 |
+
>(({ className, children, ...props }, ref) => (
|
36 |
+
<DialogPortal>
|
37 |
+
<DialogOverlay />
|
38 |
+
<DialogPrimitive.Content
|
39 |
+
ref={ref}
|
40 |
+
className={cn(
|
41 |
+
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background 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",
|
42 |
+
className
|
43 |
+
)}
|
44 |
+
{...props}
|
45 |
+
>
|
46 |
+
{children}
|
47 |
+
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
48 |
+
<X className="h-4 w-4" />
|
49 |
+
<span className="sr-only">Close</span>
|
50 |
+
</DialogPrimitive.Close>
|
51 |
+
</DialogPrimitive.Content>
|
52 |
+
</DialogPortal>
|
53 |
+
))
|
54 |
+
DialogContent.displayName = DialogPrimitive.Content.displayName
|
55 |
+
|
56 |
+
const DialogHeader = ({
|
57 |
+
className,
|
58 |
+
...props
|
59 |
+
}: React.HTMLAttributes<HTMLDivElement>) => (
|
60 |
+
<div
|
61 |
+
className={cn(
|
62 |
+
"flex flex-col space-y-1.5 text-center sm:text-left",
|
63 |
+
className
|
64 |
+
)}
|
65 |
+
{...props}
|
66 |
+
/>
|
67 |
+
)
|
68 |
+
DialogHeader.displayName = "DialogHeader"
|
69 |
+
|
70 |
+
const DialogFooter = ({
|
71 |
+
className,
|
72 |
+
...props
|
73 |
+
}: React.HTMLAttributes<HTMLDivElement>) => (
|
74 |
+
<div
|
75 |
+
className={cn(
|
76 |
+
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
77 |
+
className
|
78 |
+
)}
|
79 |
+
{...props}
|
80 |
+
/>
|
81 |
+
)
|
82 |
+
DialogFooter.displayName = "DialogFooter"
|
83 |
+
|
84 |
+
const DialogTitle = React.forwardRef<
|
85 |
+
React.ElementRef<typeof DialogPrimitive.Title>,
|
86 |
+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
87 |
+
>(({ className, ...props }, ref) => (
|
88 |
+
<DialogPrimitive.Title
|
89 |
+
ref={ref}
|
90 |
+
className={cn(
|
91 |
+
"text-lg font-semibold leading-none tracking-tight",
|
92 |
+
className
|
93 |
+
)}
|
94 |
+
{...props}
|
95 |
+
/>
|
96 |
+
))
|
97 |
+
DialogTitle.displayName = DialogPrimitive.Title.displayName
|
98 |
+
|
99 |
+
const DialogDescription = React.forwardRef<
|
100 |
+
React.ElementRef<typeof DialogPrimitive.Description>,
|
101 |
+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
102 |
+
>(({ className, ...props }, ref) => (
|
103 |
+
<DialogPrimitive.Description
|
104 |
+
ref={ref}
|
105 |
+
className={cn("text-sm text-muted-foreground", className)}
|
106 |
+
{...props}
|
107 |
+
/>
|
108 |
+
))
|
109 |
+
DialogDescription.displayName = DialogPrimitive.Description.displayName
|
110 |
+
|
111 |
+
export {
|
112 |
+
Dialog,
|
113 |
+
DialogPortal,
|
114 |
+
DialogOverlay,
|
115 |
+
DialogClose,
|
116 |
+
DialogTrigger,
|
117 |
+
DialogContent,
|
118 |
+
DialogHeader,
|
119 |
+
DialogFooter,
|
120 |
+
DialogTitle,
|
121 |
+
DialogDescription,
|
122 |
+
}
|
components/ui/drawer.tsx
ADDED
@@ -0,0 +1,118 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
|
3 |
+
import * as React from "react"
|
4 |
+
import { Drawer as DrawerPrimitive } from "vaul"
|
5 |
+
|
6 |
+
import { cn } from "@/lib/utils"
|
7 |
+
|
8 |
+
const Drawer = ({
|
9 |
+
shouldScaleBackground = true,
|
10 |
+
...props
|
11 |
+
}: React.ComponentProps<typeof DrawerPrimitive.Root>) => (
|
12 |
+
<DrawerPrimitive.Root
|
13 |
+
shouldScaleBackground={shouldScaleBackground}
|
14 |
+
{...props}
|
15 |
+
/>
|
16 |
+
)
|
17 |
+
Drawer.displayName = "Drawer"
|
18 |
+
|
19 |
+
const DrawerTrigger = DrawerPrimitive.Trigger
|
20 |
+
|
21 |
+
const DrawerPortal = DrawerPrimitive.Portal
|
22 |
+
|
23 |
+
const DrawerClose = DrawerPrimitive.Close
|
24 |
+
|
25 |
+
const DrawerOverlay = React.forwardRef<
|
26 |
+
React.ElementRef<typeof DrawerPrimitive.Overlay>,
|
27 |
+
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay>
|
28 |
+
>(({ className, ...props }, ref) => (
|
29 |
+
<DrawerPrimitive.Overlay
|
30 |
+
ref={ref}
|
31 |
+
className={cn("fixed inset-0 z-50 bg-black/80", className)}
|
32 |
+
{...props}
|
33 |
+
/>
|
34 |
+
))
|
35 |
+
DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName
|
36 |
+
|
37 |
+
const DrawerContent = React.forwardRef<
|
38 |
+
React.ElementRef<typeof DrawerPrimitive.Content>,
|
39 |
+
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content>
|
40 |
+
>(({ className, children, ...props }, ref) => (
|
41 |
+
<DrawerPortal>
|
42 |
+
<DrawerOverlay />
|
43 |
+
<DrawerPrimitive.Content
|
44 |
+
ref={ref}
|
45 |
+
className={cn(
|
46 |
+
"fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background",
|
47 |
+
className
|
48 |
+
)}
|
49 |
+
{...props}
|
50 |
+
>
|
51 |
+
<div className="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" />
|
52 |
+
{children}
|
53 |
+
</DrawerPrimitive.Content>
|
54 |
+
</DrawerPortal>
|
55 |
+
))
|
56 |
+
DrawerContent.displayName = "DrawerContent"
|
57 |
+
|
58 |
+
const DrawerHeader = ({
|
59 |
+
className,
|
60 |
+
...props
|
61 |
+
}: React.HTMLAttributes<HTMLDivElement>) => (
|
62 |
+
<div
|
63 |
+
className={cn("grid gap-1.5 p-4 text-center sm:text-left", className)}
|
64 |
+
{...props}
|
65 |
+
/>
|
66 |
+
)
|
67 |
+
DrawerHeader.displayName = "DrawerHeader"
|
68 |
+
|
69 |
+
const DrawerFooter = ({
|
70 |
+
className,
|
71 |
+
...props
|
72 |
+
}: React.HTMLAttributes<HTMLDivElement>) => (
|
73 |
+
<div
|
74 |
+
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
75 |
+
{...props}
|
76 |
+
/>
|
77 |
+
)
|
78 |
+
DrawerFooter.displayName = "DrawerFooter"
|
79 |
+
|
80 |
+
const DrawerTitle = React.forwardRef<
|
81 |
+
React.ElementRef<typeof DrawerPrimitive.Title>,
|
82 |
+
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title>
|
83 |
+
>(({ className, ...props }, ref) => (
|
84 |
+
<DrawerPrimitive.Title
|
85 |
+
ref={ref}
|
86 |
+
className={cn(
|
87 |
+
"text-lg font-semibold leading-none tracking-tight",
|
88 |
+
className
|
89 |
+
)}
|
90 |
+
{...props}
|
91 |
+
/>
|
92 |
+
))
|
93 |
+
DrawerTitle.displayName = DrawerPrimitive.Title.displayName
|
94 |
+
|
95 |
+
const DrawerDescription = React.forwardRef<
|
96 |
+
React.ElementRef<typeof DrawerPrimitive.Description>,
|
97 |
+
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description>
|
98 |
+
>(({ className, ...props }, ref) => (
|
99 |
+
<DrawerPrimitive.Description
|
100 |
+
ref={ref}
|
101 |
+
className={cn("text-sm text-muted-foreground", className)}
|
102 |
+
{...props}
|
103 |
+
/>
|
104 |
+
))
|
105 |
+
DrawerDescription.displayName = DrawerPrimitive.Description.displayName
|
106 |
+
|
107 |
+
export {
|
108 |
+
Drawer,
|
109 |
+
DrawerPortal,
|
110 |
+
DrawerOverlay,
|
111 |
+
DrawerTrigger,
|
112 |
+
DrawerClose,
|
113 |
+
DrawerContent,
|
114 |
+
DrawerHeader,
|
115 |
+
DrawerFooter,
|
116 |
+
DrawerTitle,
|
117 |
+
DrawerDescription,
|
118 |
+
}
|
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 gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
31 |
+
inset && "pl-8",
|
32 |
+
className
|
33 |
+
)}
|
34 |
+
{...props}
|
35 |
+
>
|
36 |
+
{children}
|
37 |
+
<ChevronRight className="ml-auto" />
|
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 bg-popover p-1 text-popover-foreground 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",
|
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 bg-popover p-1 text-popover-foreground 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",
|
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 gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
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-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-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-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-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-muted", 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 |
+
}
|
components/ui/form.tsx
ADDED
@@ -0,0 +1,178 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
|
3 |
+
import * as React from "react"
|
4 |
+
import * as LabelPrimitive from "@radix-ui/react-label"
|
5 |
+
import { Slot } from "@radix-ui/react-slot"
|
6 |
+
import {
|
7 |
+
Controller,
|
8 |
+
ControllerProps,
|
9 |
+
FieldPath,
|
10 |
+
FieldValues,
|
11 |
+
FormProvider,
|
12 |
+
useFormContext,
|
13 |
+
} from "react-hook-form"
|
14 |
+
|
15 |
+
import { cn } from "@/lib/utils"
|
16 |
+
import { Label } from "@/components/ui/label"
|
17 |
+
|
18 |
+
const Form = FormProvider
|
19 |
+
|
20 |
+
type FormFieldContextValue<
|
21 |
+
TFieldValues extends FieldValues = FieldValues,
|
22 |
+
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
|
23 |
+
> = {
|
24 |
+
name: TName
|
25 |
+
}
|
26 |
+
|
27 |
+
const FormFieldContext = React.createContext<FormFieldContextValue>(
|
28 |
+
{} as FormFieldContextValue
|
29 |
+
)
|
30 |
+
|
31 |
+
const FormField = <
|
32 |
+
TFieldValues extends FieldValues = FieldValues,
|
33 |
+
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
|
34 |
+
>({
|
35 |
+
...props
|
36 |
+
}: ControllerProps<TFieldValues, TName>) => {
|
37 |
+
return (
|
38 |
+
<FormFieldContext.Provider value={{ name: props.name }}>
|
39 |
+
<Controller {...props} />
|
40 |
+
</FormFieldContext.Provider>
|
41 |
+
)
|
42 |
+
}
|
43 |
+
|
44 |
+
const useFormField = () => {
|
45 |
+
const fieldContext = React.useContext(FormFieldContext)
|
46 |
+
const itemContext = React.useContext(FormItemContext)
|
47 |
+
const { getFieldState, formState } = useFormContext()
|
48 |
+
|
49 |
+
const fieldState = getFieldState(fieldContext.name, formState)
|
50 |
+
|
51 |
+
if (!fieldContext) {
|
52 |
+
throw new Error("useFormField should be used within <FormField>")
|
53 |
+
}
|
54 |
+
|
55 |
+
const { id } = itemContext
|
56 |
+
|
57 |
+
return {
|
58 |
+
id,
|
59 |
+
name: fieldContext.name,
|
60 |
+
formItemId: `${id}-form-item`,
|
61 |
+
formDescriptionId: `${id}-form-item-description`,
|
62 |
+
formMessageId: `${id}-form-item-message`,
|
63 |
+
...fieldState,
|
64 |
+
}
|
65 |
+
}
|
66 |
+
|
67 |
+
type FormItemContextValue = {
|
68 |
+
id: string
|
69 |
+
}
|
70 |
+
|
71 |
+
const FormItemContext = React.createContext<FormItemContextValue>(
|
72 |
+
{} as FormItemContextValue
|
73 |
+
)
|
74 |
+
|
75 |
+
const FormItem = React.forwardRef<
|
76 |
+
HTMLDivElement,
|
77 |
+
React.HTMLAttributes<HTMLDivElement>
|
78 |
+
>(({ className, ...props }, ref) => {
|
79 |
+
const id = React.useId()
|
80 |
+
|
81 |
+
return (
|
82 |
+
<FormItemContext.Provider value={{ id }}>
|
83 |
+
<div ref={ref} className={cn("space-y-2", className)} {...props} />
|
84 |
+
</FormItemContext.Provider>
|
85 |
+
)
|
86 |
+
})
|
87 |
+
FormItem.displayName = "FormItem"
|
88 |
+
|
89 |
+
const FormLabel = React.forwardRef<
|
90 |
+
React.ElementRef<typeof LabelPrimitive.Root>,
|
91 |
+
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
|
92 |
+
>(({ className, ...props }, ref) => {
|
93 |
+
const { error, formItemId } = useFormField()
|
94 |
+
|
95 |
+
return (
|
96 |
+
<Label
|
97 |
+
ref={ref}
|
98 |
+
className={cn(error && "text-destructive", className)}
|
99 |
+
htmlFor={formItemId}
|
100 |
+
{...props}
|
101 |
+
/>
|
102 |
+
)
|
103 |
+
})
|
104 |
+
FormLabel.displayName = "FormLabel"
|
105 |
+
|
106 |
+
const FormControl = React.forwardRef<
|
107 |
+
React.ElementRef<typeof Slot>,
|
108 |
+
React.ComponentPropsWithoutRef<typeof Slot>
|
109 |
+
>(({ ...props }, ref) => {
|
110 |
+
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
|
111 |
+
|
112 |
+
return (
|
113 |
+
<Slot
|
114 |
+
ref={ref}
|
115 |
+
id={formItemId}
|
116 |
+
aria-describedby={
|
117 |
+
!error
|
118 |
+
? `${formDescriptionId}`
|
119 |
+
: `${formDescriptionId} ${formMessageId}`
|
120 |
+
}
|
121 |
+
aria-invalid={!!error}
|
122 |
+
{...props}
|
123 |
+
/>
|
124 |
+
)
|
125 |
+
})
|
126 |
+
FormControl.displayName = "FormControl"
|
127 |
+
|
128 |
+
const FormDescription = React.forwardRef<
|
129 |
+
HTMLParagraphElement,
|
130 |
+
React.HTMLAttributes<HTMLParagraphElement>
|
131 |
+
>(({ className, ...props }, ref) => {
|
132 |
+
const { formDescriptionId } = useFormField()
|
133 |
+
|
134 |
+
return (
|
135 |
+
<p
|
136 |
+
ref={ref}
|
137 |
+
id={formDescriptionId}
|
138 |
+
className={cn("text-sm text-muted-foreground", className)}
|
139 |
+
{...props}
|
140 |
+
/>
|
141 |
+
)
|
142 |
+
})
|
143 |
+
FormDescription.displayName = "FormDescription"
|
144 |
+
|
145 |
+
const FormMessage = React.forwardRef<
|
146 |
+
HTMLParagraphElement,
|
147 |
+
React.HTMLAttributes<HTMLParagraphElement>
|
148 |
+
>(({ className, children, ...props }, ref) => {
|
149 |
+
const { error, formMessageId } = useFormField()
|
150 |
+
const body = error ? String(error?.message) : children
|
151 |
+
|
152 |
+
if (!body) {
|
153 |
+
return null
|
154 |
+
}
|
155 |
+
|
156 |
+
return (
|
157 |
+
<p
|
158 |
+
ref={ref}
|
159 |
+
id={formMessageId}
|
160 |
+
className={cn("text-sm font-medium text-destructive", className)}
|
161 |
+
{...props}
|
162 |
+
>
|
163 |
+
{body}
|
164 |
+
</p>
|
165 |
+
)
|
166 |
+
})
|
167 |
+
FormMessage.displayName = "FormMessage"
|
168 |
+
|
169 |
+
export {
|
170 |
+
useFormField,
|
171 |
+
Form,
|
172 |
+
FormItem,
|
173 |
+
FormLabel,
|
174 |
+
FormControl,
|
175 |
+
FormDescription,
|
176 |
+
FormMessage,
|
177 |
+
FormField,
|
178 |
+
}
|
components/ui/hover-card.tsx
ADDED
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
|
3 |
+
import * as React from "react"
|
4 |
+
import * as HoverCardPrimitive from "@radix-ui/react-hover-card"
|
5 |
+
|
6 |
+
import { cn } from "@/lib/utils"
|
7 |
+
|
8 |
+
const HoverCard = HoverCardPrimitive.Root
|
9 |
+
|
10 |
+
const HoverCardTrigger = HoverCardPrimitive.Trigger
|
11 |
+
|
12 |
+
const HoverCardContent = React.forwardRef<
|
13 |
+
React.ElementRef<typeof HoverCardPrimitive.Content>,
|
14 |
+
React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content>
|
15 |
+
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
|
16 |
+
<HoverCardPrimitive.Content
|
17 |
+
ref={ref}
|
18 |
+
align={align}
|
19 |
+
sideOffset={sideOffset}
|
20 |
+
className={cn(
|
21 |
+
"z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground 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",
|
22 |
+
className
|
23 |
+
)}
|
24 |
+
{...props}
|
25 |
+
/>
|
26 |
+
))
|
27 |
+
HoverCardContent.displayName = HoverCardPrimitive.Content.displayName
|
28 |
+
|
29 |
+
export { HoverCard, HoverCardTrigger, HoverCardContent }
|
components/ui/input-otp.tsx
ADDED
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
|
3 |
+
import * as React from "react"
|
4 |
+
import { OTPInput, OTPInputContext } from "input-otp"
|
5 |
+
import { Dot } from "lucide-react"
|
6 |
+
|
7 |
+
import { cn } from "@/lib/utils"
|
8 |
+
|
9 |
+
const InputOTP = React.forwardRef<
|
10 |
+
React.ElementRef<typeof OTPInput>,
|
11 |
+
React.ComponentPropsWithoutRef<typeof OTPInput>
|
12 |
+
>(({ className, containerClassName, ...props }, ref) => (
|
13 |
+
<OTPInput
|
14 |
+
ref={ref}
|
15 |
+
containerClassName={cn(
|
16 |
+
"flex items-center gap-2 has-[:disabled]:opacity-50",
|
17 |
+
containerClassName
|
18 |
+
)}
|
19 |
+
className={cn("disabled:cursor-not-allowed", className)}
|
20 |
+
{...props}
|
21 |
+
/>
|
22 |
+
))
|
23 |
+
InputOTP.displayName = "InputOTP"
|
24 |
+
|
25 |
+
const InputOTPGroup = React.forwardRef<
|
26 |
+
React.ElementRef<"div">,
|
27 |
+
React.ComponentPropsWithoutRef<"div">
|
28 |
+
>(({ className, ...props }, ref) => (
|
29 |
+
<div ref={ref} className={cn("flex items-center", className)} {...props} />
|
30 |
+
))
|
31 |
+
InputOTPGroup.displayName = "InputOTPGroup"
|
32 |
+
|
33 |
+
const InputOTPSlot = React.forwardRef<
|
34 |
+
React.ElementRef<"div">,
|
35 |
+
React.ComponentPropsWithoutRef<"div"> & { index: number }
|
36 |
+
>(({ index, className, ...props }, ref) => {
|
37 |
+
const inputOTPContext = React.useContext(OTPInputContext)
|
38 |
+
const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index]
|
39 |
+
|
40 |
+
return (
|
41 |
+
<div
|
42 |
+
ref={ref}
|
43 |
+
className={cn(
|
44 |
+
"relative flex h-10 w-10 items-center justify-center border-y border-r border-input text-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md",
|
45 |
+
isActive && "z-10 ring-2 ring-ring ring-offset-background",
|
46 |
+
className
|
47 |
+
)}
|
48 |
+
{...props}
|
49 |
+
>
|
50 |
+
{char}
|
51 |
+
{hasFakeCaret && (
|
52 |
+
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
|
53 |
+
<div className="h-4 w-px animate-caret-blink bg-foreground duration-1000" />
|
54 |
+
</div>
|
55 |
+
)}
|
56 |
+
</div>
|
57 |
+
)
|
58 |
+
})
|
59 |
+
InputOTPSlot.displayName = "InputOTPSlot"
|
60 |
+
|
61 |
+
const InputOTPSeparator = React.forwardRef<
|
62 |
+
React.ElementRef<"div">,
|
63 |
+
React.ComponentPropsWithoutRef<"div">
|
64 |
+
>(({ ...props }, ref) => (
|
65 |
+
<div ref={ref} role="separator" {...props}>
|
66 |
+
<Dot />
|
67 |
+
</div>
|
68 |
+
))
|
69 |
+
InputOTPSeparator.displayName = "InputOTPSeparator"
|
70 |
+
|
71 |
+
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator }
|
components/ui/input.tsx
ADDED
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import * as React from "react"
|
2 |
+
|
3 |
+
import { cn } from "@/lib/utils"
|
4 |
+
|
5 |
+
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
|
6 |
+
({ className, type, ...props }, ref) => {
|
7 |
+
return (
|
8 |
+
<input
|
9 |
+
type={type}
|
10 |
+
className={cn(
|
11 |
+
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground 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 md:text-sm",
|
12 |
+
className
|
13 |
+
)}
|
14 |
+
ref={ref}
|
15 |
+
{...props}
|
16 |
+
/>
|
17 |
+
)
|
18 |
+
}
|
19 |
+
)
|
20 |
+
Input.displayName = "Input"
|
21 |
+
|
22 |
+
export { Input }
|
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 }
|
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 bg-background p-1",
|
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-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
|
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-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
|
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 bg-popover p-1 text-popover-foreground 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",
|
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 bg-popover p-1 text-popover-foreground 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",
|
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-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-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-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-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-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-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-muted", 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-muted-foreground",
|
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 |
+
}
|
components/ui/navigation-menu.tsx
ADDED
@@ -0,0 +1,128 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import * as React from "react"
|
2 |
+
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu"
|
3 |
+
import { cva } from "class-variance-authority"
|
4 |
+
import { ChevronDown } from "lucide-react"
|
5 |
+
|
6 |
+
import { cn } from "@/lib/utils"
|
7 |
+
|
8 |
+
const NavigationMenu = React.forwardRef<
|
9 |
+
React.ElementRef<typeof NavigationMenuPrimitive.Root>,
|
10 |
+
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root>
|
11 |
+
>(({ className, children, ...props }, ref) => (
|
12 |
+
<NavigationMenuPrimitive.Root
|
13 |
+
ref={ref}
|
14 |
+
className={cn(
|
15 |
+
"relative z-10 flex max-w-max flex-1 items-center justify-center",
|
16 |
+
className
|
17 |
+
)}
|
18 |
+
{...props}
|
19 |
+
>
|
20 |
+
{children}
|
21 |
+
<NavigationMenuViewport />
|
22 |
+
</NavigationMenuPrimitive.Root>
|
23 |
+
))
|
24 |
+
NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName
|
25 |
+
|
26 |
+
const NavigationMenuList = React.forwardRef<
|
27 |
+
React.ElementRef<typeof NavigationMenuPrimitive.List>,
|
28 |
+
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.List>
|
29 |
+
>(({ className, ...props }, ref) => (
|
30 |
+
<NavigationMenuPrimitive.List
|
31 |
+
ref={ref}
|
32 |
+
className={cn(
|
33 |
+
"group flex flex-1 list-none items-center justify-center space-x-1",
|
34 |
+
className
|
35 |
+
)}
|
36 |
+
{...props}
|
37 |
+
/>
|
38 |
+
))
|
39 |
+
NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName
|
40 |
+
|
41 |
+
const NavigationMenuItem = NavigationMenuPrimitive.Item
|
42 |
+
|
43 |
+
const navigationMenuTriggerStyle = cva(
|
44 |
+
"group inline-flex h-10 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50"
|
45 |
+
)
|
46 |
+
|
47 |
+
const NavigationMenuTrigger = React.forwardRef<
|
48 |
+
React.ElementRef<typeof NavigationMenuPrimitive.Trigger>,
|
49 |
+
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger>
|
50 |
+
>(({ className, children, ...props }, ref) => (
|
51 |
+
<NavigationMenuPrimitive.Trigger
|
52 |
+
ref={ref}
|
53 |
+
className={cn(navigationMenuTriggerStyle(), "group", className)}
|
54 |
+
{...props}
|
55 |
+
>
|
56 |
+
{children}{" "}
|
57 |
+
<ChevronDown
|
58 |
+
className="relative top-[1px] ml-1 h-3 w-3 transition duration-200 group-data-[state=open]:rotate-180"
|
59 |
+
aria-hidden="true"
|
60 |
+
/>
|
61 |
+
</NavigationMenuPrimitive.Trigger>
|
62 |
+
))
|
63 |
+
NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName
|
64 |
+
|
65 |
+
const NavigationMenuContent = React.forwardRef<
|
66 |
+
React.ElementRef<typeof NavigationMenuPrimitive.Content>,
|
67 |
+
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Content>
|
68 |
+
>(({ className, ...props }, ref) => (
|
69 |
+
<NavigationMenuPrimitive.Content
|
70 |
+
ref={ref}
|
71 |
+
className={cn(
|
72 |
+
"left-0 top-0 w-full data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 md:absolute md:w-auto ",
|
73 |
+
className
|
74 |
+
)}
|
75 |
+
{...props}
|
76 |
+
/>
|
77 |
+
))
|
78 |
+
NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName
|
79 |
+
|
80 |
+
const NavigationMenuLink = NavigationMenuPrimitive.Link
|
81 |
+
|
82 |
+
const NavigationMenuViewport = React.forwardRef<
|
83 |
+
React.ElementRef<typeof NavigationMenuPrimitive.Viewport>,
|
84 |
+
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport>
|
85 |
+
>(({ className, ...props }, ref) => (
|
86 |
+
<div className={cn("absolute left-0 top-full flex justify-center")}>
|
87 |
+
<NavigationMenuPrimitive.Viewport
|
88 |
+
className={cn(
|
89 |
+
"origin-top-center relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 md:w-[var(--radix-navigation-menu-viewport-width)]",
|
90 |
+
className
|
91 |
+
)}
|
92 |
+
ref={ref}
|
93 |
+
{...props}
|
94 |
+
/>
|
95 |
+
</div>
|
96 |
+
))
|
97 |
+
NavigationMenuViewport.displayName =
|
98 |
+
NavigationMenuPrimitive.Viewport.displayName
|
99 |
+
|
100 |
+
const NavigationMenuIndicator = React.forwardRef<
|
101 |
+
React.ElementRef<typeof NavigationMenuPrimitive.Indicator>,
|
102 |
+
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Indicator>
|
103 |
+
>(({ className, ...props }, ref) => (
|
104 |
+
<NavigationMenuPrimitive.Indicator
|
105 |
+
ref={ref}
|
106 |
+
className={cn(
|
107 |
+
"top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in",
|
108 |
+
className
|
109 |
+
)}
|
110 |
+
{...props}
|
111 |
+
>
|
112 |
+
<div className="relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-border shadow-md" />
|
113 |
+
</NavigationMenuPrimitive.Indicator>
|
114 |
+
))
|
115 |
+
NavigationMenuIndicator.displayName =
|
116 |
+
NavigationMenuPrimitive.Indicator.displayName
|
117 |
+
|
118 |
+
export {
|
119 |
+
navigationMenuTriggerStyle,
|
120 |
+
NavigationMenu,
|
121 |
+
NavigationMenuList,
|
122 |
+
NavigationMenuItem,
|
123 |
+
NavigationMenuContent,
|
124 |
+
NavigationMenuTrigger,
|
125 |
+
NavigationMenuLink,
|
126 |
+
NavigationMenuIndicator,
|
127 |
+
NavigationMenuViewport,
|
128 |
+
}
|
components/ui/pagination.tsx
ADDED
@@ -0,0 +1,117 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import * as React from "react"
|
2 |
+
import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react"
|
3 |
+
|
4 |
+
import { cn } from "@/lib/utils"
|
5 |
+
import { ButtonProps, buttonVariants } from "@/components/ui/button"
|
6 |
+
|
7 |
+
const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => (
|
8 |
+
<nav
|
9 |
+
role="navigation"
|
10 |
+
aria-label="pagination"
|
11 |
+
className={cn("mx-auto flex w-full justify-center", className)}
|
12 |
+
{...props}
|
13 |
+
/>
|
14 |
+
)
|
15 |
+
Pagination.displayName = "Pagination"
|
16 |
+
|
17 |
+
const PaginationContent = React.forwardRef<
|
18 |
+
HTMLUListElement,
|
19 |
+
React.ComponentProps<"ul">
|
20 |
+
>(({ className, ...props }, ref) => (
|
21 |
+
<ul
|
22 |
+
ref={ref}
|
23 |
+
className={cn("flex flex-row items-center gap-1", className)}
|
24 |
+
{...props}
|
25 |
+
/>
|
26 |
+
))
|
27 |
+
PaginationContent.displayName = "PaginationContent"
|
28 |
+
|
29 |
+
const PaginationItem = React.forwardRef<
|
30 |
+
HTMLLIElement,
|
31 |
+
React.ComponentProps<"li">
|
32 |
+
>(({ className, ...props }, ref) => (
|
33 |
+
<li ref={ref} className={cn("", className)} {...props} />
|
34 |
+
))
|
35 |
+
PaginationItem.displayName = "PaginationItem"
|
36 |
+
|
37 |
+
type PaginationLinkProps = {
|
38 |
+
isActive?: boolean
|
39 |
+
} & Pick<ButtonProps, "size"> &
|
40 |
+
React.ComponentProps<"a">
|
41 |
+
|
42 |
+
const PaginationLink = ({
|
43 |
+
className,
|
44 |
+
isActive,
|
45 |
+
size = "icon",
|
46 |
+
...props
|
47 |
+
}: PaginationLinkProps) => (
|
48 |
+
<a
|
49 |
+
aria-current={isActive ? "page" : undefined}
|
50 |
+
className={cn(
|
51 |
+
buttonVariants({
|
52 |
+
variant: isActive ? "outline" : "ghost",
|
53 |
+
size,
|
54 |
+
}),
|
55 |
+
className
|
56 |
+
)}
|
57 |
+
{...props}
|
58 |
+
/>
|
59 |
+
)
|
60 |
+
PaginationLink.displayName = "PaginationLink"
|
61 |
+
|
62 |
+
const PaginationPrevious = ({
|
63 |
+
className,
|
64 |
+
...props
|
65 |
+
}: React.ComponentProps<typeof PaginationLink>) => (
|
66 |
+
<PaginationLink
|
67 |
+
aria-label="Go to previous page"
|
68 |
+
size="default"
|
69 |
+
className={cn("gap-1 pl-2.5", className)}
|
70 |
+
{...props}
|
71 |
+
>
|
72 |
+
<ChevronLeft className="h-4 w-4" />
|
73 |
+
<span>Previous</span>
|
74 |
+
</PaginationLink>
|
75 |
+
)
|
76 |
+
PaginationPrevious.displayName = "PaginationPrevious"
|
77 |
+
|
78 |
+
const PaginationNext = ({
|
79 |
+
className,
|
80 |
+
...props
|
81 |
+
}: React.ComponentProps<typeof PaginationLink>) => (
|
82 |
+
<PaginationLink
|
83 |
+
aria-label="Go to next page"
|
84 |
+
size="default"
|
85 |
+
className={cn("gap-1 pr-2.5", className)}
|
86 |
+
{...props}
|
87 |
+
>
|
88 |
+
<span>Next</span>
|
89 |
+
<ChevronRight className="h-4 w-4" />
|
90 |
+
</PaginationLink>
|
91 |
+
)
|
92 |
+
PaginationNext.displayName = "PaginationNext"
|
93 |
+
|
94 |
+
const PaginationEllipsis = ({
|
95 |
+
className,
|
96 |
+
...props
|
97 |
+
}: React.ComponentProps<"span">) => (
|
98 |
+
<span
|
99 |
+
aria-hidden
|
100 |
+
className={cn("flex h-9 w-9 items-center justify-center", className)}
|
101 |
+
{...props}
|
102 |
+
>
|
103 |
+
<MoreHorizontal className="h-4 w-4" />
|
104 |
+
<span className="sr-only">More pages</span>
|
105 |
+
</span>
|
106 |
+
)
|
107 |
+
PaginationEllipsis.displayName = "PaginationEllipsis"
|
108 |
+
|
109 |
+
export {
|
110 |
+
Pagination,
|
111 |
+
PaginationContent,
|
112 |
+
PaginationEllipsis,
|
113 |
+
PaginationItem,
|
114 |
+
PaginationLink,
|
115 |
+
PaginationNext,
|
116 |
+
PaginationPrevious,
|
117 |
+
}
|
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 bg-popover p-4 text-popover-foreground 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",
|
23 |
+
className
|
24 |
+
)}
|
25 |
+
{...props}
|
26 |
+
/>
|
27 |
+
</PopoverPrimitive.Portal>
|
28 |
+
))
|
29 |
+
PopoverContent.displayName = PopoverPrimitive.Content.displayName
|
30 |
+
|
31 |
+
export { Popover, PopoverTrigger, PopoverContent }
|
components/ui/progress.tsx
ADDED
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
|
3 |
+
import * as React from "react"
|
4 |
+
import * as ProgressPrimitive from "@radix-ui/react-progress"
|
5 |
+
|
6 |
+
import { cn } from "@/lib/utils"
|
7 |
+
|
8 |
+
const Progress = React.forwardRef<
|
9 |
+
React.ElementRef<typeof ProgressPrimitive.Root>,
|
10 |
+
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
|
11 |
+
>(({ className, value, ...props }, ref) => (
|
12 |
+
<ProgressPrimitive.Root
|
13 |
+
ref={ref}
|
14 |
+
className={cn(
|
15 |
+
"relative h-4 w-full overflow-hidden rounded-full bg-secondary",
|
16 |
+
className
|
17 |
+
)}
|
18 |
+
{...props}
|
19 |
+
>
|
20 |
+
<ProgressPrimitive.Indicator
|
21 |
+
className="h-full w-full flex-1 bg-primary transition-all"
|
22 |
+
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
23 |
+
/>
|
24 |
+
</ProgressPrimitive.Root>
|
25 |
+
))
|
26 |
+
Progress.displayName = ProgressPrimitive.Root.displayName
|
27 |
+
|
28 |
+
export { Progress }
|
components/ui/radio-group.tsx
ADDED
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
|
3 |
+
import * as React from "react"
|
4 |
+
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
|
5 |
+
import { Circle } from "lucide-react"
|
6 |
+
|
7 |
+
import { cn } from "@/lib/utils"
|
8 |
+
|
9 |
+
const RadioGroup = React.forwardRef<
|
10 |
+
React.ElementRef<typeof RadioGroupPrimitive.Root>,
|
11 |
+
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
|
12 |
+
>(({ className, ...props }, ref) => {
|
13 |
+
return (
|
14 |
+
<RadioGroupPrimitive.Root
|
15 |
+
className={cn("grid gap-2", className)}
|
16 |
+
{...props}
|
17 |
+
ref={ref}
|
18 |
+
/>
|
19 |
+
)
|
20 |
+
})
|
21 |
+
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
|
22 |
+
|
23 |
+
const RadioGroupItem = React.forwardRef<
|
24 |
+
React.ElementRef<typeof RadioGroupPrimitive.Item>,
|
25 |
+
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
|
26 |
+
>(({ className, ...props }, ref) => {
|
27 |
+
return (
|
28 |
+
<RadioGroupPrimitive.Item
|
29 |
+
ref={ref}
|
30 |
+
className={cn(
|
31 |
+
"aspect-square h-4 w-4 rounded-full border border-primary text-primary ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
32 |
+
className
|
33 |
+
)}
|
34 |
+
{...props}
|
35 |
+
>
|
36 |
+
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
|
37 |
+
<Circle className="h-2.5 w-2.5 fill-current text-current" />
|
38 |
+
</RadioGroupPrimitive.Indicator>
|
39 |
+
</RadioGroupPrimitive.Item>
|
40 |
+
)
|
41 |
+
})
|
42 |
+
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName
|
43 |
+
|
44 |
+
export { RadioGroup, RadioGroupItem }
|
components/ui/resizable.tsx
ADDED
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
|
3 |
+
import { GripVertical } from "lucide-react"
|
4 |
+
import * as ResizablePrimitive from "react-resizable-panels"
|
5 |
+
|
6 |
+
import { cn } from "@/lib/utils"
|
7 |
+
|
8 |
+
const ResizablePanelGroup = ({
|
9 |
+
className,
|
10 |
+
...props
|
11 |
+
}: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) => (
|
12 |
+
<ResizablePrimitive.PanelGroup
|
13 |
+
className={cn(
|
14 |
+
"flex h-full w-full data-[panel-group-direction=vertical]:flex-col",
|
15 |
+
className
|
16 |
+
)}
|
17 |
+
{...props}
|
18 |
+
/>
|
19 |
+
)
|
20 |
+
|
21 |
+
const ResizablePanel = ResizablePrimitive.Panel
|
22 |
+
|
23 |
+
const ResizableHandle = ({
|
24 |
+
withHandle,
|
25 |
+
className,
|
26 |
+
...props
|
27 |
+
}: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {
|
28 |
+
withHandle?: boolean
|
29 |
+
}) => (
|
30 |
+
<ResizablePrimitive.PanelResizeHandle
|
31 |
+
className={cn(
|
32 |
+
"relative flex w-px items-center justify-center bg-border after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 [&[data-panel-group-direction=vertical]>div]:rotate-90",
|
33 |
+
className
|
34 |
+
)}
|
35 |
+
{...props}
|
36 |
+
>
|
37 |
+
{withHandle && (
|
38 |
+
<div className="z-10 flex h-4 w-3 items-center justify-center rounded-sm border bg-border">
|
39 |
+
<GripVertical className="h-2.5 w-2.5" />
|
40 |
+
</div>
|
41 |
+
)}
|
42 |
+
</ResizablePrimitive.PanelResizeHandle>
|
43 |
+
)
|
44 |
+
|
45 |
+
export { ResizablePanelGroup, ResizablePanel, ResizableHandle }
|
components/ui/scroll-area.tsx
ADDED
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
|
3 |
+
import * as React from "react"
|
4 |
+
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
|
5 |
+
|
6 |
+
import { cn } from "@/lib/utils"
|
7 |
+
|
8 |
+
const ScrollArea = React.forwardRef<
|
9 |
+
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
|
10 |
+
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
|
11 |
+
>(({ className, children, ...props }, ref) => (
|
12 |
+
<ScrollAreaPrimitive.Root
|
13 |
+
ref={ref}
|
14 |
+
className={cn("relative overflow-hidden", className)}
|
15 |
+
{...props}
|
16 |
+
>
|
17 |
+
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
|
18 |
+
{children}
|
19 |
+
</ScrollAreaPrimitive.Viewport>
|
20 |
+
<ScrollBar />
|
21 |
+
<ScrollAreaPrimitive.Corner />
|
22 |
+
</ScrollAreaPrimitive.Root>
|
23 |
+
))
|
24 |
+
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
|
25 |
+
|
26 |
+
const ScrollBar = React.forwardRef<
|
27 |
+
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
|
28 |
+
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
|
29 |
+
>(({ className, orientation = "vertical", ...props }, ref) => (
|
30 |
+
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
31 |
+
ref={ref}
|
32 |
+
orientation={orientation}
|
33 |
+
className={cn(
|
34 |
+
"flex touch-none select-none transition-colors",
|
35 |
+
orientation === "vertical" &&
|
36 |
+
"h-full w-2.5 border-l border-l-transparent p-[1px]",
|
37 |
+
orientation === "horizontal" &&
|
38 |
+
"h-2.5 flex-col border-t border-t-transparent p-[1px]",
|
39 |
+
className
|
40 |
+
)}
|
41 |
+
{...props}
|
42 |
+
>
|
43 |
+
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
|
44 |
+
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
45 |
+
))
|
46 |
+
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
|
47 |
+
|
48 |
+
export { ScrollArea, ScrollBar }
|
components/ui/select.tsx
ADDED
@@ -0,0 +1,160 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
|
3 |
+
import * as React from "react"
|
4 |
+
import * as SelectPrimitive from "@radix-ui/react-select"
|
5 |
+
import { Check, ChevronDown, ChevronUp } 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-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
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 SelectScrollUpButton = React.forwardRef<
|
36 |
+
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
|
37 |
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
|
38 |
+
>(({ className, ...props }, ref) => (
|
39 |
+
<SelectPrimitive.ScrollUpButton
|
40 |
+
ref={ref}
|
41 |
+
className={cn(
|
42 |
+
"flex cursor-default items-center justify-center py-1",
|
43 |
+
className
|
44 |
+
)}
|
45 |
+
{...props}
|
46 |
+
>
|
47 |
+
<ChevronUp className="h-4 w-4" />
|
48 |
+
</SelectPrimitive.ScrollUpButton>
|
49 |
+
))
|
50 |
+
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
|
51 |
+
|
52 |
+
const SelectScrollDownButton = React.forwardRef<
|
53 |
+
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
|
54 |
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
|
55 |
+
>(({ className, ...props }, ref) => (
|
56 |
+
<SelectPrimitive.ScrollDownButton
|
57 |
+
ref={ref}
|
58 |
+
className={cn(
|
59 |
+
"flex cursor-default items-center justify-center py-1",
|
60 |
+
className
|
61 |
+
)}
|
62 |
+
{...props}
|
63 |
+
>
|
64 |
+
<ChevronDown className="h-4 w-4" />
|
65 |
+
</SelectPrimitive.ScrollDownButton>
|
66 |
+
))
|
67 |
+
SelectScrollDownButton.displayName =
|
68 |
+
SelectPrimitive.ScrollDownButton.displayName
|
69 |
+
|
70 |
+
const SelectContent = React.forwardRef<
|
71 |
+
React.ElementRef<typeof SelectPrimitive.Content>,
|
72 |
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
73 |
+
>(({ className, children, position = "popper", ...props }, ref) => (
|
74 |
+
<SelectPrimitive.Portal>
|
75 |
+
<SelectPrimitive.Content
|
76 |
+
ref={ref}
|
77 |
+
className={cn(
|
78 |
+
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground 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",
|
79 |
+
position === "popper" &&
|
80 |
+
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
81 |
+
className
|
82 |
+
)}
|
83 |
+
position={position}
|
84 |
+
{...props}
|
85 |
+
>
|
86 |
+
<SelectScrollUpButton />
|
87 |
+
<SelectPrimitive.Viewport
|
88 |
+
className={cn(
|
89 |
+
"p-1",
|
90 |
+
position === "popper" &&
|
91 |
+
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
|
92 |
+
)}
|
93 |
+
>
|
94 |
+
{children}
|
95 |
+
</SelectPrimitive.Viewport>
|
96 |
+
<SelectScrollDownButton />
|
97 |
+
</SelectPrimitive.Content>
|
98 |
+
</SelectPrimitive.Portal>
|
99 |
+
))
|
100 |
+
SelectContent.displayName = SelectPrimitive.Content.displayName
|
101 |
+
|
102 |
+
const SelectLabel = React.forwardRef<
|
103 |
+
React.ElementRef<typeof SelectPrimitive.Label>,
|
104 |
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
105 |
+
>(({ className, ...props }, ref) => (
|
106 |
+
<SelectPrimitive.Label
|
107 |
+
ref={ref}
|
108 |
+
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
|
109 |
+
{...props}
|
110 |
+
/>
|
111 |
+
))
|
112 |
+
SelectLabel.displayName = SelectPrimitive.Label.displayName
|
113 |
+
|
114 |
+
const SelectItem = React.forwardRef<
|
115 |
+
React.ElementRef<typeof SelectPrimitive.Item>,
|
116 |
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
117 |
+
>(({ className, children, ...props }, ref) => (
|
118 |
+
<SelectPrimitive.Item
|
119 |
+
ref={ref}
|
120 |
+
className={cn(
|
121 |
+
"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-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
122 |
+
className
|
123 |
+
)}
|
124 |
+
{...props}
|
125 |
+
>
|
126 |
+
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
127 |
+
<SelectPrimitive.ItemIndicator>
|
128 |
+
<Check className="h-4 w-4" />
|
129 |
+
</SelectPrimitive.ItemIndicator>
|
130 |
+
</span>
|
131 |
+
|
132 |
+
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
133 |
+
</SelectPrimitive.Item>
|
134 |
+
))
|
135 |
+
SelectItem.displayName = SelectPrimitive.Item.displayName
|
136 |
+
|
137 |
+
const SelectSeparator = React.forwardRef<
|
138 |
+
React.ElementRef<typeof SelectPrimitive.Separator>,
|
139 |
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
140 |
+
>(({ className, ...props }, ref) => (
|
141 |
+
<SelectPrimitive.Separator
|
142 |
+
ref={ref}
|
143 |
+
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
144 |
+
{...props}
|
145 |
+
/>
|
146 |
+
))
|
147 |
+
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
|
148 |
+
|
149 |
+
export {
|
150 |
+
Select,
|
151 |
+
SelectGroup,
|
152 |
+
SelectValue,
|
153 |
+
SelectTrigger,
|
154 |
+
SelectContent,
|
155 |
+
SelectLabel,
|
156 |
+
SelectItem,
|
157 |
+
SelectSeparator,
|
158 |
+
SelectScrollUpButton,
|
159 |
+
SelectScrollDownButton,
|
160 |
+
}
|
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-border",
|
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 }
|
components/ui/sheet.tsx
ADDED
@@ -0,0 +1,140 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
|
3 |
+
import * as React from "react"
|
4 |
+
import * as SheetPrimitive from "@radix-ui/react-dialog"
|
5 |
+
import { cva, type VariantProps } from "class-variance-authority"
|
6 |
+
import { X } from "lucide-react"
|
7 |
+
|
8 |
+
import { cn } from "@/lib/utils"
|
9 |
+
|
10 |
+
const Sheet = SheetPrimitive.Root
|
11 |
+
|
12 |
+
const SheetTrigger = SheetPrimitive.Trigger
|
13 |
+
|
14 |
+
const SheetClose = SheetPrimitive.Close
|
15 |
+
|
16 |
+
const SheetPortal = SheetPrimitive.Portal
|
17 |
+
|
18 |
+
const SheetOverlay = React.forwardRef<
|
19 |
+
React.ElementRef<typeof SheetPrimitive.Overlay>,
|
20 |
+
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
|
21 |
+
>(({ className, ...props }, ref) => (
|
22 |
+
<SheetPrimitive.Overlay
|
23 |
+
className={cn(
|
24 |
+
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
25 |
+
className
|
26 |
+
)}
|
27 |
+
{...props}
|
28 |
+
ref={ref}
|
29 |
+
/>
|
30 |
+
))
|
31 |
+
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
|
32 |
+
|
33 |
+
const sheetVariants = cva(
|
34 |
+
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
|
35 |
+
{
|
36 |
+
variants: {
|
37 |
+
side: {
|
38 |
+
top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
|
39 |
+
bottom:
|
40 |
+
"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
|
41 |
+
left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
|
42 |
+
right:
|
43 |
+
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
|
44 |
+
},
|
45 |
+
},
|
46 |
+
defaultVariants: {
|
47 |
+
side: "right",
|
48 |
+
},
|
49 |
+
}
|
50 |
+
)
|
51 |
+
|
52 |
+
interface SheetContentProps
|
53 |
+
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
|
54 |
+
VariantProps<typeof sheetVariants> {}
|
55 |
+
|
56 |
+
const SheetContent = React.forwardRef<
|
57 |
+
React.ElementRef<typeof SheetPrimitive.Content>,
|
58 |
+
SheetContentProps
|
59 |
+
>(({ side = "right", className, children, ...props }, ref) => (
|
60 |
+
<SheetPortal>
|
61 |
+
<SheetOverlay />
|
62 |
+
<SheetPrimitive.Content
|
63 |
+
ref={ref}
|
64 |
+
className={cn(sheetVariants({ side }), className)}
|
65 |
+
{...props}
|
66 |
+
>
|
67 |
+
{children}
|
68 |
+
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
|
69 |
+
<X className="h-4 w-4" />
|
70 |
+
<span className="sr-only">Close</span>
|
71 |
+
</SheetPrimitive.Close>
|
72 |
+
</SheetPrimitive.Content>
|
73 |
+
</SheetPortal>
|
74 |
+
))
|
75 |
+
SheetContent.displayName = SheetPrimitive.Content.displayName
|
76 |
+
|
77 |
+
const SheetHeader = ({
|
78 |
+
className,
|
79 |
+
...props
|
80 |
+
}: React.HTMLAttributes<HTMLDivElement>) => (
|
81 |
+
<div
|
82 |
+
className={cn(
|
83 |
+
"flex flex-col space-y-2 text-center sm:text-left",
|
84 |
+
className
|
85 |
+
)}
|
86 |
+
{...props}
|
87 |
+
/>
|
88 |
+
)
|
89 |
+
SheetHeader.displayName = "SheetHeader"
|
90 |
+
|
91 |
+
const SheetFooter = ({
|
92 |
+
className,
|
93 |
+
...props
|
94 |
+
}: React.HTMLAttributes<HTMLDivElement>) => (
|
95 |
+
<div
|
96 |
+
className={cn(
|
97 |
+
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
98 |
+
className
|
99 |
+
)}
|
100 |
+
{...props}
|
101 |
+
/>
|
102 |
+
)
|
103 |
+
SheetFooter.displayName = "SheetFooter"
|
104 |
+
|
105 |
+
const SheetTitle = React.forwardRef<
|
106 |
+
React.ElementRef<typeof SheetPrimitive.Title>,
|
107 |
+
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
|
108 |
+
>(({ className, ...props }, ref) => (
|
109 |
+
<SheetPrimitive.Title
|
110 |
+
ref={ref}
|
111 |
+
className={cn("text-lg font-semibold text-foreground", className)}
|
112 |
+
{...props}
|
113 |
+
/>
|
114 |
+
))
|
115 |
+
SheetTitle.displayName = SheetPrimitive.Title.displayName
|
116 |
+
|
117 |
+
const SheetDescription = React.forwardRef<
|
118 |
+
React.ElementRef<typeof SheetPrimitive.Description>,
|
119 |
+
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
|
120 |
+
>(({ className, ...props }, ref) => (
|
121 |
+
<SheetPrimitive.Description
|
122 |
+
ref={ref}
|
123 |
+
className={cn("text-sm text-muted-foreground", className)}
|
124 |
+
{...props}
|
125 |
+
/>
|
126 |
+
))
|
127 |
+
SheetDescription.displayName = SheetPrimitive.Description.displayName
|
128 |
+
|
129 |
+
export {
|
130 |
+
Sheet,
|
131 |
+
SheetPortal,
|
132 |
+
SheetOverlay,
|
133 |
+
SheetTrigger,
|
134 |
+
SheetClose,
|
135 |
+
SheetContent,
|
136 |
+
SheetHeader,
|
137 |
+
SheetFooter,
|
138 |
+
SheetTitle,
|
139 |
+
SheetDescription,
|
140 |
+
}
|
components/ui/sidebar.tsx
ADDED
@@ -0,0 +1,763 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
|
3 |
+
import * as React from "react"
|
4 |
+
import { Slot } from "@radix-ui/react-slot"
|
5 |
+
import { VariantProps, cva } from "class-variance-authority"
|
6 |
+
import { PanelLeft } from "lucide-react"
|
7 |
+
|
8 |
+
import { useIsMobile } from "@/hooks/use-mobile"
|
9 |
+
import { cn } from "@/lib/utils"
|
10 |
+
import { Button } from "@/components/ui/button"
|
11 |
+
import { Input } from "@/components/ui/input"
|
12 |
+
import { Separator } from "@/components/ui/separator"
|
13 |
+
import { Sheet, SheetContent } from "@/components/ui/sheet"
|
14 |
+
import { Skeleton } from "@/components/ui/skeleton"
|
15 |
+
import {
|
16 |
+
Tooltip,
|
17 |
+
TooltipContent,
|
18 |
+
TooltipProvider,
|
19 |
+
TooltipTrigger,
|
20 |
+
} from "@/components/ui/tooltip"
|
21 |
+
|
22 |
+
const SIDEBAR_COOKIE_NAME = "sidebar:state"
|
23 |
+
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
|
24 |
+
const SIDEBAR_WIDTH = "16rem"
|
25 |
+
const SIDEBAR_WIDTH_MOBILE = "18rem"
|
26 |
+
const SIDEBAR_WIDTH_ICON = "3rem"
|
27 |
+
const SIDEBAR_KEYBOARD_SHORTCUT = "b"
|
28 |
+
|
29 |
+
type SidebarContext = {
|
30 |
+
state: "expanded" | "collapsed"
|
31 |
+
open: boolean
|
32 |
+
setOpen: (open: boolean) => void
|
33 |
+
openMobile: boolean
|
34 |
+
setOpenMobile: (open: boolean) => void
|
35 |
+
isMobile: boolean
|
36 |
+
toggleSidebar: () => void
|
37 |
+
}
|
38 |
+
|
39 |
+
const SidebarContext = React.createContext<SidebarContext | null>(null)
|
40 |
+
|
41 |
+
function useSidebar() {
|
42 |
+
const context = React.useContext(SidebarContext)
|
43 |
+
if (!context) {
|
44 |
+
throw new Error("useSidebar must be used within a SidebarProvider.")
|
45 |
+
}
|
46 |
+
|
47 |
+
return context
|
48 |
+
}
|
49 |
+
|
50 |
+
const SidebarProvider = React.forwardRef<
|
51 |
+
HTMLDivElement,
|
52 |
+
React.ComponentProps<"div"> & {
|
53 |
+
defaultOpen?: boolean
|
54 |
+
open?: boolean
|
55 |
+
onOpenChange?: (open: boolean) => void
|
56 |
+
}
|
57 |
+
>(
|
58 |
+
(
|
59 |
+
{
|
60 |
+
defaultOpen = true,
|
61 |
+
open: openProp,
|
62 |
+
onOpenChange: setOpenProp,
|
63 |
+
className,
|
64 |
+
style,
|
65 |
+
children,
|
66 |
+
...props
|
67 |
+
},
|
68 |
+
ref
|
69 |
+
) => {
|
70 |
+
const isMobile = useIsMobile()
|
71 |
+
const [openMobile, setOpenMobile] = React.useState(false)
|
72 |
+
|
73 |
+
// This is the internal state of the sidebar.
|
74 |
+
// We use openProp and setOpenProp for control from outside the component.
|
75 |
+
const [_open, _setOpen] = React.useState(defaultOpen)
|
76 |
+
const open = openProp ?? _open
|
77 |
+
const setOpen = React.useCallback(
|
78 |
+
(value: boolean | ((value: boolean) => boolean)) => {
|
79 |
+
const openState = typeof value === "function" ? value(open) : value
|
80 |
+
if (setOpenProp) {
|
81 |
+
setOpenProp(openState)
|
82 |
+
} else {
|
83 |
+
_setOpen(openState)
|
84 |
+
}
|
85 |
+
|
86 |
+
// This sets the cookie to keep the sidebar state.
|
87 |
+
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
|
88 |
+
},
|
89 |
+
[setOpenProp, open]
|
90 |
+
)
|
91 |
+
|
92 |
+
// Helper to toggle the sidebar.
|
93 |
+
const toggleSidebar = React.useCallback(() => {
|
94 |
+
return isMobile
|
95 |
+
? setOpenMobile((open) => !open)
|
96 |
+
: setOpen((open) => !open)
|
97 |
+
}, [isMobile, setOpen, setOpenMobile])
|
98 |
+
|
99 |
+
// Adds a keyboard shortcut to toggle the sidebar.
|
100 |
+
React.useEffect(() => {
|
101 |
+
const handleKeyDown = (event: KeyboardEvent) => {
|
102 |
+
if (
|
103 |
+
event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
|
104 |
+
(event.metaKey || event.ctrlKey)
|
105 |
+
) {
|
106 |
+
event.preventDefault()
|
107 |
+
toggleSidebar()
|
108 |
+
}
|
109 |
+
}
|
110 |
+
|
111 |
+
window.addEventListener("keydown", handleKeyDown)
|
112 |
+
return () => window.removeEventListener("keydown", handleKeyDown)
|
113 |
+
}, [toggleSidebar])
|
114 |
+
|
115 |
+
// We add a state so that we can do data-state="expanded" or "collapsed".
|
116 |
+
// This makes it easier to style the sidebar with Tailwind classes.
|
117 |
+
const state = open ? "expanded" : "collapsed"
|
118 |
+
|
119 |
+
const contextValue = React.useMemo<SidebarContext>(
|
120 |
+
() => ({
|
121 |
+
state,
|
122 |
+
open,
|
123 |
+
setOpen,
|
124 |
+
isMobile,
|
125 |
+
openMobile,
|
126 |
+
setOpenMobile,
|
127 |
+
toggleSidebar,
|
128 |
+
}),
|
129 |
+
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]
|
130 |
+
)
|
131 |
+
|
132 |
+
return (
|
133 |
+
<SidebarContext.Provider value={contextValue}>
|
134 |
+
<TooltipProvider delayDuration={0}>
|
135 |
+
<div
|
136 |
+
style={
|
137 |
+
{
|
138 |
+
"--sidebar-width": SIDEBAR_WIDTH,
|
139 |
+
"--sidebar-width-icon": SIDEBAR_WIDTH_ICON,
|
140 |
+
...style,
|
141 |
+
} as React.CSSProperties
|
142 |
+
}
|
143 |
+
className={cn(
|
144 |
+
"group/sidebar-wrapper flex min-h-svh w-full has-[[data-variant=inset]]:bg-sidebar",
|
145 |
+
className
|
146 |
+
)}
|
147 |
+
ref={ref}
|
148 |
+
{...props}
|
149 |
+
>
|
150 |
+
{children}
|
151 |
+
</div>
|
152 |
+
</TooltipProvider>
|
153 |
+
</SidebarContext.Provider>
|
154 |
+
)
|
155 |
+
}
|
156 |
+
)
|
157 |
+
SidebarProvider.displayName = "SidebarProvider"
|
158 |
+
|
159 |
+
const Sidebar = React.forwardRef<
|
160 |
+
HTMLDivElement,
|
161 |
+
React.ComponentProps<"div"> & {
|
162 |
+
side?: "left" | "right"
|
163 |
+
variant?: "sidebar" | "floating" | "inset"
|
164 |
+
collapsible?: "offcanvas" | "icon" | "none"
|
165 |
+
}
|
166 |
+
>(
|
167 |
+
(
|
168 |
+
{
|
169 |
+
side = "left",
|
170 |
+
variant = "sidebar",
|
171 |
+
collapsible = "offcanvas",
|
172 |
+
className,
|
173 |
+
children,
|
174 |
+
...props
|
175 |
+
},
|
176 |
+
ref
|
177 |
+
) => {
|
178 |
+
const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
|
179 |
+
|
180 |
+
if (collapsible === "none") {
|
181 |
+
return (
|
182 |
+
<div
|
183 |
+
className={cn(
|
184 |
+
"flex h-full w-[--sidebar-width] flex-col bg-sidebar text-sidebar-foreground",
|
185 |
+
className
|
186 |
+
)}
|
187 |
+
ref={ref}
|
188 |
+
{...props}
|
189 |
+
>
|
190 |
+
{children}
|
191 |
+
</div>
|
192 |
+
)
|
193 |
+
}
|
194 |
+
|
195 |
+
if (isMobile) {
|
196 |
+
return (
|
197 |
+
<Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
|
198 |
+
<SheetContent
|
199 |
+
data-sidebar="sidebar"
|
200 |
+
data-mobile="true"
|
201 |
+
className="w-[--sidebar-width] bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden"
|
202 |
+
style={
|
203 |
+
{
|
204 |
+
"--sidebar-width": SIDEBAR_WIDTH_MOBILE,
|
205 |
+
} as React.CSSProperties
|
206 |
+
}
|
207 |
+
side={side}
|
208 |
+
>
|
209 |
+
<div className="flex h-full w-full flex-col">{children}</div>
|
210 |
+
</SheetContent>
|
211 |
+
</Sheet>
|
212 |
+
)
|
213 |
+
}
|
214 |
+
|
215 |
+
return (
|
216 |
+
<div
|
217 |
+
ref={ref}
|
218 |
+
className="group peer hidden md:block text-sidebar-foreground"
|
219 |
+
data-state={state}
|
220 |
+
data-collapsible={state === "collapsed" ? collapsible : ""}
|
221 |
+
data-variant={variant}
|
222 |
+
data-side={side}
|
223 |
+
>
|
224 |
+
{/* This is what handles the sidebar gap on desktop */}
|
225 |
+
<div
|
226 |
+
className={cn(
|
227 |
+
"duration-200 relative h-svh w-[--sidebar-width] bg-transparent transition-[width] ease-linear",
|
228 |
+
"group-data-[collapsible=offcanvas]:w-0",
|
229 |
+
"group-data-[side=right]:rotate-180",
|
230 |
+
variant === "floating" || variant === "inset"
|
231 |
+
? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4))]"
|
232 |
+
: "group-data-[collapsible=icon]:w-[--sidebar-width-icon]"
|
233 |
+
)}
|
234 |
+
/>
|
235 |
+
<div
|
236 |
+
className={cn(
|
237 |
+
"duration-200 fixed inset-y-0 z-10 hidden h-svh w-[--sidebar-width] transition-[left,right,width] ease-linear md:flex",
|
238 |
+
side === "left"
|
239 |
+
? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"
|
240 |
+
: "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
|
241 |
+
// Adjust the padding for floating and inset variants.
|
242 |
+
variant === "floating" || variant === "inset"
|
243 |
+
? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4)_+2px)]"
|
244 |
+
: "group-data-[collapsible=icon]:w-[--sidebar-width-icon] group-data-[side=left]:border-r group-data-[side=right]:border-l",
|
245 |
+
className
|
246 |
+
)}
|
247 |
+
{...props}
|
248 |
+
>
|
249 |
+
<div
|
250 |
+
data-sidebar="sidebar"
|
251 |
+
className="flex h-full w-full flex-col bg-sidebar group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:border-sidebar-border group-data-[variant=floating]:shadow"
|
252 |
+
>
|
253 |
+
{children}
|
254 |
+
</div>
|
255 |
+
</div>
|
256 |
+
</div>
|
257 |
+
)
|
258 |
+
}
|
259 |
+
)
|
260 |
+
Sidebar.displayName = "Sidebar"
|
261 |
+
|
262 |
+
const SidebarTrigger = React.forwardRef<
|
263 |
+
React.ElementRef<typeof Button>,
|
264 |
+
React.ComponentProps<typeof Button>
|
265 |
+
>(({ className, onClick, ...props }, ref) => {
|
266 |
+
const { toggleSidebar } = useSidebar()
|
267 |
+
|
268 |
+
return (
|
269 |
+
<Button
|
270 |
+
ref={ref}
|
271 |
+
data-sidebar="trigger"
|
272 |
+
variant="ghost"
|
273 |
+
size="icon"
|
274 |
+
className={cn("h-7 w-7", className)}
|
275 |
+
onClick={(event) => {
|
276 |
+
onClick?.(event)
|
277 |
+
toggleSidebar()
|
278 |
+
}}
|
279 |
+
{...props}
|
280 |
+
>
|
281 |
+
<PanelLeft />
|
282 |
+
<span className="sr-only">Toggle Sidebar</span>
|
283 |
+
</Button>
|
284 |
+
)
|
285 |
+
})
|
286 |
+
SidebarTrigger.displayName = "SidebarTrigger"
|
287 |
+
|
288 |
+
const SidebarRail = React.forwardRef<
|
289 |
+
HTMLButtonElement,
|
290 |
+
React.ComponentProps<"button">
|
291 |
+
>(({ className, ...props }, ref) => {
|
292 |
+
const { toggleSidebar } = useSidebar()
|
293 |
+
|
294 |
+
return (
|
295 |
+
<button
|
296 |
+
ref={ref}
|
297 |
+
data-sidebar="rail"
|
298 |
+
aria-label="Toggle Sidebar"
|
299 |
+
tabIndex={-1}
|
300 |
+
onClick={toggleSidebar}
|
301 |
+
title="Toggle Sidebar"
|
302 |
+
className={cn(
|
303 |
+
"absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] hover:after:bg-sidebar-border group-data-[side=left]:-right-4 group-data-[side=right]:left-0 sm:flex",
|
304 |
+
"[[data-side=left]_&]:cursor-w-resize [[data-side=right]_&]:cursor-e-resize",
|
305 |
+
"[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize",
|
306 |
+
"group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full group-data-[collapsible=offcanvas]:hover:bg-sidebar",
|
307 |
+
"[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
|
308 |
+
"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
|
309 |
+
className
|
310 |
+
)}
|
311 |
+
{...props}
|
312 |
+
/>
|
313 |
+
)
|
314 |
+
})
|
315 |
+
SidebarRail.displayName = "SidebarRail"
|
316 |
+
|
317 |
+
const SidebarInset = React.forwardRef<
|
318 |
+
HTMLDivElement,
|
319 |
+
React.ComponentProps<"main">
|
320 |
+
>(({ className, ...props }, ref) => {
|
321 |
+
return (
|
322 |
+
<main
|
323 |
+
ref={ref}
|
324 |
+
className={cn(
|
325 |
+
"relative flex min-h-svh flex-1 flex-col bg-background",
|
326 |
+
"peer-data-[variant=inset]:min-h-[calc(100svh-theme(spacing.4))] md:peer-data-[variant=inset]:m-2 md:peer-data-[state=collapsed]:peer-data-[variant=inset]:ml-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow",
|
327 |
+
className
|
328 |
+
)}
|
329 |
+
{...props}
|
330 |
+
/>
|
331 |
+
)
|
332 |
+
})
|
333 |
+
SidebarInset.displayName = "SidebarInset"
|
334 |
+
|
335 |
+
const SidebarInput = React.forwardRef<
|
336 |
+
React.ElementRef<typeof Input>,
|
337 |
+
React.ComponentProps<typeof Input>
|
338 |
+
>(({ className, ...props }, ref) => {
|
339 |
+
return (
|
340 |
+
<Input
|
341 |
+
ref={ref}
|
342 |
+
data-sidebar="input"
|
343 |
+
className={cn(
|
344 |
+
"h-8 w-full bg-background shadow-none focus-visible:ring-2 focus-visible:ring-sidebar-ring",
|
345 |
+
className
|
346 |
+
)}
|
347 |
+
{...props}
|
348 |
+
/>
|
349 |
+
)
|
350 |
+
})
|
351 |
+
SidebarInput.displayName = "SidebarInput"
|
352 |
+
|
353 |
+
const SidebarHeader = React.forwardRef<
|
354 |
+
HTMLDivElement,
|
355 |
+
React.ComponentProps<"div">
|
356 |
+
>(({ className, ...props }, ref) => {
|
357 |
+
return (
|
358 |
+
<div
|
359 |
+
ref={ref}
|
360 |
+
data-sidebar="header"
|
361 |
+
className={cn("flex flex-col gap-2 p-2", className)}
|
362 |
+
{...props}
|
363 |
+
/>
|
364 |
+
)
|
365 |
+
})
|
366 |
+
SidebarHeader.displayName = "SidebarHeader"
|
367 |
+
|
368 |
+
const SidebarFooter = React.forwardRef<
|
369 |
+
HTMLDivElement,
|
370 |
+
React.ComponentProps<"div">
|
371 |
+
>(({ className, ...props }, ref) => {
|
372 |
+
return (
|
373 |
+
<div
|
374 |
+
ref={ref}
|
375 |
+
data-sidebar="footer"
|
376 |
+
className={cn("flex flex-col gap-2 p-2", className)}
|
377 |
+
{...props}
|
378 |
+
/>
|
379 |
+
)
|
380 |
+
})
|
381 |
+
SidebarFooter.displayName = "SidebarFooter"
|
382 |
+
|
383 |
+
const SidebarSeparator = React.forwardRef<
|
384 |
+
React.ElementRef<typeof Separator>,
|
385 |
+
React.ComponentProps<typeof Separator>
|
386 |
+
>(({ className, ...props }, ref) => {
|
387 |
+
return (
|
388 |
+
<Separator
|
389 |
+
ref={ref}
|
390 |
+
data-sidebar="separator"
|
391 |
+
className={cn("mx-2 w-auto bg-sidebar-border", className)}
|
392 |
+
{...props}
|
393 |
+
/>
|
394 |
+
)
|
395 |
+
})
|
396 |
+
SidebarSeparator.displayName = "SidebarSeparator"
|
397 |
+
|
398 |
+
const SidebarContent = React.forwardRef<
|
399 |
+
HTMLDivElement,
|
400 |
+
React.ComponentProps<"div">
|
401 |
+
>(({ className, ...props }, ref) => {
|
402 |
+
return (
|
403 |
+
<div
|
404 |
+
ref={ref}
|
405 |
+
data-sidebar="content"
|
406 |
+
className={cn(
|
407 |
+
"flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
|
408 |
+
className
|
409 |
+
)}
|
410 |
+
{...props}
|
411 |
+
/>
|
412 |
+
)
|
413 |
+
})
|
414 |
+
SidebarContent.displayName = "SidebarContent"
|
415 |
+
|
416 |
+
const SidebarGroup = React.forwardRef<
|
417 |
+
HTMLDivElement,
|
418 |
+
React.ComponentProps<"div">
|
419 |
+
>(({ className, ...props }, ref) => {
|
420 |
+
return (
|
421 |
+
<div
|
422 |
+
ref={ref}
|
423 |
+
data-sidebar="group"
|
424 |
+
className={cn("relative flex w-full min-w-0 flex-col p-2", className)}
|
425 |
+
{...props}
|
426 |
+
/>
|
427 |
+
)
|
428 |
+
})
|
429 |
+
SidebarGroup.displayName = "SidebarGroup"
|
430 |
+
|
431 |
+
const SidebarGroupLabel = React.forwardRef<
|
432 |
+
HTMLDivElement,
|
433 |
+
React.ComponentProps<"div"> & { asChild?: boolean }
|
434 |
+
>(({ className, asChild = false, ...props }, ref) => {
|
435 |
+
const Comp = asChild ? Slot : "div"
|
436 |
+
|
437 |
+
return (
|
438 |
+
<Comp
|
439 |
+
ref={ref}
|
440 |
+
data-sidebar="group-label"
|
441 |
+
className={cn(
|
442 |
+
"duration-200 flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium text-sidebar-foreground/70 outline-none ring-sidebar-ring transition-[margin,opa] ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
443 |
+
"group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
|
444 |
+
className
|
445 |
+
)}
|
446 |
+
{...props}
|
447 |
+
/>
|
448 |
+
)
|
449 |
+
})
|
450 |
+
SidebarGroupLabel.displayName = "SidebarGroupLabel"
|
451 |
+
|
452 |
+
const SidebarGroupAction = React.forwardRef<
|
453 |
+
HTMLButtonElement,
|
454 |
+
React.ComponentProps<"button"> & { asChild?: boolean }
|
455 |
+
>(({ className, asChild = false, ...props }, ref) => {
|
456 |
+
const Comp = asChild ? Slot : "button"
|
457 |
+
|
458 |
+
return (
|
459 |
+
<Comp
|
460 |
+
ref={ref}
|
461 |
+
data-sidebar="group-action"
|
462 |
+
className={cn(
|
463 |
+
"absolute right-3 top-3.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
464 |
+
// Increases the hit area of the button on mobile.
|
465 |
+
"after:absolute after:-inset-2 after:md:hidden",
|
466 |
+
"group-data-[collapsible=icon]:hidden",
|
467 |
+
className
|
468 |
+
)}
|
469 |
+
{...props}
|
470 |
+
/>
|
471 |
+
)
|
472 |
+
})
|
473 |
+
SidebarGroupAction.displayName = "SidebarGroupAction"
|
474 |
+
|
475 |
+
const SidebarGroupContent = React.forwardRef<
|
476 |
+
HTMLDivElement,
|
477 |
+
React.ComponentProps<"div">
|
478 |
+
>(({ className, ...props }, ref) => (
|
479 |
+
<div
|
480 |
+
ref={ref}
|
481 |
+
data-sidebar="group-content"
|
482 |
+
className={cn("w-full text-sm", className)}
|
483 |
+
{...props}
|
484 |
+
/>
|
485 |
+
))
|
486 |
+
SidebarGroupContent.displayName = "SidebarGroupContent"
|
487 |
+
|
488 |
+
const SidebarMenu = React.forwardRef<
|
489 |
+
HTMLUListElement,
|
490 |
+
React.ComponentProps<"ul">
|
491 |
+
>(({ className, ...props }, ref) => (
|
492 |
+
<ul
|
493 |
+
ref={ref}
|
494 |
+
data-sidebar="menu"
|
495 |
+
className={cn("flex w-full min-w-0 flex-col gap-1", className)}
|
496 |
+
{...props}
|
497 |
+
/>
|
498 |
+
))
|
499 |
+
SidebarMenu.displayName = "SidebarMenu"
|
500 |
+
|
501 |
+
const SidebarMenuItem = React.forwardRef<
|
502 |
+
HTMLLIElement,
|
503 |
+
React.ComponentProps<"li">
|
504 |
+
>(({ className, ...props }, ref) => (
|
505 |
+
<li
|
506 |
+
ref={ref}
|
507 |
+
data-sidebar="menu-item"
|
508 |
+
className={cn("group/menu-item relative", className)}
|
509 |
+
{...props}
|
510 |
+
/>
|
511 |
+
))
|
512 |
+
SidebarMenuItem.displayName = "SidebarMenuItem"
|
513 |
+
|
514 |
+
const sidebarMenuButtonVariants = cva(
|
515 |
+
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
|
516 |
+
{
|
517 |
+
variants: {
|
518 |
+
variant: {
|
519 |
+
default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
|
520 |
+
outline:
|
521 |
+
"bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
|
522 |
+
},
|
523 |
+
size: {
|
524 |
+
default: "h-8 text-sm",
|
525 |
+
sm: "h-7 text-xs",
|
526 |
+
lg: "h-12 text-sm group-data-[collapsible=icon]:!p-0",
|
527 |
+
},
|
528 |
+
},
|
529 |
+
defaultVariants: {
|
530 |
+
variant: "default",
|
531 |
+
size: "default",
|
532 |
+
},
|
533 |
+
}
|
534 |
+
)
|
535 |
+
|
536 |
+
const SidebarMenuButton = React.forwardRef<
|
537 |
+
HTMLButtonElement,
|
538 |
+
React.ComponentProps<"button"> & {
|
539 |
+
asChild?: boolean
|
540 |
+
isActive?: boolean
|
541 |
+
tooltip?: string | React.ComponentProps<typeof TooltipContent>
|
542 |
+
} & VariantProps<typeof sidebarMenuButtonVariants>
|
543 |
+
>(
|
544 |
+
(
|
545 |
+
{
|
546 |
+
asChild = false,
|
547 |
+
isActive = false,
|
548 |
+
variant = "default",
|
549 |
+
size = "default",
|
550 |
+
tooltip,
|
551 |
+
className,
|
552 |
+
...props
|
553 |
+
},
|
554 |
+
ref
|
555 |
+
) => {
|
556 |
+
const Comp = asChild ? Slot : "button"
|
557 |
+
const { isMobile, state } = useSidebar()
|
558 |
+
|
559 |
+
const button = (
|
560 |
+
<Comp
|
561 |
+
ref={ref}
|
562 |
+
data-sidebar="menu-button"
|
563 |
+
data-size={size}
|
564 |
+
data-active={isActive}
|
565 |
+
className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
|
566 |
+
{...props}
|
567 |
+
/>
|
568 |
+
)
|
569 |
+
|
570 |
+
if (!tooltip) {
|
571 |
+
return button
|
572 |
+
}
|
573 |
+
|
574 |
+
if (typeof tooltip === "string") {
|
575 |
+
tooltip = {
|
576 |
+
children: tooltip,
|
577 |
+
}
|
578 |
+
}
|
579 |
+
|
580 |
+
return (
|
581 |
+
<Tooltip>
|
582 |
+
<TooltipTrigger asChild>{button}</TooltipTrigger>
|
583 |
+
<TooltipContent
|
584 |
+
side="right"
|
585 |
+
align="center"
|
586 |
+
hidden={state !== "collapsed" || isMobile}
|
587 |
+
{...tooltip}
|
588 |
+
/>
|
589 |
+
</Tooltip>
|
590 |
+
)
|
591 |
+
}
|
592 |
+
)
|
593 |
+
SidebarMenuButton.displayName = "SidebarMenuButton"
|
594 |
+
|
595 |
+
const SidebarMenuAction = React.forwardRef<
|
596 |
+
HTMLButtonElement,
|
597 |
+
React.ComponentProps<"button"> & {
|
598 |
+
asChild?: boolean
|
599 |
+
showOnHover?: boolean
|
600 |
+
}
|
601 |
+
>(({ className, asChild = false, showOnHover = false, ...props }, ref) => {
|
602 |
+
const Comp = asChild ? Slot : "button"
|
603 |
+
|
604 |
+
return (
|
605 |
+
<Comp
|
606 |
+
ref={ref}
|
607 |
+
data-sidebar="menu-action"
|
608 |
+
className={cn(
|
609 |
+
"absolute right-1 top-1.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 peer-hover/menu-button:text-sidebar-accent-foreground [&>svg]:size-4 [&>svg]:shrink-0",
|
610 |
+
// Increases the hit area of the button on mobile.
|
611 |
+
"after:absolute after:-inset-2 after:md:hidden",
|
612 |
+
"peer-data-[size=sm]/menu-button:top-1",
|
613 |
+
"peer-data-[size=default]/menu-button:top-1.5",
|
614 |
+
"peer-data-[size=lg]/menu-button:top-2.5",
|
615 |
+
"group-data-[collapsible=icon]:hidden",
|
616 |
+
showOnHover &&
|
617 |
+
"group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground md:opacity-0",
|
618 |
+
className
|
619 |
+
)}
|
620 |
+
{...props}
|
621 |
+
/>
|
622 |
+
)
|
623 |
+
})
|
624 |
+
SidebarMenuAction.displayName = "SidebarMenuAction"
|
625 |
+
|
626 |
+
const SidebarMenuBadge = React.forwardRef<
|
627 |
+
HTMLDivElement,
|
628 |
+
React.ComponentProps<"div">
|
629 |
+
>(({ className, ...props }, ref) => (
|
630 |
+
<div
|
631 |
+
ref={ref}
|
632 |
+
data-sidebar="menu-badge"
|
633 |
+
className={cn(
|
634 |
+
"absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums text-sidebar-foreground select-none pointer-events-none",
|
635 |
+
"peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground",
|
636 |
+
"peer-data-[size=sm]/menu-button:top-1",
|
637 |
+
"peer-data-[size=default]/menu-button:top-1.5",
|
638 |
+
"peer-data-[size=lg]/menu-button:top-2.5",
|
639 |
+
"group-data-[collapsible=icon]:hidden",
|
640 |
+
className
|
641 |
+
)}
|
642 |
+
{...props}
|
643 |
+
/>
|
644 |
+
))
|
645 |
+
SidebarMenuBadge.displayName = "SidebarMenuBadge"
|
646 |
+
|
647 |
+
const SidebarMenuSkeleton = React.forwardRef<
|
648 |
+
HTMLDivElement,
|
649 |
+
React.ComponentProps<"div"> & {
|
650 |
+
showIcon?: boolean
|
651 |
+
}
|
652 |
+
>(({ className, showIcon = false, ...props }, ref) => {
|
653 |
+
// Random width between 50 to 90%.
|
654 |
+
const width = React.useMemo(() => {
|
655 |
+
return `${Math.floor(Math.random() * 40) + 50}%`
|
656 |
+
}, [])
|
657 |
+
|
658 |
+
return (
|
659 |
+
<div
|
660 |
+
ref={ref}
|
661 |
+
data-sidebar="menu-skeleton"
|
662 |
+
className={cn("rounded-md h-8 flex gap-2 px-2 items-center", className)}
|
663 |
+
{...props}
|
664 |
+
>
|
665 |
+
{showIcon && (
|
666 |
+
<Skeleton
|
667 |
+
className="size-4 rounded-md"
|
668 |
+
data-sidebar="menu-skeleton-icon"
|
669 |
+
/>
|
670 |
+
)}
|
671 |
+
<Skeleton
|
672 |
+
className="h-4 flex-1 max-w-[--skeleton-width]"
|
673 |
+
data-sidebar="menu-skeleton-text"
|
674 |
+
style={
|
675 |
+
{
|
676 |
+
"--skeleton-width": width,
|
677 |
+
} as React.CSSProperties
|
678 |
+
}
|
679 |
+
/>
|
680 |
+
</div>
|
681 |
+
)
|
682 |
+
})
|
683 |
+
SidebarMenuSkeleton.displayName = "SidebarMenuSkeleton"
|
684 |
+
|
685 |
+
const SidebarMenuSub = React.forwardRef<
|
686 |
+
HTMLUListElement,
|
687 |
+
React.ComponentProps<"ul">
|
688 |
+
>(({ className, ...props }, ref) => (
|
689 |
+
<ul
|
690 |
+
ref={ref}
|
691 |
+
data-sidebar="menu-sub"
|
692 |
+
className={cn(
|
693 |
+
"mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l border-sidebar-border px-2.5 py-0.5",
|
694 |
+
"group-data-[collapsible=icon]:hidden",
|
695 |
+
className
|
696 |
+
)}
|
697 |
+
{...props}
|
698 |
+
/>
|
699 |
+
))
|
700 |
+
SidebarMenuSub.displayName = "SidebarMenuSub"
|
701 |
+
|
702 |
+
const SidebarMenuSubItem = React.forwardRef<
|
703 |
+
HTMLLIElement,
|
704 |
+
React.ComponentProps<"li">
|
705 |
+
>(({ ...props }, ref) => <li ref={ref} {...props} />)
|
706 |
+
SidebarMenuSubItem.displayName = "SidebarMenuSubItem"
|
707 |
+
|
708 |
+
const SidebarMenuSubButton = React.forwardRef<
|
709 |
+
HTMLAnchorElement,
|
710 |
+
React.ComponentProps<"a"> & {
|
711 |
+
asChild?: boolean
|
712 |
+
size?: "sm" | "md"
|
713 |
+
isActive?: boolean
|
714 |
+
}
|
715 |
+
>(({ asChild = false, size = "md", isActive, className, ...props }, ref) => {
|
716 |
+
const Comp = asChild ? Slot : "a"
|
717 |
+
|
718 |
+
return (
|
719 |
+
<Comp
|
720 |
+
ref={ref}
|
721 |
+
data-sidebar="menu-sub-button"
|
722 |
+
data-size={size}
|
723 |
+
data-active={isActive}
|
724 |
+
className={cn(
|
725 |
+
"flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground outline-none ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground",
|
726 |
+
"data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
|
727 |
+
size === "sm" && "text-xs",
|
728 |
+
size === "md" && "text-sm",
|
729 |
+
"group-data-[collapsible=icon]:hidden",
|
730 |
+
className
|
731 |
+
)}
|
732 |
+
{...props}
|
733 |
+
/>
|
734 |
+
)
|
735 |
+
})
|
736 |
+
SidebarMenuSubButton.displayName = "SidebarMenuSubButton"
|
737 |
+
|
738 |
+
export {
|
739 |
+
Sidebar,
|
740 |
+
SidebarContent,
|
741 |
+
SidebarFooter,
|
742 |
+
SidebarGroup,
|
743 |
+
SidebarGroupAction,
|
744 |
+
SidebarGroupContent,
|
745 |
+
SidebarGroupLabel,
|
746 |
+
SidebarHeader,
|
747 |
+
SidebarInput,
|
748 |
+
SidebarInset,
|
749 |
+
SidebarMenu,
|
750 |
+
SidebarMenuAction,
|
751 |
+
SidebarMenuBadge,
|
752 |
+
SidebarMenuButton,
|
753 |
+
SidebarMenuItem,
|
754 |
+
SidebarMenuSkeleton,
|
755 |
+
SidebarMenuSub,
|
756 |
+
SidebarMenuSubButton,
|
757 |
+
SidebarMenuSubItem,
|
758 |
+
SidebarProvider,
|
759 |
+
SidebarRail,
|
760 |
+
SidebarSeparator,
|
761 |
+
SidebarTrigger,
|
762 |
+
useSidebar,
|
763 |
+
}
|