[dyad] commited on
Commit
a27839e
·
0 Parent(s):

Init Dyad app

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .env +4 -0
  2. .env.example +3 -0
  3. .gitattributes +35 -0
  4. .gitignore +24 -0
  5. AI_RULES.md +19 -0
  6. Dockerfile +23 -0
  7. LICENSE +38 -0
  8. README.md +56 -0
  9. components.json +20 -0
  10. eslint.config.js +29 -0
  11. index.html +17 -0
  12. package-lock.json +0 -0
  13. package.json +97 -0
  14. pnpm-lock.yaml +0 -0
  15. postcss.config.js +6 -0
  16. public/front-pilot.svg +1 -0
  17. src/App.css +42 -0
  18. src/App.tsx +37 -0
  19. src/components/AnimatedBackground.tsx +51 -0
  20. src/components/ChatInterface.tsx +197 -0
  21. src/components/CodeEditor.tsx +81 -0
  22. src/components/DiffReviewModal.tsx +131 -0
  23. src/components/PreviewPanel.tsx +81 -0
  24. src/components/ProjectHistory.tsx +117 -0
  25. src/components/VersionsPanel.tsx +115 -0
  26. src/components/made-with-dyad.tsx +9 -0
  27. src/components/theme-provider.tsx +9 -0
  28. src/components/theme-toggle.tsx +40 -0
  29. src/components/ui/accordion.tsx +56 -0
  30. src/components/ui/alert-dialog.tsx +139 -0
  31. src/components/ui/alert.tsx +59 -0
  32. src/components/ui/aspect-ratio.tsx +5 -0
  33. src/components/ui/avatar.tsx +48 -0
  34. src/components/ui/badge.tsx +36 -0
  35. src/components/ui/breadcrumb.tsx +115 -0
  36. src/components/ui/button.tsx +56 -0
  37. src/components/ui/calendar.tsx +64 -0
  38. src/components/ui/card.tsx +86 -0
  39. src/components/ui/carousel.tsx +260 -0
  40. src/components/ui/chart.tsx +363 -0
  41. src/components/ui/checkbox.tsx +28 -0
  42. src/components/ui/collapsible.tsx +9 -0
  43. src/components/ui/command.tsx +153 -0
  44. src/components/ui/context-menu.tsx +198 -0
  45. src/components/ui/dialog.tsx +120 -0
  46. src/components/ui/drawer.tsx +116 -0
  47. src/components/ui/dropdown-menu.tsx +198 -0
  48. src/components/ui/form.tsx +177 -0
  49. src/components/ui/hover-card.tsx +27 -0
  50. src/components/ui/input-otp.tsx +69 -0
.env ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ VITE_GEMINI_API_KEY=AIzaSyD65JljqhCIH6ymhpjn-kvmsijXCvG35Fw
2
+ VITE_SUPABASE_URL=https://zxqgedgzxobmezdltffh.supabase.co
3
+
4
+ VITE_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inp4cWdlZGd6eG9ibWV6ZGx0ZmZoIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTUwODE2NzUsImV4cCI6MjA3MDY1NzY3NX0.2hnWRNXgxvQzAl9BE3MKbsffg4YatGupWuYAMwIxDQM
.env.example ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ VITE_OPENROUTER_API_KEY=your_openrouter_api_key_here
2
+ VITE_SUPABASE_URL=your_supabase_project_url_here
3
+ VITE_SUPABASE_ANON_KEY=your_supabase_anon_key_here
.gitattributes ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Logs
2
+ logs
3
+ *.log
4
+ npm-debug.log*
5
+ yarn-debug.log*
6
+ yarn-error.log*
7
+ pnpm-debug.log*
8
+ lerna-debug.log*
9
+
10
+ node_modules
11
+ dist
12
+ dist-ssr
13
+ *.local
14
+
15
+ # Editor directories and files
16
+ .vscode/*
17
+ !.vscode/extensions.json
18
+ .idea
19
+ .DS_Store
20
+ *.suo
21
+ *.ntvs*
22
+ *.njsproj
23
+ *.sln
24
+ *.sw?
AI_RULES.md ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Tech Stack
2
+
3
+ - You are building a React application.
4
+ - Use TypeScript.
5
+ - Use React Router. KEEP the routes in src/App.tsx
6
+ - Always put source code in the src folder.
7
+ - Put pages into src/pages/
8
+ - Put components into src/components/
9
+ - The main page (default page) is src/pages/Index.tsx
10
+ - UPDATE the main page to include the new components. OTHERWISE, the user can NOT see any components!
11
+ - ALWAYS try to use the shadcn/ui library.
12
+ - Tailwind CSS: always use Tailwind CSS for styling components. Utilize Tailwind classes extensively for layout, spacing, colors, and other design aspects.
13
+
14
+ Available packages and libraries:
15
+
16
+ - The lucide-react package is installed for icons.
17
+ - You ALREADY have ALL the shadcn/ui components and their dependencies installed. So you don't need to install them again.
18
+ - You have ALL the necessary Radix UI components installed.
19
+ - Use prebuilt components from the shadcn/ui library after importing them. Note that these files shouldn't be edited, so make new components if you need to change them.
Dockerfile ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM node:22-alpine
2
+ USER root
3
+
4
+ USER 1000
5
+ WORKDIR /usr/src/app
6
+
7
+ # Copy package files
8
+ COPY --chown=1000 package.json package-lock.json ./
9
+
10
+ # Install all dependencies including dev dependencies for building and running
11
+ RUN npm install --legacy-peer-deps
12
+
13
+ # Copy application files
14
+ COPY --chown=1000 . .
15
+
16
+ # Build the application
17
+ RUN npm run build
18
+
19
+ # Expose the port Hugging Face Spaces expects
20
+ EXPOSE 7860
21
+
22
+ # Start the application on the correct port for Hugging Face
23
+ CMD ["npm", "run", "preview", "--", "--host", "0.0.0.0", "--port", "7860"]
LICENSE ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Proprietary License
2
+
3
+ Copyright (c) 2025 Halopai Inc. All Rights Reserved.
4
+
5
+ This software and associated documentation files (the "Software") are the proprietary property of Halopai Inc. and are protected by copyright law and international treaties.
6
+
7
+ ## Restrictions
8
+
9
+ You may not:
10
+ - Copy, modify, or create derivative works of the Software
11
+ - Distribute, sublicense, lease, rent, or otherwise transfer the Software
12
+ - Reverse engineer, decompile, or disassemble the Software
13
+ - Remove or alter any proprietary notices or labels on the Software
14
+ - Use the Software for any purpose except as expressly authorized
15
+
16
+ ## Ownership
17
+
18
+ All rights, title, and interest in and to the Software, including all intellectual property rights, remain with Halopai Inc. This license does not grant you any ownership rights in the Software.
19
+
20
+ ## Limited Use
21
+
22
+ Halopai Inc. grants you a limited, non-exclusive, non-transferable license to use the Software solely for your internal business purposes, subject to the terms of this license.
23
+
24
+ ## Termination
25
+
26
+ This license is effective until terminated. Halopai Inc. may terminate this license at any time without notice if you fail to comply with any term of this license.
27
+
28
+ ## Disclaimer
29
+
30
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
31
+
32
+ ## Governing Law
33
+
34
+ This license shall be governed by and construed in accordance with the laws of the jurisdiction where Halopai Inc. is located.
35
+
36
+ ---
37
+
38
+ For permission to use, modify, or distribute this software, please contact Halopai Inc. at legal@halopai.com
README.md ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: FrontPilot AI Website Builder
3
+ emoji: 🚀
4
+ colorFrom: blue
5
+ colorTo: purple
6
+ sdk: docker
7
+ sdk_version: "20.04"
8
+ pinned: false
9
+ ---
10
+ # FrontPilot
11
+
12
+ **AI-Powered Rapid Prototyping for Modern Web Development**
13
+
14
+ [![License](https://img.shields.io/badge/license-Proprietary-blue.svg)](LICENSE)
15
+
16
+ <p align="center">
17
+ <img src="public/front-pilot.svg" alt="FrontPilot Logo" width="120" />
18
+ </p>
19
+
20
+ FrontPilot is an innovative AI-powered prototyping tool that enables users to create stunning websites in seconds without writing code. Simply describe what you want, and our advanced AI generates a fully functional prototype for you.
21
+
22
+ ## Purpose
23
+
24
+ FrontPilot was created to revolutionize the web development process by eliminating the barriers between ideas and implementation. Our mission is to empower creators, designers, and entrepreneurs to bring their web visions to life instantly, without requiring technical coding skills.
25
+
26
+ ## What It Can Do
27
+
28
+ - **Natural Language to Website Conversion** - Transform plain English descriptions into fully functional websites
29
+ - **Instant Prototype Generation** - Create working website prototypes in seconds, not days or weeks
30
+ - **AI-Powered Design Intelligence** - Leverage advanced AI to generate modern, responsive designs
31
+ - **Real-time Preview & Editing** - See changes instantly as you refine your prototype
32
+ - **Version History Management** - Track, compare, and restore previous versions of your projects
33
+ - **One-Click Export** - Download your prototypes as production-ready HTML files
34
+ - **Cross-Device Compatibility** - Automatically generate responsive designs that work on all devices
35
+ - **Smart Component Generation** - Create complex UI elements like forms, galleries, and dashboards through simple descriptions
36
+
37
+ ## Target Use Cases
38
+
39
+ - Rapid prototyping for design agencies
40
+ - Quick landing page creation for marketers
41
+ - Instant website mockups for entrepreneurs
42
+ - UI/UX concept validation for product teams
43
+ - Educational tool for learning web design principles
44
+ - Content creators needing quick web solutions
45
+
46
+ ## Security & Privacy
47
+
48
+ FrontPilot prioritizes user data security with enterprise-grade encryption and compliance with modern privacy standards. All prototypes are securely stored and only accessible to authorized users.
49
+
50
+
51
+
52
+ <p align="center">
53
+ <strong>Made with ❤️ by Halopai Inc.</strong>
54
+ <br/>
55
+ <em>AI-powered rapid prototyping for modern web development</em>
56
+ </p>
components.json ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema.json",
3
+ "style": "default",
4
+ "rsc": false,
5
+ "tsx": true,
6
+ "tailwind": {
7
+ "config": "tailwind.config.ts",
8
+ "css": "src/index.css",
9
+ "baseColor": "slate",
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
+ }
eslint.config.js ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import js from "@eslint/js";
2
+ import globals from "globals";
3
+ import reactHooks from "eslint-plugin-react-hooks";
4
+ import reactRefresh from "eslint-plugin-react-refresh";
5
+ import tseslint from "typescript-eslint";
6
+
7
+ export default tseslint.config(
8
+ { ignores: ["dist"] },
9
+ {
10
+ extends: [js.configs.recommended, ...tseslint.configs.recommended],
11
+ files: ["**/*.{ts,tsx}"],
12
+ languageOptions: {
13
+ ecmaVersion: 2020,
14
+ globals: globals.browser,
15
+ },
16
+ plugins: {
17
+ "react-hooks": reactHooks,
18
+ "react-refresh": reactRefresh,
19
+ },
20
+ rules: {
21
+ ...reactHooks.configs.recommended.rules,
22
+ "react-refresh/only-export-components": [
23
+ "warn",
24
+ { allowConstantExport: true },
25
+ ],
26
+ "@typescript-eslint/no-unused-vars": "off",
27
+ },
28
+ },
29
+ );
index.html ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <meta name="description" content="FrontPilot - AI-Powered Rapid Prototyping for Modern Web Development. Create stunning websites in seconds without writing code." />
7
+ <meta name="keywords" content="AI, web development, prototyping, website builder, no-code, FrontPilot" />
8
+ <meta name="author" content="Halopai Inc." />
9
+ <title>FrontPilot - AI Prototype Builder</title>
10
+ <link rel="icon" type="image/svg+xml" href="/front-pilot.svg" />
11
+ </head>
12
+
13
+ <body>
14
+ <div id="root"></div>
15
+ <script type="module" src="/src/main.tsx"></script>
16
+ </body>
17
+ </html>
package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
package.json ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "vite_react_shadcn_ts",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "build:dev": "vite build --mode development",
10
+ "lint": "eslint .",
11
+ "preview": "vite preview"
12
+ },
13
+ "dependencies": {
14
+ "@google/generative-ai": "^0.24.1",
15
+ "@hookform/resolvers": "^3.9.0",
16
+ "@monaco-editor/react": "^4.7.0",
17
+ "@radix-ui/react-accordion": "^1.2.0",
18
+ "@radix-ui/react-alert-dialog": "^1.1.1",
19
+ "@radix-ui/react-aspect-ratio": "^1.1.0",
20
+ "@radix-ui/react-avatar": "^1.1.0",
21
+ "@radix-ui/react-checkbox": "^1.1.1",
22
+ "@radix-ui/react-collapsible": "^1.1.0",
23
+ "@radix-ui/react-context-menu": "^2.2.1",
24
+ "@radix-ui/react-dialog": "^1.1.2",
25
+ "@radix-ui/react-dropdown-menu": "^2.1.1",
26
+ "@radix-ui/react-hover-card": "^1.1.1",
27
+ "@radix-ui/react-label": "^2.1.0",
28
+ "@radix-ui/react-menubar": "^1.1.1",
29
+ "@radix-ui/react-navigation-menu": "^1.2.0",
30
+ "@radix-ui/react-popover": "^1.1.1",
31
+ "@radix-ui/react-progress": "^1.1.0",
32
+ "@radix-ui/react-radio-group": "^1.2.0",
33
+ "@radix-ui/react-scroll-area": "^1.1.0",
34
+ "@radix-ui/react-select": "^2.1.1",
35
+ "@radix-ui/react-separator": "^1.1.0",
36
+ "@radix-ui/react-slider": "^1.2.0",
37
+ "@radix-ui/react-slot": "^1.1.0",
38
+ "@radix-ui/react-switch": "^1.1.0",
39
+ "@radix-ui/react-tabs": "^1.1.12",
40
+ "@radix-ui/react-toast": "^1.2.14",
41
+ "@radix-ui/react-toggle": "^1.1.0",
42
+ "@radix-ui/react-toggle-group": "^1.1.0",
43
+ "@radix-ui/react-tooltip": "^1.1.4",
44
+ "@supabase/supabase-js": "^2.55.0",
45
+ "@tanstack/react-query": "^5.56.2",
46
+ "class-variance-authority": "^0.7.1",
47
+ "clsx": "^2.1.1",
48
+ "cmdk": "^1.0.0",
49
+ "date-fns": "^3.6.0",
50
+ "diff": "^8.0.2",
51
+ "embla-carousel-react": "^8.3.0",
52
+ "file-saver": "^2.0.5",
53
+ "framer-motion": "^12.23.12",
54
+ "input-otp": "^1.2.4",
55
+ "js-beautify": "^1.15.4",
56
+ "jszip": "^3.10.1",
57
+ "lucide-react": "^0.462.0",
58
+ "next-themes": "^0.3.0",
59
+ "prismjs": "^1.30.0",
60
+ "react": "^18.3.1",
61
+ "react-day-picker": "^8.10.1",
62
+ "react-dom": "^18.3.1",
63
+ "react-hook-form": "^7.53.0",
64
+ "react-resizable-panels": "^2.1.3",
65
+ "react-router-dom": "^6.26.2",
66
+ "react-simple-code-editor": "^0.14.1",
67
+ "react-tsparticles": "^2.12.2",
68
+ "recharts": "^2.12.7",
69
+ "sonner": "^1.7.4",
70
+ "tailwind-merge": "^2.5.2",
71
+ "tailwindcss-animate": "^1.0.7",
72
+ "tsparticles": "^3.9.1",
73
+ "tsparticles-engine": "^2.12.0",
74
+ "tsparticles-preset-stars": "^2.12.0",
75
+ "vaul": "^0.9.3",
76
+ "zod": "^3.23.8"
77
+ },
78
+ "devDependencies": {
79
+ "@dyad-sh/react-vite-component-tagger": "^0.8.0",
80
+ "@eslint/js": "^9.9.0",
81
+ "@tailwindcss/typography": "^0.5.15",
82
+ "@types/node": "^22.5.5",
83
+ "@types/react": "^18.3.3",
84
+ "@types/react-dom": "^18.3.0",
85
+ "@vitejs/plugin-react-swc": "^3.9.0",
86
+ "autoprefixer": "^10.4.20",
87
+ "eslint": "^9.9.0",
88
+ "eslint-plugin-react-hooks": "^5.1.0-rc.0",
89
+ "eslint-plugin-react-refresh": "^0.4.9",
90
+ "globals": "^15.9.0",
91
+ "postcss": "^8.4.47",
92
+ "tailwindcss": "^3.4.11",
93
+ "typescript": "^5.5.3",
94
+ "typescript-eslint": "^8.0.1",
95
+ "vite": "^6.3.4"
96
+ }
97
+ }
pnpm-lock.yaml ADDED
The diff for this file is too large to render. See raw diff
 
postcss.config.js ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ export default {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {},
5
+ },
6
+ };
public/front-pilot.svg ADDED
src/App.css ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #root {
2
+ max-width: 1280px;
3
+ margin: 0 auto;
4
+ padding: 2rem;
5
+ text-align: center;
6
+ }
7
+
8
+ .logo {
9
+ height: 6em;
10
+ padding: 1.5em;
11
+ will-change: filter;
12
+ transition: filter 300ms;
13
+ }
14
+ .logo:hover {
15
+ filter: drop-shadow(0 0 2em #646cffaa);
16
+ }
17
+ .logo.react:hover {
18
+ filter: drop-shadow(0 0 2em #61dafbaa);
19
+ }
20
+
21
+ @keyframes logo-spin {
22
+ from {
23
+ transform: rotate(0deg);
24
+ }
25
+ to {
26
+ transform: rotate(360deg);
27
+ }
28
+ }
29
+
30
+ @media (prefers-reduced-motion: no-preference) {
31
+ a:nth-of-type(2) .logo {
32
+ animation: logo-spin infinite 20s linear;
33
+ }
34
+ }
35
+
36
+ .card {
37
+ padding: 2em;
38
+ }
39
+
40
+ .read-the-docs {
41
+ color: #888;
42
+ }
src/App.tsx ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Toaster } from "@/components/ui/toaster";
2
+ import { Toaster as Sonner } from "@/components/ui/sonner";
3
+ import { TooltipProvider } from "@/components/ui/tooltip";
4
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
5
+ import { BrowserRouter, Routes, Route } from "react-router-dom";
6
+ import { ThemeProvider } from "@/components/theme-provider";
7
+ import Index from "./pages/Index";
8
+ import NotFound from "./pages/NotFound";
9
+ import Landing from "./pages/Landing";
10
+ import Auth from "./pages/Auth";
11
+ import Dashboard from "./pages/Dashboard";
12
+ import VerifyEmail from "./pages/VerifyEmail";
13
+
14
+ const queryClient = new QueryClient();
15
+
16
+ const App = () => (
17
+ <QueryClientProvider client={queryClient}>
18
+ <ThemeProvider attribute="class" defaultTheme="system" enableSystem>
19
+ <TooltipProvider>
20
+ <Toaster />
21
+ <Sonner />
22
+ <BrowserRouter>
23
+ <Routes>
24
+ <Route path="/" element={<Landing />} />
25
+ <Route path="/auth" element={<Auth />} />
26
+ <Route path="/verify-email" element={<VerifyEmail />} />
27
+ <Route path="/dashboard" element={<Dashboard />} />
28
+ <Route path="/builder" element={<Index />} />
29
+ <Route path="*" element={<NotFound />} />
30
+ </Routes>
31
+ </BrowserRouter>
32
+ </TooltipProvider>
33
+ </ThemeProvider>
34
+ </QueryClientProvider>
35
+ );
36
+
37
+ export default App;
src/components/AnimatedBackground.tsx ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useCallback } from "react";
4
+ import Particles from "react-tsparticles";
5
+ import type { Engine } from "tsparticles-engine";
6
+ import { loadStarsPreset } from "tsparticles-preset-stars";
7
+
8
+ const AnimatedBackground = () => {
9
+ const particlesInit = useCallback(async (engine: Engine) => {
10
+ // This function is compatible with the v2 engine provided by react-tsparticles
11
+ await loadStarsPreset(engine);
12
+ }, []);
13
+
14
+ const options = {
15
+ preset: "stars",
16
+ background: {
17
+ color: {
18
+ value: "#0d1117",
19
+ },
20
+ },
21
+ particles: {
22
+ color: {
23
+ value: "#ffffff",
24
+ },
25
+ links: {
26
+ enable: false,
27
+ },
28
+ move: {
29
+ enable: true,
30
+ speed: 0.5,
31
+ direction: "bottom",
32
+ random: false,
33
+ straight: true,
34
+ outModes: {
35
+ default: "out",
36
+ },
37
+ },
38
+ size: {
39
+ value: { min: 1, max: 2.5 },
40
+ },
41
+ },
42
+ };
43
+
44
+ return (
45
+ <div className="fixed inset-0 -z-10">
46
+ <Particles id="tsparticles" init={particlesInit} options={options as any} />
47
+ </div>
48
+ );
49
+ };
50
+
51
+ export default AnimatedBackground;
src/components/ChatInterface.tsx ADDED
@@ -0,0 +1,197 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useRef, useEffect, useState } from "react";
4
+ import { Button } from "@/components/ui/button";
5
+ import { Textarea } from "@/components/ui/textarea";
6
+ import { Send, Loader2, Sparkles, User, Bot, Check, X, GitMerge, Code, FileEdit, Eye } from "lucide-react";
7
+ import { ChatMessage } from "@/services/gemini";
8
+ import { DiffResult, generateDiffSummary } from "@/services/diffPatch";
9
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
10
+
11
+ interface ChangeProposal {
12
+ filename: string;
13
+ diff: DiffResult;
14
+ }
15
+
16
+ interface ChatInterfaceProps {
17
+ prompt: string;
18
+ setPrompt: (prompt: string) => void;
19
+ messages: ChatMessage[];
20
+ isLoading: boolean;
21
+ onSubmit: (e: React.FormEvent) => void;
22
+ pendingModification: { changes: ChangeProposal[] } | null;
23
+ onAccept: () => void;
24
+ onReject: () => void;
25
+ onReview: (diff: DiffResult) => void;
26
+ selectedElement: { tagName: string; description: string } | null;
27
+ onClearSelectedElement: () => void;
28
+ }
29
+
30
+ const loadingMessages = [
31
+ "Thinking...",
32
+ "Warming up the AI...",
33
+ "Analyzing your request...",
34
+ "Consulting with the code spirits...",
35
+ "Brewing a fresh batch of code...",
36
+ "Thinking more deeply...",
37
+ "Structuring the layout...",
38
+ "Compiling pixels...",
39
+ "Almost done...",
40
+ "Putting on the finishing touches..."
41
+ ];
42
+
43
+ const ChatInterface = ({
44
+ prompt,
45
+ setPrompt,
46
+ messages,
47
+ isLoading,
48
+ onSubmit,
49
+ pendingModification,
50
+ onAccept,
51
+ onReject,
52
+ onReview,
53
+ selectedElement,
54
+ onClearSelectedElement,
55
+ }: ChatInterfaceProps) => {
56
+ const chatContainerRef = useRef<HTMLDivElement>(null);
57
+ const [loadingMessage, setLoadingMessage] = useState(loadingMessages[0]);
58
+
59
+ useEffect(() => {
60
+ if (chatContainerRef.current) {
61
+ chatContainerRef.current.scrollTop = chatContainerRef.current.scrollHeight;
62
+ }
63
+ }, [messages, isLoading, pendingModification]);
64
+
65
+ useEffect(() => {
66
+ let intervalId: NodeJS.Timeout;
67
+ if (isLoading) {
68
+ let messageIndex = 0;
69
+ setLoadingMessage(loadingMessages[0]);
70
+ intervalId = setInterval(() => {
71
+ messageIndex = (messageIndex + 1) % loadingMessages.length;
72
+ setLoadingMessage(loadingMessages[messageIndex]);
73
+ }, 3500);
74
+ }
75
+
76
+ return () => {
77
+ if (intervalId) {
78
+ clearInterval(intervalId);
79
+ }
80
+ };
81
+ }, [isLoading]);
82
+
83
+ const handleFormSubmit = (e: React.FormEvent) => {
84
+ e.preventDefault();
85
+ if (!prompt.trim() || isLoading) return;
86
+ onSubmit(e);
87
+ // Clear the prompt after submission
88
+ setPrompt("");
89
+ };
90
+
91
+ const renderMessage = (message: ChatMessage, index: number) => (
92
+ <div key={index} className={`flex animate-in ${message.role === "user" ? "justify-end" : "justify-start"}`}>
93
+ <div className={`max-w-[85%] rounded-2xl p-4 ${message.role === "user" ? "bg-primary text-primary-foreground rounded-br-none" : "bg-card border rounded-bl-none shadow-sm"}`}>
94
+ <div className="flex items-center mb-1">
95
+ {message.role === "user" ? <User className="h-4 w-4 mr-2" /> : <Bot className="h-4 w-4 mr-2" />}
96
+ <span className="font-medium text-sm">{message.role === "user" ? "You" : "Assistant"}</span>
97
+ </div>
98
+ <div className="text-sm whitespace-pre-wrap">{message.content}</div>
99
+ </div>
100
+ </div>
101
+ );
102
+
103
+ return (
104
+ <div className="h-full flex flex-col relative bg-secondary/30">
105
+ <div ref={chatContainerRef} className="flex-1 overflow-y-auto p-4 space-y-4 pb-40">
106
+ {messages.length === 0 && !isLoading && (
107
+ <div className="text-center text-muted-foreground mt-8 animate-in">
108
+ <div className="mx-auto w-16 h-16 bg-gradient-to-r from-blue-100 to-indigo-100 rounded-full flex items-center justify-center mb-4">
109
+ <Sparkles className="h-8 w-8 text-primary" />
110
+ </div>
111
+ <h3 className="font-bold text-lg mb-2 text-foreground">Describe your website prototype</h3>
112
+ <p className="text-sm mb-4">Example: "Create a landing page and an about page..."</p>
113
+ </div>
114
+ )}
115
+ {messages.map(renderMessage)}
116
+ {pendingModification && (
117
+ <div className="animate-in">
118
+ <div className="flex items-center p-4 bg-card rounded-t-2xl border-b">
119
+ <GitMerge className="h-5 w-5 mr-3 text-primary" />
120
+ <div>
121
+ <h4 className="font-medium text-foreground">Code Modification Proposed</h4>
122
+ <p className="text-sm text-muted-foreground">Review the changes below.</p>
123
+ </div>
124
+ </div>
125
+ <div className="space-y-2 p-4 bg-card rounded-b-2xl border-x border-b">
126
+ {pendingModification.changes.map((change, index) => {
127
+ const summary = generateDiffSummary(change.diff);
128
+ return (
129
+ <Card key={index} className="bg-secondary/50">
130
+ <CardContent className="p-3 flex items-center justify-between">
131
+ <div className="flex items-center">
132
+ <FileEdit className="h-4 w-4 text-muted-foreground mr-3" />
133
+ <div>
134
+ <p className="text-sm font-mono font-medium">{change.filename}</p>
135
+ <p className="text-xs text-muted-foreground">
136
+ <span className="text-green-600">+{summary.added}</span>, <span className="text-red-600">-{summary.removed}</span> lines
137
+ </p>
138
+ </div>
139
+ </div>
140
+ <Button size="sm" variant="outline" onClick={() => onReview(change.diff)}>
141
+ <Eye className="h-4 w-4 mr-1" /> Review
142
+ </Button>
143
+ </CardContent>
144
+ </Card>
145
+ );
146
+ })}
147
+ <div className="flex flex-wrap items-center gap-2 pt-2">
148
+ <Button size="sm" onClick={onAccept} className="bg-green-600 hover:bg-green-700"><Check className="h-4 w-4 mr-1" /> Accept All</Button>
149
+ <Button size="sm" onClick={onReject} variant="destructive"><X className="h-4 w-4 mr-1" /> Reject All</Button>
150
+ </div>
151
+ </div>
152
+ </div>
153
+ )}
154
+ {isLoading && (
155
+ <div className="flex items-center p-4 bg-card rounded-2xl border rounded-bl-none shadow-sm animate-in">
156
+ <Loader2 className="h-4 w-4 animate-spin text-primary mr-3" />
157
+ <span className="text-sm text-muted-foreground">{loadingMessage}</span>
158
+ </div>
159
+ )}
160
+ </div>
161
+
162
+ <div className="absolute bottom-0 left-0 right-0 p-4 bg-secondary/30">
163
+ {selectedElement && (
164
+ <div className="bg-card border rounded-lg p-2.5 mb-2 flex items-center justify-between animate-in fade-in slide-in-from-bottom-2 duration-300 shadow-sm">
165
+ <div className="flex items-center gap-3 flex-1 min-w-0">
166
+ <Code className="h-5 w-5 text-muted-foreground flex-shrink-0" />
167
+ <div className="flex-1 min-w-0">
168
+ <p className="font-mono text-sm font-medium text-foreground">{selectedElement.tagName}</p>
169
+ <p className="text-xs text-muted-foreground truncate">{selectedElement.description}</p>
170
+ </div>
171
+ </div>
172
+ <Button variant="ghost" size="icon" className="h-7 w-7 flex-shrink-0" onClick={onClearSelectedElement}>
173
+ <X className="h-4 w-4" />
174
+ </Button>
175
+ </div>
176
+ )}
177
+ <form onSubmit={handleFormSubmit}>
178
+ <div className="relative">
179
+ <Textarea
180
+ value={prompt}
181
+ onChange={(e) => setPrompt(e.target.value)}
182
+ placeholder="Describe the website you want to create or request a modification..."
183
+ className="w-full resize-none border border-border/50 bg-background/60 backdrop-blur-lg focus:border-primary focus:ring-primary rounded-2xl shadow-xl py-4 pl-4 pr-16"
184
+ rows={3}
185
+ disabled={isLoading || !!pendingModification}
186
+ />
187
+ <Button type="submit" disabled={isLoading || !prompt.trim() || !!pendingModification} className="absolute right-3 bottom-3 h-10 w-10 rounded-full bg-primary text-primary-foreground hover:bg-primary/90 shadow-lg flex items-center justify-center transition-transform hover:scale-110">
188
+ <Send className="h-4 w-4" />
189
+ </Button>
190
+ </div>
191
+ </form>
192
+ </div>
193
+ </div>
194
+ );
195
+ };
196
+
197
+ export default ChatInterface;
src/components/CodeEditor.tsx ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useEffect, useState } from "react";
4
+ import Editor from "@monaco-editor/react";
5
+ import { Button } from "@/components/ui/button";
6
+ import { Play, Download } from "lucide-react";
7
+ import { Card, CardContent } from "@/components/ui/card";
8
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
9
+ import { ProjectFile } from "@/types/project";
10
+
11
+ interface CodeEditorProps {
12
+ files: ProjectFile[];
13
+ activeFile: string;
14
+ onFileChange: (fileName: string, newCode: string) => void;
15
+ onFileSelect: (fileName: string) => void;
16
+ onExport: () => void;
17
+ }
18
+
19
+ const CodeEditor = ({
20
+ files,
21
+ activeFile,
22
+ onFileChange,
23
+ onFileSelect,
24
+ onExport
25
+ }: CodeEditorProps) => {
26
+ const currentFile = files.find(f => f.name === activeFile);
27
+
28
+ return (
29
+ <div className="h-full flex flex-col bg-gray-50">
30
+ <Tabs value={activeFile} onValueChange={onFileSelect} className="h-full flex flex-col">
31
+ <div className="p-2 border-b bg-white">
32
+ <TabsList>
33
+ {files.map(file => (
34
+ <TabsTrigger key={file.name} value={file.name}>
35
+ {file.name}
36
+ </TabsTrigger>
37
+ ))}
38
+ </TabsList>
39
+ </div>
40
+
41
+ <Card className="flex-1 border-0 shadow-none">
42
+ <CardContent className="p-0 h-full">
43
+ {files.map(file => (
44
+ <TabsContent key={file.name} value={file.name} className="h-full m-0">
45
+ <Editor
46
+ height="100%"
47
+ defaultLanguage="html"
48
+ value={file.code}
49
+ onChange={(value) => onFileChange(file.name, value || "")}
50
+ theme="vs-light"
51
+ options={{
52
+ minimap: { enabled: true },
53
+ scrollBeyondLastLine: false,
54
+ automaticLayout: true,
55
+ fontSize: 14,
56
+ tabSize: 2,
57
+ wordWrap: "on",
58
+ }}
59
+ />
60
+ </TabsContent>
61
+ ))}
62
+ </CardContent>
63
+ </Card>
64
+ </Tabs>
65
+ <div className="p-3 border-t bg-white flex justify-end items-center">
66
+ <Button
67
+ onClick={onExport}
68
+ size="sm"
69
+ variant="outline"
70
+ disabled={files.length === 0}
71
+ className="border-gray-300"
72
+ >
73
+ <Download className="h-4 w-4 mr-1" />
74
+ Export Project
75
+ </Button>
76
+ </div>
77
+ </div>
78
+ );
79
+ };
80
+
81
+ export default CodeEditor;
src/components/DiffReviewModal.tsx ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { Button } from "@/components/ui/button";
5
+ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
6
+ import { ScrollArea } from "@/components/ui/scroll-area";
7
+ import { DiffResult } from "@/services/diffPatch";
8
+ import { Card, CardContent } from "@/components/ui/card";
9
+
10
+ interface DiffReviewModalProps {
11
+ isOpen: boolean;
12
+ onClose: () => void;
13
+ diffResult: DiffResult | null;
14
+ onApply: () => void;
15
+ }
16
+
17
+ const DiffReviewModal = ({ isOpen, onClose, diffResult, onApply }: DiffReviewModalProps) => {
18
+ const [isApplying, setIsApplying] = useState(false);
19
+
20
+ const handleApply = async () => {
21
+ setIsApplying(true);
22
+ try {
23
+ await onApply();
24
+ onClose();
25
+ } finally {
26
+ setIsApplying(false);
27
+ }
28
+ };
29
+
30
+ if (!diffResult) return null;
31
+
32
+ let oldLineNum = 1;
33
+ let newLineNum = 1;
34
+
35
+ return (
36
+ <Dialog open={isOpen} onOpenChange={onClose}>
37
+ <DialogContent className="max-w-4xl h-[80vh] flex flex-col">
38
+ <DialogHeader>
39
+ <DialogTitle className="flex items-center">
40
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="mr-2">
41
+ <path d="M12 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
42
+ <path d="M14 15H9"/>
43
+ <path d="M16 12c.5523 0 1-.4477 1-1s-.4477-1-1-1-.999.4477-.999 1 .4467 1 .999 1Z"/>
44
+ <path d="M16 18c1.6569 0 3-1.3431 3-3s-1.3431-3-3-3-3 1.3431-3 3 1.3431 3 3 3Z"/>
45
+ <path d="m17 2 4 4"/>
46
+ <path d="M21 2 12 11"/>
47
+ </svg>
48
+ Code Changes Review
49
+ </DialogTitle>
50
+ </DialogHeader>
51
+ <div className="text-sm text-gray-500 mb-2 flex items-center">
52
+ <span className="bg-green-100 text-green-800 px-2 py-1 rounded mr-2">+{diffResult.added} lines</span>
53
+ <span className="bg-red-100 text-red-800 px-2 py-1 rounded">-{diffResult.removed} lines</span>
54
+ </div>
55
+ <Card className="flex-1 border border-gray-200 min-h-0">
56
+ <CardContent className="p-0 h-full">
57
+ <ScrollArea className="h-full font-mono text-sm bg-gray-50 rounded-md">
58
+ <div className="p-4">
59
+ {diffResult.changes.map((change, changeIndex) => {
60
+ const lines = change.value.endsWith('\n') ? change.value.slice(0, -1).split('\n') : change.value.split('\n');
61
+
62
+ return lines.map((line, lineIndex) => {
63
+ let currentOldLine = ' ';
64
+ let currentNewLine = ' ';
65
+
66
+ if (change.removed) {
67
+ currentOldLine = String(oldLineNum++);
68
+ } else if (change.added) {
69
+ currentNewLine = String(newLineNum++);
70
+ } else {
71
+ currentOldLine = String(oldLineNum++);
72
+ currentNewLine = String(newLineNum++);
73
+ }
74
+
75
+ return (
76
+ <div
77
+ key={`${changeIndex}-${lineIndex}`}
78
+ className={`flex items-start py-0.5 ${
79
+ change.added ? 'bg-green-100' :
80
+ change.removed ? 'bg-red-100' : ''
81
+ }`}
82
+ >
83
+ <span className="w-10 text-right pr-2 text-gray-400 select-none">{currentOldLine}</span>
84
+ <span className="w-10 text-right pr-2 text-gray-400 select-none">{currentNewLine}</span>
85
+ <span className={`w-6 text-center font-bold ${
86
+ change.added ? 'text-green-700' :
87
+ change.removed ? 'text-red-700' : 'text-gray-500'
88
+ }`}>
89
+ {change.added ? '+' : change.removed ? '-' : ' '}
90
+ </span>
91
+ <span className={`flex-1 whitespace-pre-wrap break-all ${
92
+ change.added ? 'text-green-900' :
93
+ change.removed ? 'text-red-900' : 'text-gray-800'
94
+ }`}>
95
+ {line}
96
+ </span>
97
+ </div>
98
+ );
99
+ });
100
+ })}
101
+ </div>
102
+ </ScrollArea>
103
+ </CardContent>
104
+ </Card>
105
+ <DialogFooter className="flex space-x-2 pt-4">
106
+ <Button variant="outline" onClick={onClose} className="border-gray-300">
107
+ Cancel
108
+ </Button>
109
+ <Button
110
+ onClick={handleApply}
111
+ disabled={isApplying}
112
+ className="bg-gradient-to-r from-blue-600 to-indigo-700 hover:from-blue-700 hover:to-indigo-800"
113
+ >
114
+ {isApplying ? (
115
+ <>
116
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="mr-2 animate-spin">
117
+ <path d="M21 12a9 9 0 1 1-6.219-8.56"/>
118
+ </svg>
119
+ Applying...
120
+ </>
121
+ ) : (
122
+ "Apply Changes"
123
+ )}
124
+ </Button>
125
+ </DialogFooter>
126
+ </DialogContent>
127
+ </Dialog>
128
+ );
129
+ };
130
+
131
+ export default DiffReviewModal;
src/components/PreviewPanel.tsx ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { forwardRef } from "react";
4
+ import { Card, CardContent } from "@/components/ui/card";
5
+
6
+ interface PreviewPanelProps {
7
+ code: string;
8
+ className?: string;
9
+ }
10
+
11
+ const initialContent = `
12
+ <!DOCTYPE html>
13
+ <html>
14
+ <head>
15
+ <title>Preview</title>
16
+ <style>
17
+ body {
18
+ display: flex;
19
+ justify-content: center;
20
+ align-items: center;
21
+ height: 100vh;
22
+ margin: 0;
23
+ background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
24
+ font-family: system-ui, sans-serif;
25
+ color: #6b7280;
26
+ }
27
+ .container {
28
+ text-align: center;
29
+ padding: 2rem;
30
+ background: white;
31
+ border-radius: 12px;
32
+ box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1);
33
+ max-width: 500px;
34
+ }
35
+ .icon {
36
+ width: 60px;
37
+ height: 60px;
38
+ margin: 0 auto 1rem;
39
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
40
+ border-radius: 50%;
41
+ display: flex;
42
+ align-items: center;
43
+ justify-content: center;
44
+ }
45
+ </style>
46
+ </head>
47
+ <body>
48
+ <div class="container">
49
+ <div class="icon">
50
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
51
+ <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path>
52
+ </svg>
53
+ </div>
54
+ <h2>Preview Panel</h2>
55
+ <p>Enter a prompt to generate a preview of your website</p>
56
+ </div>
57
+ </body>
58
+ </html>
59
+ `;
60
+
61
+ const PreviewPanel = forwardRef<HTMLIFrameElement, PreviewPanelProps>(({ code, className = "" }, ref) => {
62
+ return (
63
+ <div className="h-full p-4 bg-gray-50">
64
+ <Card className="h-full border-0 shadow-none">
65
+ <CardContent className="p-0 h-full">
66
+ <iframe
67
+ ref={ref}
68
+ title="preview"
69
+ className={`w-full h-full border-0 rounded-lg shadow-sm ${className}`}
70
+ sandbox="allow-scripts allow-same-origin"
71
+ srcDoc={code || initialContent}
72
+ />
73
+ </CardContent>
74
+ </Card>
75
+ </div>
76
+ );
77
+ });
78
+
79
+ PreviewPanel.displayName = "PreviewPanel";
80
+
81
+ export default PreviewPanel;
src/components/ProjectHistory.tsx ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
5
+ import { Badge } from "@/components/ui/badge";
6
+ import { Button } from "@/components/ui/button";
7
+ import {
8
+ CheckCircle,
9
+ Clock,
10
+ XCircle,
11
+ MoreHorizontal,
12
+ Play,
13
+ Eye
14
+ } from "lucide-react";
15
+ import { Project } from "@/types/project";
16
+ import { useNavigate } from "react-router-dom";
17
+
18
+ interface ProjectHistoryProps {
19
+ projects: Project[];
20
+ onViewProject: (project: Project) => void;
21
+ }
22
+
23
+ const ProjectHistory = ({ projects, onViewProject }: ProjectHistoryProps) => {
24
+ const navigate = useNavigate();
25
+ const [expandedProject, setExpandedProject] = useState<string | null>(null);
26
+
27
+ const getStatusIcon = (status: Project["status"]) => {
28
+ switch (status) {
29
+ case 'completed':
30
+ return <CheckCircle className="h-4 w-4 text-green-500" />;
31
+ case 'pending':
32
+ return <Clock className="h-4 w-4 text-blue-500" />;
33
+ case 'failed':
34
+ return <XCircle className="h-4 w-4 text-red-500" />;
35
+ default:
36
+ return <Clock className="h-4 w-4 text-gray-500" />;
37
+ }
38
+ };
39
+
40
+ const getStatusBadge = (status: Project["status"]) => {
41
+ switch (status) {
42
+ case 'completed':
43
+ return <Badge variant="secondary" className="bg-green-100 text-green-800">Completed</Badge>;
44
+ case 'pending':
45
+ return <Badge variant="secondary" className="bg-blue-100 text-blue-800">In Progress</Badge>;
46
+ case 'failed':
47
+ return <Badge variant="secondary" className="bg-red-100 text-red-800">Failed</Badge>;
48
+ default:
49
+ return <Badge variant="secondary" className="bg-gray-100 text-gray-800">Unknown</Badge>;
50
+ }
51
+ };
52
+
53
+ const formatDate = (date: Date) => {
54
+ return new Intl.DateTimeFormat("en-US", {
55
+ month: "short",
56
+ day: "numeric",
57
+ hour: "2-digit",
58
+ minute: "2-digit",
59
+ }).format(date);
60
+ };
61
+
62
+ return (
63
+ <div className="space-y-4">
64
+ {projects.map((project) => (
65
+ <Card key={project.id} className="border border-border">
66
+ <CardHeader className="pb-3">
67
+ <div className="flex justify-between items-start">
68
+ <div className="flex-1 min-w-0">
69
+ <CardTitle className="text-lg flex items-center gap-2">
70
+ <span className="truncate">{project.name}</span>
71
+ {getStatusIcon(project.status)}
72
+ </CardTitle>
73
+ <p className="text-sm text-muted-foreground mt-1">
74
+ Created {formatDate(project.createdAt)}
75
+ </p>
76
+ </div>
77
+ <div className="flex items-center gap-2 ml-4">
78
+ {getStatusBadge(project.status)}
79
+ <Button
80
+ variant="ghost"
81
+ size="icon"
82
+ onClick={() => setExpandedProject(expandedProject === project.id ? null : project.id)}
83
+ >
84
+ <MoreHorizontal className="h-4 w-4" />
85
+ </Button>
86
+ </div>
87
+ </div>
88
+ </CardHeader>
89
+
90
+ {expandedProject === project.id && (
91
+ <CardContent className="pt-0 border-t">
92
+ <div className="flex gap-2 mt-3">
93
+ <Button
94
+ size="sm"
95
+ onClick={() => navigate(`/builder?projectId=${project.id}`)}
96
+ >
97
+ <Play className="h-4 w-4 mr-2" />
98
+ Continue
99
+ </Button>
100
+ <Button
101
+ variant="outline"
102
+ size="sm"
103
+ onClick={() => onViewProject(project)}
104
+ >
105
+ <Eye className="h-4 w-4 mr-2" />
106
+ View
107
+ </Button>
108
+ </div>
109
+ </CardContent>
110
+ )}
111
+ </Card>
112
+ ))}
113
+ </div>
114
+ );
115
+ };
116
+
117
+ export default ProjectHistory;
src/components/VersionsPanel.tsx ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { Card, CardContent } from "@/components/ui/card";
5
+ import { Button } from "@/components/ui/button";
6
+ import { Badge } from "@/components/ui/badge";
7
+ import { History, Clock } from "lucide-react";
8
+ import { format } from "date-fns";
9
+ import { ProjectVersion } from "@/types/project";
10
+
11
+ interface VersionsPanelProps {
12
+ versions: ProjectVersion[];
13
+ onRestoreVersion: (versionId: string) => void;
14
+ }
15
+
16
+ const VersionsPanel = ({ versions, onRestoreVersion }: VersionsPanelProps) => {
17
+ const [expandedVersion, setExpandedVersion] = useState<string | null>(null);
18
+
19
+ const toggleExpand = (versionId: string) => {
20
+ setExpandedVersion(expandedVersion === versionId ? null : versionId);
21
+ };
22
+
23
+ // Helper function to safely format dates
24
+ const formatDate = (dateString: string) => {
25
+ try {
26
+ const date = new Date(dateString);
27
+ if (isNaN(date.getTime())) {
28
+ return "Invalid date";
29
+ }
30
+ return format(date, "MMM d, yyyy h:mm a");
31
+ } catch (error) {
32
+ console.error("Error formatting date:", error);
33
+ return "Invalid date";
34
+ }
35
+ };
36
+
37
+ return (
38
+ <div className="p-4 bg-background h-full overflow-y-auto">
39
+ <div className="flex items-center mb-4">
40
+ <History className="h-5 w-5 mr-2 text-foreground" />
41
+ <h3 className="font-bold text-lg text-foreground">Version History</h3>
42
+ </div>
43
+
44
+ {versions.length === 0 ? (
45
+ <div className="text-center py-8 text-muted-foreground">
46
+ <Clock className="h-12 w-12 mx-auto mb-2 opacity-50" />
47
+ <p>No versions found</p>
48
+ <p className="text-sm mt-1">Your project versions will appear here</p>
49
+ </div>
50
+ ) : (
51
+ <div className="space-y-3">
52
+ {versions.map((version, index) => {
53
+ const versionNumber = versions.length - index;
54
+ const isCurrent = index === 0;
55
+
56
+ return (
57
+ <Card key={version.id} className="bg-card border border-border">
58
+ <CardContent className="p-4">
59
+ <div className="flex justify-between items-start">
60
+ <div>
61
+ <div className="flex items-center">
62
+ <h4 className="font-medium text-foreground">Version {versionNumber}</h4>
63
+ {isCurrent && (
64
+ <Badge variant="secondary" className="ml-2 bg-primary/10 text-primary">
65
+ Current
66
+ </Badge>
67
+ )}
68
+ </div>
69
+ <p className="text-xs text-muted-foreground mt-1 flex items-center">
70
+ <Clock className="h-3 w-3 mr-1" />
71
+ {formatDate(version.createdAt)}
72
+ </p>
73
+ </div>
74
+ <Button
75
+ variant="outline"
76
+ size="sm"
77
+ onClick={() => onRestoreVersion(version.id)}
78
+ disabled={isCurrent}
79
+ className="text-xs"
80
+ >
81
+ Restore
82
+ </Button>
83
+ </div>
84
+
85
+ {version.messages && version.messages.length > 0 && (
86
+ <div className="mt-3">
87
+ <Button
88
+ variant="ghost"
89
+ size="sm"
90
+ className="p-0 h-auto text-muted-foreground hover:text-foreground text-xs"
91
+ onClick={() => toggleExpand(version.id)}
92
+ >
93
+ {expandedVersion === version.id ? "Hide Prompt" : "Show Prompt"}
94
+ </Button>
95
+
96
+ {expandedVersion === version.id && (
97
+ <div className="mt-2 p-3 bg-muted rounded-md">
98
+ <p className="text-sm text-foreground">
99
+ {version.messages.find(m => m.role === 'user')?.content || 'No prompt found'}
100
+ </p>
101
+ </div>
102
+ )}
103
+ </div>
104
+ )}
105
+ </CardContent>
106
+ </Card>
107
+ );
108
+ })}
109
+ </div>
110
+ )}
111
+ </div>
112
+ );
113
+ };
114
+
115
+ export default VersionsPanel;
src/components/made-with-dyad.tsx ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ export const MadeWithDyad = () => {
2
+ return (
3
+ <div className="py-3 text-center border-t bg-white">
4
+ <div className="text-xs text-gray-500">
5
+ &copy; 2025 Halopai Inc. All Rights Reserved.
6
+ </div>
7
+ </div>
8
+ );
9
+ };
src/components/theme-provider.tsx ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import React from "react"
4
+ import { ThemeProvider as NextThemesProvider } from "next-themes"
5
+ import { type ThemeProviderProps } from "next-themes/dist/types"
6
+
7
+ export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
8
+ return <NextThemesProvider {...props}>{children}</NextThemesProvider>
9
+ }
src/components/theme-toggle.tsx ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import React from "react"
4
+ import { Moon, Sun } from "lucide-react"
5
+ import { useTheme } from "next-themes"
6
+
7
+ import { Button } from "@/components/ui/button"
8
+ import {
9
+ DropdownMenu,
10
+ DropdownMenuContent,
11
+ DropdownMenuItem,
12
+ DropdownMenuTrigger,
13
+ } from "@/components/ui/dropdown-menu"
14
+
15
+ export function ModeToggle() {
16
+ const { setTheme } = useTheme()
17
+
18
+ return (
19
+ <DropdownMenu>
20
+ <DropdownMenuTrigger asChild>
21
+ <Button variant="outline" size="icon">
22
+ <Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
23
+ <Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
24
+ <span className="sr-only">Toggle theme</span>
25
+ </Button>
26
+ </DropdownMenuTrigger>
27
+ <DropdownMenuContent align="end">
28
+ <DropdownMenuItem onClick={() => setTheme("light")}>
29
+ Light
30
+ </DropdownMenuItem>
31
+ <DropdownMenuItem onClick={() => setTheme("dark")}>
32
+ Dark
33
+ </DropdownMenuItem>
34
+ <DropdownMenuItem onClick={() => setTheme("system")}>
35
+ System
36
+ </DropdownMenuItem>
37
+ </DropdownMenuContent>
38
+ </DropdownMenu>
39
+ )
40
+ }
src/components/ui/accordion.tsx ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import * as AccordionPrimitive from "@radix-ui/react-accordion";
3
+ import { ChevronDown } from "lucide-react";
4
+
5
+ import { cn } from "@/lib/utils";
6
+
7
+ const Accordion = AccordionPrimitive.Root;
8
+
9
+ const AccordionItem = React.forwardRef<
10
+ React.ElementRef<typeof AccordionPrimitive.Item>,
11
+ React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
12
+ >(({ className, ...props }, ref) => (
13
+ <AccordionPrimitive.Item
14
+ ref={ref}
15
+ className={cn("border-b", className)}
16
+ {...props}
17
+ />
18
+ ));
19
+ AccordionItem.displayName = "AccordionItem";
20
+
21
+ const AccordionTrigger = React.forwardRef<
22
+ React.ElementRef<typeof AccordionPrimitive.Trigger>,
23
+ React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
24
+ >(({ className, children, ...props }, ref) => (
25
+ <AccordionPrimitive.Header className="flex">
26
+ <AccordionPrimitive.Trigger
27
+ ref={ref}
28
+ className={cn(
29
+ "flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
30
+ className,
31
+ )}
32
+ {...props}
33
+ >
34
+ {children}
35
+ <ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
36
+ </AccordionPrimitive.Trigger>
37
+ </AccordionPrimitive.Header>
38
+ ));
39
+ AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
40
+
41
+ const AccordionContent = React.forwardRef<
42
+ React.ElementRef<typeof AccordionPrimitive.Content>,
43
+ React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
44
+ >(({ className, children, ...props }, ref) => (
45
+ <AccordionPrimitive.Content
46
+ ref={ref}
47
+ className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
48
+ {...props}
49
+ >
50
+ <div className={cn("pb-4 pt-0", className)}>{children}</div>
51
+ </AccordionPrimitive.Content>
52
+ ));
53
+
54
+ AccordionContent.displayName = AccordionPrimitive.Content.displayName;
55
+
56
+ export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
src/components/ui/alert-dialog.tsx ADDED
@@ -0,0 +1,139 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
3
+
4
+ import { cn } from "@/lib/utils";
5
+ import { buttonVariants } from "@/components/ui/button";
6
+
7
+ const AlertDialog = AlertDialogPrimitive.Root;
8
+
9
+ const AlertDialogTrigger = AlertDialogPrimitive.Trigger;
10
+
11
+ const AlertDialogPortal = AlertDialogPrimitive.Portal;
12
+
13
+ const AlertDialogOverlay = React.forwardRef<
14
+ React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
15
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
16
+ >(({ className, ...props }, ref) => (
17
+ <AlertDialogPrimitive.Overlay
18
+ className={cn(
19
+ "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",
20
+ className,
21
+ )}
22
+ {...props}
23
+ ref={ref}
24
+ />
25
+ ));
26
+ AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;
27
+
28
+ const AlertDialogContent = React.forwardRef<
29
+ React.ElementRef<typeof AlertDialogPrimitive.Content>,
30
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
31
+ >(({ className, ...props }, ref) => (
32
+ <AlertDialogPortal>
33
+ <AlertDialogOverlay />
34
+ <AlertDialogPrimitive.Content
35
+ ref={ref}
36
+ className={cn(
37
+ "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",
38
+ className,
39
+ )}
40
+ {...props}
41
+ />
42
+ </AlertDialogPortal>
43
+ ));
44
+ AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;
45
+
46
+ const AlertDialogHeader = ({
47
+ className,
48
+ ...props
49
+ }: React.HTMLAttributes<HTMLDivElement>) => (
50
+ <div
51
+ className={cn(
52
+ "flex flex-col space-y-2 text-center sm:text-left",
53
+ className,
54
+ )}
55
+ {...props}
56
+ />
57
+ );
58
+ AlertDialogHeader.displayName = "AlertDialogHeader";
59
+
60
+ const AlertDialogFooter = ({
61
+ className,
62
+ ...props
63
+ }: React.HTMLAttributes<HTMLDivElement>) => (
64
+ <div
65
+ className={cn(
66
+ "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
67
+ className,
68
+ )}
69
+ {...props}
70
+ />
71
+ );
72
+ AlertDialogFooter.displayName = "AlertDialogFooter";
73
+
74
+ const AlertDialogTitle = React.forwardRef<
75
+ React.ElementRef<typeof AlertDialogPrimitive.Title>,
76
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
77
+ >(({ className, ...props }, ref) => (
78
+ <AlertDialogPrimitive.Title
79
+ ref={ref}
80
+ className={cn("text-lg font-semibold", className)}
81
+ {...props}
82
+ />
83
+ ));
84
+ AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName;
85
+
86
+ const AlertDialogDescription = React.forwardRef<
87
+ React.ElementRef<typeof AlertDialogPrimitive.Description>,
88
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
89
+ >(({ className, ...props }, ref) => (
90
+ <AlertDialogPrimitive.Description
91
+ ref={ref}
92
+ className={cn("text-sm text-muted-foreground", className)}
93
+ {...props}
94
+ />
95
+ ));
96
+ AlertDialogDescription.displayName =
97
+ AlertDialogPrimitive.Description.displayName;
98
+
99
+ const AlertDialogAction = React.forwardRef<
100
+ React.ElementRef<typeof AlertDialogPrimitive.Action>,
101
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
102
+ >(({ className, ...props }, ref) => (
103
+ <AlertDialogPrimitive.Action
104
+ ref={ref}
105
+ className={cn(buttonVariants(), className)}
106
+ {...props}
107
+ />
108
+ ));
109
+ AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName;
110
+
111
+ const AlertDialogCancel = React.forwardRef<
112
+ React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
113
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
114
+ >(({ className, ...props }, ref) => (
115
+ <AlertDialogPrimitive.Cancel
116
+ ref={ref}
117
+ className={cn(
118
+ buttonVariants({ variant: "outline" }),
119
+ "mt-2 sm:mt-0",
120
+ className,
121
+ )}
122
+ {...props}
123
+ />
124
+ ));
125
+ AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName;
126
+
127
+ export {
128
+ AlertDialog,
129
+ AlertDialogPortal,
130
+ AlertDialogOverlay,
131
+ AlertDialogTrigger,
132
+ AlertDialogContent,
133
+ AlertDialogHeader,
134
+ AlertDialogFooter,
135
+ AlertDialogTitle,
136
+ AlertDialogDescription,
137
+ AlertDialogAction,
138
+ AlertDialogCancel,
139
+ };
src/components/ui/alert.tsx ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import { cva, type VariantProps } from "class-variance-authority";
3
+
4
+ import { cn } from "@/lib/utils";
5
+
6
+ const alertVariants = cva(
7
+ "relative w-full rounded-lg border 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 };
src/components/ui/aspect-ratio.tsx ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio";
2
+
3
+ const AspectRatio = AspectRatioPrimitive.Root;
4
+
5
+ export { AspectRatio };
src/components/ui/avatar.tsx ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import * as AvatarPrimitive from "@radix-ui/react-avatar";
3
+
4
+ import { cn } from "@/lib/utils";
5
+
6
+ const Avatar = React.forwardRef<
7
+ React.ElementRef<typeof AvatarPrimitive.Root>,
8
+ React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
9
+ >(({ className, ...props }, ref) => (
10
+ <AvatarPrimitive.Root
11
+ ref={ref}
12
+ className={cn(
13
+ "relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
14
+ className,
15
+ )}
16
+ {...props}
17
+ />
18
+ ));
19
+ Avatar.displayName = AvatarPrimitive.Root.displayName;
20
+
21
+ const AvatarImage = React.forwardRef<
22
+ React.ElementRef<typeof AvatarPrimitive.Image>,
23
+ React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
24
+ >(({ className, ...props }, ref) => (
25
+ <AvatarPrimitive.Image
26
+ ref={ref}
27
+ className={cn("aspect-square h-full w-full", className)}
28
+ {...props}
29
+ />
30
+ ));
31
+ AvatarImage.displayName = AvatarPrimitive.Image.displayName;
32
+
33
+ const AvatarFallback = React.forwardRef<
34
+ React.ElementRef<typeof AvatarPrimitive.Fallback>,
35
+ React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
36
+ >(({ className, ...props }, ref) => (
37
+ <AvatarPrimitive.Fallback
38
+ ref={ref}
39
+ className={cn(
40
+ "flex h-full w-full items-center justify-center rounded-full bg-muted",
41
+ className,
42
+ )}
43
+ {...props}
44
+ />
45
+ ));
46
+ AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
47
+
48
+ export { Avatar, AvatarImage, AvatarFallback };
src/components/ui/badge.tsx ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import { cva, type VariantProps } from "class-variance-authority"
3
+
4
+ import { cn } from "@/lib/utils"
5
+
6
+ const badgeVariants = cva(
7
+ "inline-flex items-center rounded-full border 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 }
src/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]:size-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
+ };
src/components/ui/button.tsx ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import { Slot } from "@radix-ui/react-slot";
3
+ import { cva, type VariantProps } from "class-variance-authority";
4
+
5
+ import { cn } from "@/lib/utils";
6
+
7
+ const buttonVariants = cva(
8
+ "inline-flex items-center justify-center 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 };
src/components/ui/calendar.tsx ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import { ChevronLeft, ChevronRight } from "lucide-react";
3
+ import { DayPicker } from "react-day-picker";
4
+
5
+ import { cn } from "@/lib/utils";
6
+ import { buttonVariants } from "@/components/ui/button";
7
+
8
+ export type CalendarProps = React.ComponentProps<typeof DayPicker>;
9
+
10
+ function Calendar({
11
+ className,
12
+ classNames,
13
+ showOutsideDays = true,
14
+ ...props
15
+ }: CalendarProps) {
16
+ return (
17
+ <DayPicker
18
+ showOutsideDays={showOutsideDays}
19
+ className={cn("p-3", className)}
20
+ classNames={{
21
+ months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
22
+ month: "space-y-4",
23
+ caption: "flex justify-center pt-1 relative items-center",
24
+ caption_label: "text-sm font-medium",
25
+ nav: "space-x-1 flex items-center",
26
+ nav_button: cn(
27
+ buttonVariants({ variant: "outline" }),
28
+ "h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100",
29
+ ),
30
+ nav_button_previous: "absolute left-1",
31
+ nav_button_next: "absolute right-1",
32
+ table: "w-full border-collapse space-y-1",
33
+ head_row: "flex",
34
+ head_cell:
35
+ "text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]",
36
+ row: "flex w-full mt-2",
37
+ 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",
38
+ day: cn(
39
+ buttonVariants({ variant: "ghost" }),
40
+ "h-9 w-9 p-0 font-normal aria-selected:opacity-100",
41
+ ),
42
+ day_range_end: "day-range-end",
43
+ day_selected:
44
+ "bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
45
+ day_today: "bg-accent text-accent-foreground",
46
+ day_outside:
47
+ "day-outside text-muted-foreground opacity-50 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30",
48
+ day_disabled: "text-muted-foreground opacity-50",
49
+ day_range_middle:
50
+ "aria-selected:bg-accent aria-selected:text-accent-foreground",
51
+ day_hidden: "invisible",
52
+ ...classNames,
53
+ }}
54
+ components={{
55
+ IconLeft: ({ ..._props }) => <ChevronLeft className="h-4 w-4" />,
56
+ IconRight: ({ ..._props }) => <ChevronRight className="h-4 w-4" />,
57
+ }}
58
+ {...props}
59
+ />
60
+ );
61
+ }
62
+ Calendar.displayName = "Calendar";
63
+
64
+ export { Calendar };
src/components/ui/card.tsx ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+
3
+ import { cn } from "@/lib/utils";
4
+
5
+ const Card = React.forwardRef<
6
+ HTMLDivElement,
7
+ React.HTMLAttributes<HTMLDivElement>
8
+ >(({ className, ...props }, ref) => (
9
+ <div
10
+ ref={ref}
11
+ className={cn(
12
+ "rounded-lg border bg-card text-card-foreground shadow-sm",
13
+ className,
14
+ )}
15
+ {...props}
16
+ />
17
+ ));
18
+ Card.displayName = "Card";
19
+
20
+ const CardHeader = React.forwardRef<
21
+ HTMLDivElement,
22
+ React.HTMLAttributes<HTMLDivElement>
23
+ >(({ className, ...props }, ref) => (
24
+ <div
25
+ ref={ref}
26
+ className={cn("flex flex-col space-y-1.5 p-6", className)}
27
+ {...props}
28
+ />
29
+ ));
30
+ CardHeader.displayName = "CardHeader";
31
+
32
+ const CardTitle = React.forwardRef<
33
+ HTMLParagraphElement,
34
+ React.HTMLAttributes<HTMLHeadingElement>
35
+ >(({ className, ...props }, ref) => (
36
+ <h3
37
+ ref={ref}
38
+ className={cn(
39
+ "text-2xl font-semibold leading-none tracking-tight",
40
+ className,
41
+ )}
42
+ {...props}
43
+ />
44
+ ));
45
+ CardTitle.displayName = "CardTitle";
46
+
47
+ const CardDescription = React.forwardRef<
48
+ HTMLParagraphElement,
49
+ React.HTMLAttributes<HTMLParagraphElement>
50
+ >(({ className, ...props }, ref) => (
51
+ <p
52
+ ref={ref}
53
+ className={cn("text-sm text-muted-foreground", className)}
54
+ {...props}
55
+ />
56
+ ));
57
+ CardDescription.displayName = "CardDescription";
58
+
59
+ const CardContent = React.forwardRef<
60
+ HTMLDivElement,
61
+ React.HTMLAttributes<HTMLDivElement>
62
+ >(({ className, ...props }, ref) => (
63
+ <div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
64
+ ));
65
+ CardContent.displayName = "CardContent";
66
+
67
+ const CardFooter = React.forwardRef<
68
+ HTMLDivElement,
69
+ React.HTMLAttributes<HTMLDivElement>
70
+ >(({ className, ...props }, ref) => (
71
+ <div
72
+ ref={ref}
73
+ className={cn("flex items-center p-6 pt-0", className)}
74
+ {...props}
75
+ />
76
+ ));
77
+ CardFooter.displayName = "CardFooter";
78
+
79
+ export {
80
+ Card,
81
+ CardHeader,
82
+ CardFooter,
83
+ CardTitle,
84
+ CardDescription,
85
+ CardContent,
86
+ };
src/components/ui/carousel.tsx ADDED
@@ -0,0 +1,260 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import useEmblaCarousel, {
3
+ type UseEmblaCarouselType,
4
+ } from "embla-carousel-react";
5
+ import { ArrowLeft, ArrowRight } from "lucide-react";
6
+
7
+ import { cn } from "@/lib/utils";
8
+ import { Button } from "@/components/ui/button";
9
+
10
+ type CarouselApi = UseEmblaCarouselType[1];
11
+ type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
12
+ type CarouselOptions = UseCarouselParameters[0];
13
+ type CarouselPlugin = UseCarouselParameters[1];
14
+
15
+ type CarouselProps = {
16
+ opts?: CarouselOptions;
17
+ plugins?: CarouselPlugin;
18
+ orientation?: "horizontal" | "vertical";
19
+ setApi?: (api: CarouselApi) => void;
20
+ };
21
+
22
+ type CarouselContextProps = {
23
+ carouselRef: ReturnType<typeof useEmblaCarousel>[0];
24
+ api: ReturnType<typeof useEmblaCarousel>[1];
25
+ scrollPrev: () => void;
26
+ scrollNext: () => void;
27
+ canScrollPrev: boolean;
28
+ canScrollNext: boolean;
29
+ } & CarouselProps;
30
+
31
+ const CarouselContext = React.createContext<CarouselContextProps | null>(null);
32
+
33
+ function useCarousel() {
34
+ const context = React.useContext(CarouselContext);
35
+
36
+ if (!context) {
37
+ throw new Error("useCarousel must be used within a <Carousel />");
38
+ }
39
+
40
+ return context;
41
+ }
42
+
43
+ const Carousel = React.forwardRef<
44
+ HTMLDivElement,
45
+ React.HTMLAttributes<HTMLDivElement> & CarouselProps
46
+ >(
47
+ (
48
+ {
49
+ orientation = "horizontal",
50
+ opts,
51
+ setApi,
52
+ plugins,
53
+ className,
54
+ children,
55
+ ...props
56
+ },
57
+ ref,
58
+ ) => {
59
+ const [carouselRef, api] = useEmblaCarousel(
60
+ {
61
+ ...opts,
62
+ axis: orientation === "horizontal" ? "x" : "y",
63
+ },
64
+ plugins,
65
+ );
66
+ const [canScrollPrev, setCanScrollPrev] = React.useState(false);
67
+ const [canScrollNext, setCanScrollNext] = React.useState(false);
68
+
69
+ const onSelect = React.useCallback((api: CarouselApi) => {
70
+ if (!api) {
71
+ return;
72
+ }
73
+
74
+ setCanScrollPrev(api.canScrollPrev());
75
+ setCanScrollNext(api.canScrollNext());
76
+ }, []);
77
+
78
+ const scrollPrev = React.useCallback(() => {
79
+ api?.scrollPrev();
80
+ }, [api]);
81
+
82
+ const scrollNext = React.useCallback(() => {
83
+ api?.scrollNext();
84
+ }, [api]);
85
+
86
+ const handleKeyDown = React.useCallback(
87
+ (event: React.KeyboardEvent<HTMLDivElement>) => {
88
+ if (event.key === "ArrowLeft") {
89
+ event.preventDefault();
90
+ scrollPrev();
91
+ } else if (event.key === "ArrowRight") {
92
+ event.preventDefault();
93
+ scrollNext();
94
+ }
95
+ },
96
+ [scrollPrev, scrollNext],
97
+ );
98
+
99
+ React.useEffect(() => {
100
+ if (!api || !setApi) {
101
+ return;
102
+ }
103
+
104
+ setApi(api);
105
+ }, [api, setApi]);
106
+
107
+ React.useEffect(() => {
108
+ if (!api) {
109
+ return;
110
+ }
111
+
112
+ onSelect(api);
113
+ api.on("reInit", onSelect);
114
+ api.on("select", onSelect);
115
+
116
+ return () => {
117
+ api?.off("select", onSelect);
118
+ };
119
+ }, [api, onSelect]);
120
+
121
+ return (
122
+ <CarouselContext.Provider
123
+ value={{
124
+ carouselRef,
125
+ api: api,
126
+ opts,
127
+ orientation:
128
+ orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
129
+ scrollPrev,
130
+ scrollNext,
131
+ canScrollPrev,
132
+ canScrollNext,
133
+ }}
134
+ >
135
+ <div
136
+ ref={ref}
137
+ onKeyDownCapture={handleKeyDown}
138
+ className={cn("relative", className)}
139
+ role="region"
140
+ aria-roledescription="carousel"
141
+ {...props}
142
+ >
143
+ {children}
144
+ </div>
145
+ </CarouselContext.Provider>
146
+ );
147
+ },
148
+ );
149
+ Carousel.displayName = "Carousel";
150
+
151
+ const CarouselContent = React.forwardRef<
152
+ HTMLDivElement,
153
+ React.HTMLAttributes<HTMLDivElement>
154
+ >(({ className, ...props }, ref) => {
155
+ const { carouselRef, orientation } = useCarousel();
156
+
157
+ return (
158
+ <div ref={carouselRef} className="overflow-hidden">
159
+ <div
160
+ ref={ref}
161
+ className={cn(
162
+ "flex",
163
+ orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
164
+ className,
165
+ )}
166
+ {...props}
167
+ />
168
+ </div>
169
+ );
170
+ });
171
+ CarouselContent.displayName = "CarouselContent";
172
+
173
+ const CarouselItem = React.forwardRef<
174
+ HTMLDivElement,
175
+ React.HTMLAttributes<HTMLDivElement>
176
+ >(({ className, ...props }, ref) => {
177
+ const { orientation } = useCarousel();
178
+
179
+ return (
180
+ <div
181
+ ref={ref}
182
+ role="group"
183
+ aria-roledescription="slide"
184
+ className={cn(
185
+ "min-w-0 shrink-0 grow-0 basis-full",
186
+ orientation === "horizontal" ? "pl-4" : "pt-4",
187
+ className,
188
+ )}
189
+ {...props}
190
+ />
191
+ );
192
+ });
193
+ CarouselItem.displayName = "CarouselItem";
194
+
195
+ const CarouselPrevious = React.forwardRef<
196
+ HTMLButtonElement,
197
+ React.ComponentProps<typeof Button>
198
+ >(({ className, variant = "outline", size = "icon", ...props }, ref) => {
199
+ const { orientation, scrollPrev, canScrollPrev } = useCarousel();
200
+
201
+ return (
202
+ <Button
203
+ ref={ref}
204
+ variant={variant}
205
+ size={size}
206
+ className={cn(
207
+ "absolute h-8 w-8 rounded-full",
208
+ orientation === "horizontal"
209
+ ? "-left-12 top-1/2 -translate-y-1/2"
210
+ : "-top-12 left-1/2 -translate-x-1/2 rotate-90",
211
+ className,
212
+ )}
213
+ disabled={!canScrollPrev}
214
+ onClick={scrollPrev}
215
+ {...props}
216
+ >
217
+ <ArrowLeft className="h-4 w-4" />
218
+ <span className="sr-only">Previous slide</span>
219
+ </Button>
220
+ );
221
+ });
222
+ CarouselPrevious.displayName = "CarouselPrevious";
223
+
224
+ const CarouselNext = React.forwardRef<
225
+ HTMLButtonElement,
226
+ React.ComponentProps<typeof Button>
227
+ >(({ className, variant = "outline", size = "icon", ...props }, ref) => {
228
+ const { orientation, scrollNext, canScrollNext } = useCarousel();
229
+
230
+ return (
231
+ <Button
232
+ ref={ref}
233
+ variant={variant}
234
+ size={size}
235
+ className={cn(
236
+ "absolute h-8 w-8 rounded-full",
237
+ orientation === "horizontal"
238
+ ? "-right-12 top-1/2 -translate-y-1/2"
239
+ : "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
240
+ className,
241
+ )}
242
+ disabled={!canScrollNext}
243
+ onClick={scrollNext}
244
+ {...props}
245
+ >
246
+ <ArrowRight className="h-4 w-4" />
247
+ <span className="sr-only">Next slide</span>
248
+ </Button>
249
+ );
250
+ });
251
+ CarouselNext.displayName = "CarouselNext";
252
+
253
+ export {
254
+ type CarouselApi,
255
+ Carousel,
256
+ CarouselContent,
257
+ CarouselItem,
258
+ CarouselPrevious,
259
+ CarouselNext,
260
+ };
src/components/ui/chart.tsx ADDED
@@ -0,0 +1,363 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import * as RechartsPrimitive from "recharts";
3
+
4
+ import { cn } from "@/lib/utils";
5
+
6
+ // Format: { THEME_NAME: CSS_SELECTOR }
7
+ const THEMES = { light: "", dark: ".dark" } as const;
8
+
9
+ export type ChartConfig = {
10
+ [k in string]: {
11
+ label?: React.ReactNode;
12
+ icon?: React.ComponentType;
13
+ } & (
14
+ | { color?: string; theme?: never }
15
+ | { color?: never; theme: Record<keyof typeof THEMES, string> }
16
+ );
17
+ };
18
+
19
+ type ChartContextProps = {
20
+ config: ChartConfig;
21
+ };
22
+
23
+ const ChartContext = React.createContext<ChartContextProps | null>(null);
24
+
25
+ function useChart() {
26
+ const context = React.useContext(ChartContext);
27
+
28
+ if (!context) {
29
+ throw new Error("useChart must be used within a <ChartContainer />");
30
+ }
31
+
32
+ return context;
33
+ }
34
+
35
+ const ChartContainer = React.forwardRef<
36
+ HTMLDivElement,
37
+ React.ComponentProps<"div"> & {
38
+ config: ChartConfig;
39
+ children: React.ComponentProps<
40
+ typeof RechartsPrimitive.ResponsiveContainer
41
+ >["children"];
42
+ }
43
+ >(({ id, className, children, config, ...props }, ref) => {
44
+ const uniqueId = React.useId();
45
+ const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`;
46
+
47
+ return (
48
+ <ChartContext.Provider value={{ config }}>
49
+ <div
50
+ data-chart={chartId}
51
+ ref={ref}
52
+ className={cn(
53
+ "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",
54
+ className,
55
+ )}
56
+ {...props}
57
+ >
58
+ <ChartStyle id={chartId} config={config} />
59
+ <RechartsPrimitive.ResponsiveContainer>
60
+ {children}
61
+ </RechartsPrimitive.ResponsiveContainer>
62
+ </div>
63
+ </ChartContext.Provider>
64
+ );
65
+ });
66
+ ChartContainer.displayName = "Chart";
67
+
68
+ const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
69
+ const colorConfig = Object.entries(config).filter(
70
+ ([_, config]) => config.theme || config.color,
71
+ );
72
+
73
+ if (!colorConfig.length) {
74
+ return null;
75
+ }
76
+
77
+ return (
78
+ <style
79
+ dangerouslySetInnerHTML={{
80
+ __html: Object.entries(THEMES)
81
+ .map(
82
+ ([theme, prefix]) => `
83
+ ${prefix} [data-chart=${id}] {
84
+ ${colorConfig
85
+ .map(([key, itemConfig]) => {
86
+ const color =
87
+ itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
88
+ itemConfig.color;
89
+ return color ? ` --color-${key}: ${color};` : null;
90
+ })
91
+ .join("\n")}
92
+ }
93
+ `,
94
+ )
95
+ .join("\n"),
96
+ }}
97
+ />
98
+ );
99
+ };
100
+
101
+ const ChartTooltip = RechartsPrimitive.Tooltip;
102
+
103
+ const ChartTooltipContent = React.forwardRef<
104
+ HTMLDivElement,
105
+ React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
106
+ React.ComponentProps<"div"> & {
107
+ hideLabel?: boolean;
108
+ hideIndicator?: boolean;
109
+ indicator?: "line" | "dot" | "dashed";
110
+ nameKey?: string;
111
+ labelKey?: string;
112
+ }
113
+ >(
114
+ (
115
+ {
116
+ active,
117
+ payload,
118
+ className,
119
+ indicator = "dot",
120
+ hideLabel = false,
121
+ hideIndicator = false,
122
+ label,
123
+ labelFormatter,
124
+ labelClassName,
125
+ formatter,
126
+ color,
127
+ nameKey,
128
+ labelKey,
129
+ },
130
+ ref,
131
+ ) => {
132
+ const { config } = useChart();
133
+
134
+ const tooltipLabel = React.useMemo(() => {
135
+ if (hideLabel || !payload?.length) {
136
+ return null;
137
+ }
138
+
139
+ const [item] = payload;
140
+ const key = `${labelKey || item.dataKey || item.name || "value"}`;
141
+ const itemConfig = getPayloadConfigFromPayload(config, item, key);
142
+ const value =
143
+ !labelKey && typeof label === "string"
144
+ ? config[label as keyof typeof config]?.label || label
145
+ : itemConfig?.label;
146
+
147
+ if (labelFormatter) {
148
+ return (
149
+ <div className={cn("font-medium", labelClassName)}>
150
+ {labelFormatter(value, payload)}
151
+ </div>
152
+ );
153
+ }
154
+
155
+ if (!value) {
156
+ return null;
157
+ }
158
+
159
+ return <div className={cn("font-medium", labelClassName)}>{value}</div>;
160
+ }, [
161
+ label,
162
+ labelFormatter,
163
+ payload,
164
+ hideLabel,
165
+ labelClassName,
166
+ config,
167
+ labelKey,
168
+ ]);
169
+
170
+ if (!active || !payload?.length) {
171
+ return null;
172
+ }
173
+
174
+ const nestLabel = payload.length === 1 && indicator !== "dot";
175
+
176
+ return (
177
+ <div
178
+ ref={ref}
179
+ className={cn(
180
+ "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",
181
+ className,
182
+ )}
183
+ >
184
+ {!nestLabel ? tooltipLabel : null}
185
+ <div className="grid gap-1.5">
186
+ {payload.map((item, index) => {
187
+ const key = `${nameKey || item.name || item.dataKey || "value"}`;
188
+ const itemConfig = getPayloadConfigFromPayload(config, item, key);
189
+ const indicatorColor = color || item.payload.fill || item.color;
190
+
191
+ return (
192
+ <div
193
+ key={item.dataKey}
194
+ className={cn(
195
+ "flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
196
+ indicator === "dot" && "items-center",
197
+ )}
198
+ >
199
+ {formatter && item?.value !== undefined && item.name ? (
200
+ formatter(item.value, item.name, item, index, item.payload)
201
+ ) : (
202
+ <>
203
+ {itemConfig?.icon ? (
204
+ <itemConfig.icon />
205
+ ) : (
206
+ !hideIndicator && (
207
+ <div
208
+ className={cn(
209
+ "shrink-0 rounded-[2px] border-[--color-border] bg-[--color-bg]",
210
+ {
211
+ "h-2.5 w-2.5": indicator === "dot",
212
+ "w-1": indicator === "line",
213
+ "w-0 border-[1.5px] border-dashed bg-transparent":
214
+ indicator === "dashed",
215
+ "my-0.5": nestLabel && indicator === "dashed",
216
+ },
217
+ )}
218
+ style={
219
+ {
220
+ "--color-bg": indicatorColor,
221
+ "--color-border": indicatorColor,
222
+ } as React.CSSProperties
223
+ }
224
+ />
225
+ )
226
+ )}
227
+ <div
228
+ className={cn(
229
+ "flex flex-1 justify-between leading-none",
230
+ nestLabel ? "items-end" : "items-center",
231
+ )}
232
+ >
233
+ <div className="grid gap-1.5">
234
+ {nestLabel ? tooltipLabel : null}
235
+ <span className="text-muted-foreground">
236
+ {itemConfig?.label || item.name}
237
+ </span>
238
+ </div>
239
+ {item.value && (
240
+ <span className="font-mono font-medium tabular-nums text-foreground">
241
+ {item.value.toLocaleString()}
242
+ </span>
243
+ )}
244
+ </div>
245
+ </>
246
+ )}
247
+ </div>
248
+ );
249
+ })}
250
+ </div>
251
+ </div>
252
+ );
253
+ },
254
+ );
255
+ ChartTooltipContent.displayName = "ChartTooltip";
256
+
257
+ const ChartLegend = RechartsPrimitive.Legend;
258
+
259
+ const ChartLegendContent = React.forwardRef<
260
+ HTMLDivElement,
261
+ React.ComponentProps<"div"> &
262
+ Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
263
+ hideIcon?: boolean;
264
+ nameKey?: string;
265
+ }
266
+ >(
267
+ (
268
+ { className, hideIcon = false, payload, verticalAlign = "bottom", nameKey },
269
+ ref,
270
+ ) => {
271
+ const { config } = useChart();
272
+
273
+ if (!payload?.length) {
274
+ return null;
275
+ }
276
+
277
+ return (
278
+ <div
279
+ ref={ref}
280
+ className={cn(
281
+ "flex items-center justify-center gap-4",
282
+ verticalAlign === "top" ? "pb-3" : "pt-3",
283
+ className,
284
+ )}
285
+ >
286
+ {payload.map((item) => {
287
+ const key = `${nameKey || item.dataKey || "value"}`;
288
+ const itemConfig = getPayloadConfigFromPayload(config, item, key);
289
+
290
+ return (
291
+ <div
292
+ key={item.value}
293
+ className={cn(
294
+ "flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground",
295
+ )}
296
+ >
297
+ {itemConfig?.icon && !hideIcon ? (
298
+ <itemConfig.icon />
299
+ ) : (
300
+ <div
301
+ className="h-2 w-2 shrink-0 rounded-[2px]"
302
+ style={{
303
+ backgroundColor: item.color,
304
+ }}
305
+ />
306
+ )}
307
+ {itemConfig?.label}
308
+ </div>
309
+ );
310
+ })}
311
+ </div>
312
+ );
313
+ },
314
+ );
315
+ ChartLegendContent.displayName = "ChartLegend";
316
+
317
+ // Helper to extract item config from a payload.
318
+ function getPayloadConfigFromPayload(
319
+ config: ChartConfig,
320
+ payload: unknown,
321
+ key: string,
322
+ ) {
323
+ if (typeof payload !== "object" || payload === null) {
324
+ return undefined;
325
+ }
326
+
327
+ const payloadPayload =
328
+ "payload" in payload &&
329
+ typeof payload.payload === "object" &&
330
+ payload.payload !== null
331
+ ? payload.payload
332
+ : undefined;
333
+
334
+ let configLabelKey: string = key;
335
+
336
+ if (
337
+ key in payload &&
338
+ typeof payload[key as keyof typeof payload] === "string"
339
+ ) {
340
+ configLabelKey = payload[key as keyof typeof payload] as string;
341
+ } else if (
342
+ payloadPayload &&
343
+ key in payloadPayload &&
344
+ typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
345
+ ) {
346
+ configLabelKey = payloadPayload[
347
+ key as keyof typeof payloadPayload
348
+ ] as string;
349
+ }
350
+
351
+ return configLabelKey in config
352
+ ? config[configLabelKey]
353
+ : config[key as keyof typeof config];
354
+ }
355
+
356
+ export {
357
+ ChartContainer,
358
+ ChartTooltip,
359
+ ChartTooltipContent,
360
+ ChartLegend,
361
+ ChartLegendContent,
362
+ ChartStyle,
363
+ };
src/components/ui/checkbox.tsx ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
3
+ import { Check } from "lucide-react";
4
+
5
+ import { cn } from "@/lib/utils";
6
+
7
+ const Checkbox = React.forwardRef<
8
+ React.ElementRef<typeof CheckboxPrimitive.Root>,
9
+ React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
10
+ >(({ className, ...props }, ref) => (
11
+ <CheckboxPrimitive.Root
12
+ ref={ref}
13
+ className={cn(
14
+ "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",
15
+ className,
16
+ )}
17
+ {...props}
18
+ >
19
+ <CheckboxPrimitive.Indicator
20
+ className={cn("flex items-center justify-center text-current")}
21
+ >
22
+ <Check className="h-4 w-4" />
23
+ </CheckboxPrimitive.Indicator>
24
+ </CheckboxPrimitive.Root>
25
+ ));
26
+ Checkbox.displayName = CheckboxPrimitive.Root.displayName;
27
+
28
+ export { Checkbox };
src/components/ui/collapsible.tsx ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
2
+
3
+ const Collapsible = CollapsiblePrimitive.Root;
4
+
5
+ const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger;
6
+
7
+ const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent;
8
+
9
+ export { Collapsible, CollapsibleTrigger, CollapsibleContent };
src/components/ui/command.tsx ADDED
@@ -0,0 +1,153 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import { type DialogProps } from "@radix-ui/react-dialog";
3
+ import { Command as CommandPrimitive } from "cmdk";
4
+ import { Search } from "lucide-react";
5
+
6
+ import { cn } from "@/lib/utils";
7
+ import { Dialog, DialogContent } from "@/components/ui/dialog";
8
+
9
+ const Command = React.forwardRef<
10
+ React.ElementRef<typeof CommandPrimitive>,
11
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive>
12
+ >(({ className, ...props }, ref) => (
13
+ <CommandPrimitive
14
+ ref={ref}
15
+ className={cn(
16
+ "flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
17
+ className,
18
+ )}
19
+ {...props}
20
+ />
21
+ ));
22
+ Command.displayName = CommandPrimitive.displayName;
23
+
24
+ interface CommandDialogProps extends DialogProps {}
25
+
26
+ const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
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 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",
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
+ };
src/components/ui/context-menu.tsx ADDED
@@ -0,0 +1,198 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import * as ContextMenuPrimitive from "@radix-ui/react-context-menu";
3
+ import { Check, ChevronRight, Circle } from "lucide-react";
4
+
5
+ import { cn } from "@/lib/utils";
6
+
7
+ const ContextMenu = ContextMenuPrimitive.Root;
8
+
9
+ const ContextMenuTrigger = ContextMenuPrimitive.Trigger;
10
+
11
+ const ContextMenuGroup = ContextMenuPrimitive.Group;
12
+
13
+ const ContextMenuPortal = ContextMenuPrimitive.Portal;
14
+
15
+ const ContextMenuSub = ContextMenuPrimitive.Sub;
16
+
17
+ const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup;
18
+
19
+ const ContextMenuSubTrigger = React.forwardRef<
20
+ React.ElementRef<typeof ContextMenuPrimitive.SubTrigger>,
21
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger> & {
22
+ inset?: boolean;
23
+ }
24
+ >(({ className, inset, children, ...props }, ref) => (
25
+ <ContextMenuPrimitive.SubTrigger
26
+ ref={ref}
27
+ className={cn(
28
+ "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",
29
+ inset && "pl-8",
30
+ className,
31
+ )}
32
+ {...props}
33
+ >
34
+ {children}
35
+ <ChevronRight className="ml-auto h-4 w-4" />
36
+ </ContextMenuPrimitive.SubTrigger>
37
+ ));
38
+ ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName;
39
+
40
+ const ContextMenuSubContent = React.forwardRef<
41
+ React.ElementRef<typeof ContextMenuPrimitive.SubContent>,
42
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubContent>
43
+ >(({ className, ...props }, ref) => (
44
+ <ContextMenuPrimitive.SubContent
45
+ ref={ref}
46
+ className={cn(
47
+ "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",
48
+ className,
49
+ )}
50
+ {...props}
51
+ />
52
+ ));
53
+ ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName;
54
+
55
+ const ContextMenuContent = React.forwardRef<
56
+ React.ElementRef<typeof ContextMenuPrimitive.Content>,
57
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content>
58
+ >(({ className, ...props }, ref) => (
59
+ <ContextMenuPrimitive.Portal>
60
+ <ContextMenuPrimitive.Content
61
+ ref={ref}
62
+ className={cn(
63
+ "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",
64
+ className,
65
+ )}
66
+ {...props}
67
+ />
68
+ </ContextMenuPrimitive.Portal>
69
+ ));
70
+ ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName;
71
+
72
+ const ContextMenuItem = React.forwardRef<
73
+ React.ElementRef<typeof ContextMenuPrimitive.Item>,
74
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & {
75
+ inset?: boolean;
76
+ }
77
+ >(({ className, inset, ...props }, ref) => (
78
+ <ContextMenuPrimitive.Item
79
+ ref={ref}
80
+ className={cn(
81
+ "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",
82
+ inset && "pl-8",
83
+ className,
84
+ )}
85
+ {...props}
86
+ />
87
+ ));
88
+ ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName;
89
+
90
+ const ContextMenuCheckboxItem = React.forwardRef<
91
+ React.ElementRef<typeof ContextMenuPrimitive.CheckboxItem>,
92
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.CheckboxItem>
93
+ >(({ className, children, checked, ...props }, ref) => (
94
+ <ContextMenuPrimitive.CheckboxItem
95
+ ref={ref}
96
+ className={cn(
97
+ "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",
98
+ className,
99
+ )}
100
+ checked={checked}
101
+ {...props}
102
+ >
103
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
104
+ <ContextMenuPrimitive.ItemIndicator>
105
+ <Check className="h-4 w-4" />
106
+ </ContextMenuPrimitive.ItemIndicator>
107
+ </span>
108
+ {children}
109
+ </ContextMenuPrimitive.CheckboxItem>
110
+ ));
111
+ ContextMenuCheckboxItem.displayName =
112
+ ContextMenuPrimitive.CheckboxItem.displayName;
113
+
114
+ const ContextMenuRadioItem = React.forwardRef<
115
+ React.ElementRef<typeof ContextMenuPrimitive.RadioItem>,
116
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.RadioItem>
117
+ >(({ className, children, ...props }, ref) => (
118
+ <ContextMenuPrimitive.RadioItem
119
+ ref={ref}
120
+ className={cn(
121
+ "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",
122
+ className,
123
+ )}
124
+ {...props}
125
+ >
126
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
127
+ <ContextMenuPrimitive.ItemIndicator>
128
+ <Circle className="h-2 w-2 fill-current" />
129
+ </ContextMenuPrimitive.ItemIndicator>
130
+ </span>
131
+ {children}
132
+ </ContextMenuPrimitive.RadioItem>
133
+ ));
134
+ ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName;
135
+
136
+ const ContextMenuLabel = React.forwardRef<
137
+ React.ElementRef<typeof ContextMenuPrimitive.Label>,
138
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label> & {
139
+ inset?: boolean;
140
+ }
141
+ >(({ className, inset, ...props }, ref) => (
142
+ <ContextMenuPrimitive.Label
143
+ ref={ref}
144
+ className={cn(
145
+ "px-2 py-1.5 text-sm font-semibold text-foreground",
146
+ inset && "pl-8",
147
+ className,
148
+ )}
149
+ {...props}
150
+ />
151
+ ));
152
+ ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName;
153
+
154
+ const ContextMenuSeparator = React.forwardRef<
155
+ React.ElementRef<typeof ContextMenuPrimitive.Separator>,
156
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Separator>
157
+ >(({ className, ...props }, ref) => (
158
+ <ContextMenuPrimitive.Separator
159
+ ref={ref}
160
+ className={cn("-mx-1 my-1 h-px bg-border", className)}
161
+ {...props}
162
+ />
163
+ ));
164
+ ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName;
165
+
166
+ const ContextMenuShortcut = ({
167
+ className,
168
+ ...props
169
+ }: React.HTMLAttributes<HTMLSpanElement>) => {
170
+ return (
171
+ <span
172
+ className={cn(
173
+ "ml-auto text-xs tracking-widest text-muted-foreground",
174
+ className,
175
+ )}
176
+ {...props}
177
+ />
178
+ );
179
+ };
180
+ ContextMenuShortcut.displayName = "ContextMenuShortcut";
181
+
182
+ export {
183
+ ContextMenu,
184
+ ContextMenuTrigger,
185
+ ContextMenuContent,
186
+ ContextMenuItem,
187
+ ContextMenuCheckboxItem,
188
+ ContextMenuRadioItem,
189
+ ContextMenuLabel,
190
+ ContextMenuSeparator,
191
+ ContextMenuShortcut,
192
+ ContextMenuGroup,
193
+ ContextMenuPortal,
194
+ ContextMenuSub,
195
+ ContextMenuSubContent,
196
+ ContextMenuSubTrigger,
197
+ ContextMenuRadioGroup,
198
+ };
src/components/ui/dialog.tsx ADDED
@@ -0,0 +1,120 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import * as DialogPrimitive from "@radix-ui/react-dialog";
3
+ import { X } from "lucide-react";
4
+
5
+ import { cn } from "@/lib/utils";
6
+
7
+ const Dialog = DialogPrimitive.Root;
8
+
9
+ const DialogTrigger = DialogPrimitive.Trigger;
10
+
11
+ const DialogPortal = DialogPrimitive.Portal;
12
+
13
+ const DialogClose = DialogPrimitive.Close;
14
+
15
+ const DialogOverlay = React.forwardRef<
16
+ React.ElementRef<typeof DialogPrimitive.Overlay>,
17
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
18
+ >(({ className, ...props }, ref) => (
19
+ <DialogPrimitive.Overlay
20
+ ref={ref}
21
+ className={cn(
22
+ "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",
23
+ className,
24
+ )}
25
+ {...props}
26
+ />
27
+ ));
28
+ DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
29
+
30
+ const DialogContent = React.forwardRef<
31
+ React.ElementRef<typeof DialogPrimitive.Content>,
32
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
33
+ >(({ className, children, ...props }, ref) => (
34
+ <DialogPortal>
35
+ <DialogOverlay />
36
+ <DialogPrimitive.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
+ {children}
45
+ <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">
46
+ <X className="h-4 w-4" />
47
+ <span className="sr-only">Close</span>
48
+ </DialogPrimitive.Close>
49
+ </DialogPrimitive.Content>
50
+ </DialogPortal>
51
+ ));
52
+ DialogContent.displayName = DialogPrimitive.Content.displayName;
53
+
54
+ const DialogHeader = ({
55
+ className,
56
+ ...props
57
+ }: React.HTMLAttributes<HTMLDivElement>) => (
58
+ <div
59
+ className={cn(
60
+ "flex flex-col space-y-1.5 text-center sm:text-left",
61
+ className,
62
+ )}
63
+ {...props}
64
+ />
65
+ );
66
+ DialogHeader.displayName = "DialogHeader";
67
+
68
+ const DialogFooter = ({
69
+ className,
70
+ ...props
71
+ }: React.HTMLAttributes<HTMLDivElement>) => (
72
+ <div
73
+ className={cn(
74
+ "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
75
+ className,
76
+ )}
77
+ {...props}
78
+ />
79
+ );
80
+ DialogFooter.displayName = "DialogFooter";
81
+
82
+ const DialogTitle = React.forwardRef<
83
+ React.ElementRef<typeof DialogPrimitive.Title>,
84
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
85
+ >(({ className, ...props }, ref) => (
86
+ <DialogPrimitive.Title
87
+ ref={ref}
88
+ className={cn(
89
+ "text-lg font-semibold leading-none tracking-tight",
90
+ className,
91
+ )}
92
+ {...props}
93
+ />
94
+ ));
95
+ DialogTitle.displayName = DialogPrimitive.Title.displayName;
96
+
97
+ const DialogDescription = React.forwardRef<
98
+ React.ElementRef<typeof DialogPrimitive.Description>,
99
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
100
+ >(({ className, ...props }, ref) => (
101
+ <DialogPrimitive.Description
102
+ ref={ref}
103
+ className={cn("text-sm text-muted-foreground", className)}
104
+ {...props}
105
+ />
106
+ ));
107
+ DialogDescription.displayName = DialogPrimitive.Description.displayName;
108
+
109
+ export {
110
+ Dialog,
111
+ DialogPortal,
112
+ DialogOverlay,
113
+ DialogClose,
114
+ DialogTrigger,
115
+ DialogContent,
116
+ DialogHeader,
117
+ DialogFooter,
118
+ DialogTitle,
119
+ DialogDescription,
120
+ };
src/components/ui/drawer.tsx ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import { Drawer as DrawerPrimitive } from "vaul";
3
+
4
+ import { cn } from "@/lib/utils";
5
+
6
+ const Drawer = ({
7
+ shouldScaleBackground = true,
8
+ ...props
9
+ }: React.ComponentProps<typeof DrawerPrimitive.Root>) => (
10
+ <DrawerPrimitive.Root
11
+ shouldScaleBackground={shouldScaleBackground}
12
+ {...props}
13
+ />
14
+ );
15
+ Drawer.displayName = "Drawer";
16
+
17
+ const DrawerTrigger = DrawerPrimitive.Trigger;
18
+
19
+ const DrawerPortal = DrawerPrimitive.Portal;
20
+
21
+ const DrawerClose = DrawerPrimitive.Close;
22
+
23
+ const DrawerOverlay = React.forwardRef<
24
+ React.ElementRef<typeof DrawerPrimitive.Overlay>,
25
+ React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay>
26
+ >(({ className, ...props }, ref) => (
27
+ <DrawerPrimitive.Overlay
28
+ ref={ref}
29
+ className={cn("fixed inset-0 z-50 bg-black/80", className)}
30
+ {...props}
31
+ />
32
+ ));
33
+ DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName;
34
+
35
+ const DrawerContent = React.forwardRef<
36
+ React.ElementRef<typeof DrawerPrimitive.Content>,
37
+ React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content>
38
+ >(({ className, children, ...props }, ref) => (
39
+ <DrawerPortal>
40
+ <DrawerOverlay />
41
+ <DrawerPrimitive.Content
42
+ ref={ref}
43
+ className={cn(
44
+ "fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background",
45
+ className,
46
+ )}
47
+ {...props}
48
+ >
49
+ <div className="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" />
50
+ {children}
51
+ </DrawerPrimitive.Content>
52
+ </DrawerPortal>
53
+ ));
54
+ DrawerContent.displayName = "DrawerContent";
55
+
56
+ const DrawerHeader = ({
57
+ className,
58
+ ...props
59
+ }: React.HTMLAttributes<HTMLDivElement>) => (
60
+ <div
61
+ className={cn("grid gap-1.5 p-4 text-center sm:text-left", className)}
62
+ {...props}
63
+ />
64
+ );
65
+ DrawerHeader.displayName = "DrawerHeader";
66
+
67
+ const DrawerFooter = ({
68
+ className,
69
+ ...props
70
+ }: React.HTMLAttributes<HTMLDivElement>) => (
71
+ <div
72
+ className={cn("mt-auto flex flex-col gap-2 p-4", className)}
73
+ {...props}
74
+ />
75
+ );
76
+ DrawerFooter.displayName = "DrawerFooter";
77
+
78
+ const DrawerTitle = React.forwardRef<
79
+ React.ElementRef<typeof DrawerPrimitive.Title>,
80
+ React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title>
81
+ >(({ className, ...props }, ref) => (
82
+ <DrawerPrimitive.Title
83
+ ref={ref}
84
+ className={cn(
85
+ "text-lg font-semibold leading-none tracking-tight",
86
+ className,
87
+ )}
88
+ {...props}
89
+ />
90
+ ));
91
+ DrawerTitle.displayName = DrawerPrimitive.Title.displayName;
92
+
93
+ const DrawerDescription = React.forwardRef<
94
+ React.ElementRef<typeof DrawerPrimitive.Description>,
95
+ React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description>
96
+ >(({ className, ...props }, ref) => (
97
+ <DrawerPrimitive.Description
98
+ ref={ref}
99
+ className={cn("text-sm text-muted-foreground", className)}
100
+ {...props}
101
+ />
102
+ ));
103
+ DrawerDescription.displayName = DrawerPrimitive.Description.displayName;
104
+
105
+ export {
106
+ Drawer,
107
+ DrawerPortal,
108
+ DrawerOverlay,
109
+ DrawerTrigger,
110
+ DrawerClose,
111
+ DrawerContent,
112
+ DrawerHeader,
113
+ DrawerFooter,
114
+ DrawerTitle,
115
+ DrawerDescription,
116
+ };
src/components/ui/dropdown-menu.tsx ADDED
@@ -0,0 +1,198 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
3
+ import { Check, ChevronRight, Circle } from "lucide-react";
4
+
5
+ import { cn } from "@/lib/utils";
6
+
7
+ const DropdownMenu = DropdownMenuPrimitive.Root;
8
+
9
+ const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
10
+
11
+ const DropdownMenuGroup = DropdownMenuPrimitive.Group;
12
+
13
+ const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
14
+
15
+ const DropdownMenuSub = DropdownMenuPrimitive.Sub;
16
+
17
+ const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
18
+
19
+ const DropdownMenuSubTrigger = React.forwardRef<
20
+ React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
21
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
22
+ inset?: boolean;
23
+ }
24
+ >(({ className, inset, children, ...props }, ref) => (
25
+ <DropdownMenuPrimitive.SubTrigger
26
+ ref={ref}
27
+ className={cn(
28
+ "flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
29
+ inset && "pl-8",
30
+ className,
31
+ )}
32
+ {...props}
33
+ >
34
+ {children}
35
+ <ChevronRight className="ml-auto h-4 w-4" />
36
+ </DropdownMenuPrimitive.SubTrigger>
37
+ ));
38
+ DropdownMenuSubTrigger.displayName =
39
+ DropdownMenuPrimitive.SubTrigger.displayName;
40
+
41
+ const DropdownMenuSubContent = React.forwardRef<
42
+ React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
43
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
44
+ >(({ className, ...props }, ref) => (
45
+ <DropdownMenuPrimitive.SubContent
46
+ ref={ref}
47
+ className={cn(
48
+ "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",
49
+ className,
50
+ )}
51
+ {...props}
52
+ />
53
+ ));
54
+ DropdownMenuSubContent.displayName =
55
+ DropdownMenuPrimitive.SubContent.displayName;
56
+
57
+ const DropdownMenuContent = React.forwardRef<
58
+ React.ElementRef<typeof DropdownMenuPrimitive.Content>,
59
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
60
+ >(({ className, sideOffset = 4, ...props }, ref) => (
61
+ <DropdownMenuPrimitive.Portal>
62
+ <DropdownMenuPrimitive.Content
63
+ ref={ref}
64
+ sideOffset={sideOffset}
65
+ className={cn(
66
+ "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",
67
+ className,
68
+ )}
69
+ {...props}
70
+ />
71
+ </DropdownMenuPrimitive.Portal>
72
+ ));
73
+ DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
74
+
75
+ const DropdownMenuItem = React.forwardRef<
76
+ React.ElementRef<typeof DropdownMenuPrimitive.Item>,
77
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
78
+ inset?: boolean;
79
+ }
80
+ >(({ className, inset, ...props }, ref) => (
81
+ <DropdownMenuPrimitive.Item
82
+ ref={ref}
83
+ className={cn(
84
+ "relative flex cursor-default select-none items-center 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",
85
+ inset && "pl-8",
86
+ className,
87
+ )}
88
+ {...props}
89
+ />
90
+ ));
91
+ DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
92
+
93
+ const DropdownMenuCheckboxItem = React.forwardRef<
94
+ React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
95
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
96
+ >(({ className, children, checked, ...props }, ref) => (
97
+ <DropdownMenuPrimitive.CheckboxItem
98
+ ref={ref}
99
+ className={cn(
100
+ "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",
101
+ className,
102
+ )}
103
+ checked={checked}
104
+ {...props}
105
+ >
106
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
107
+ <DropdownMenuPrimitive.ItemIndicator>
108
+ <Check className="h-4 w-4" />
109
+ </DropdownMenuPrimitive.ItemIndicator>
110
+ </span>
111
+ {children}
112
+ </DropdownMenuPrimitive.CheckboxItem>
113
+ ));
114
+ DropdownMenuCheckboxItem.displayName =
115
+ DropdownMenuPrimitive.CheckboxItem.displayName;
116
+
117
+ const DropdownMenuRadioItem = React.forwardRef<
118
+ React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
119
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
120
+ >(({ className, children, ...props }, ref) => (
121
+ <DropdownMenuPrimitive.RadioItem
122
+ ref={ref}
123
+ className={cn(
124
+ "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",
125
+ className,
126
+ )}
127
+ {...props}
128
+ >
129
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
130
+ <DropdownMenuPrimitive.ItemIndicator>
131
+ <Circle className="h-2 w-2 fill-current" />
132
+ </DropdownMenuPrimitive.ItemIndicator>
133
+ </span>
134
+ {children}
135
+ </DropdownMenuPrimitive.RadioItem>
136
+ ));
137
+ DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
138
+
139
+ const DropdownMenuLabel = React.forwardRef<
140
+ React.ElementRef<typeof DropdownMenuPrimitive.Label>,
141
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
142
+ inset?: boolean;
143
+ }
144
+ >(({ className, inset, ...props }, ref) => (
145
+ <DropdownMenuPrimitive.Label
146
+ ref={ref}
147
+ className={cn(
148
+ "px-2 py-1.5 text-sm font-semibold",
149
+ inset && "pl-8",
150
+ className,
151
+ )}
152
+ {...props}
153
+ />
154
+ ));
155
+ DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
156
+
157
+ const DropdownMenuSeparator = React.forwardRef<
158
+ React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
159
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
160
+ >(({ className, ...props }, ref) => (
161
+ <DropdownMenuPrimitive.Separator
162
+ ref={ref}
163
+ className={cn("-mx-1 my-1 h-px bg-muted", className)}
164
+ {...props}
165
+ />
166
+ ));
167
+ DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
168
+
169
+ const DropdownMenuShortcut = ({
170
+ className,
171
+ ...props
172
+ }: React.HTMLAttributes<HTMLSpanElement>) => {
173
+ return (
174
+ <span
175
+ className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
176
+ {...props}
177
+ />
178
+ );
179
+ };
180
+ DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
181
+
182
+ export {
183
+ DropdownMenu,
184
+ DropdownMenuTrigger,
185
+ DropdownMenuContent,
186
+ DropdownMenuItem,
187
+ DropdownMenuCheckboxItem,
188
+ DropdownMenuRadioItem,
189
+ DropdownMenuLabel,
190
+ DropdownMenuSeparator,
191
+ DropdownMenuShortcut,
192
+ DropdownMenuGroup,
193
+ DropdownMenuPortal,
194
+ DropdownMenuSub,
195
+ DropdownMenuSubContent,
196
+ DropdownMenuSubTrigger,
197
+ DropdownMenuRadioGroup,
198
+ };
src/components/ui/form.tsx ADDED
@@ -0,0 +1,177 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import * as LabelPrimitive from "@radix-ui/react-label";
3
+ import { Slot } from "@radix-ui/react-slot";
4
+ import {
5
+ Controller,
6
+ ControllerProps,
7
+ FieldPath,
8
+ FieldValues,
9
+ FormProvider,
10
+ useFormContext,
11
+ } from "react-hook-form";
12
+
13
+ import { cn } from "@/lib/utils";
14
+ import { Label } from "@/components/ui/label";
15
+
16
+ const Form = FormProvider;
17
+
18
+ type FormFieldContextValue<
19
+ TFieldValues extends FieldValues = FieldValues,
20
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
21
+ > = {
22
+ name: TName;
23
+ };
24
+
25
+ const FormFieldContext = React.createContext<FormFieldContextValue>(
26
+ {} as FormFieldContextValue,
27
+ );
28
+
29
+ const FormField = <
30
+ TFieldValues extends FieldValues = FieldValues,
31
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
32
+ >({
33
+ ...props
34
+ }: ControllerProps<TFieldValues, TName>) => {
35
+ return (
36
+ <FormFieldContext.Provider value={{ name: props.name }}>
37
+ <Controller {...props} />
38
+ </FormFieldContext.Provider>
39
+ );
40
+ };
41
+
42
+ const useFormField = () => {
43
+ const fieldContext = React.useContext(FormFieldContext);
44
+ const itemContext = React.useContext(FormItemContext);
45
+ const { getFieldState, formState } = useFormContext();
46
+
47
+ const fieldState = getFieldState(fieldContext.name, formState);
48
+
49
+ if (!fieldContext) {
50
+ throw new Error("useFormField should be used within <FormField>");
51
+ }
52
+
53
+ const { id } = itemContext;
54
+
55
+ return {
56
+ id,
57
+ name: fieldContext.name,
58
+ formItemId: `${id}-form-item`,
59
+ formDescriptionId: `${id}-form-item-description`,
60
+ formMessageId: `${id}-form-item-message`,
61
+ ...fieldState,
62
+ };
63
+ };
64
+
65
+ type FormItemContextValue = {
66
+ id: string;
67
+ };
68
+
69
+ const FormItemContext = React.createContext<FormItemContextValue>(
70
+ {} as FormItemContextValue,
71
+ );
72
+
73
+ const FormItem = React.forwardRef<
74
+ HTMLDivElement,
75
+ React.HTMLAttributes<HTMLDivElement>
76
+ >(({ className, ...props }, ref) => {
77
+ const id = React.useId();
78
+
79
+ return (
80
+ <FormItemContext.Provider value={{ id }}>
81
+ <div ref={ref} className={cn("space-y-2", className)} {...props} />
82
+ </FormItemContext.Provider>
83
+ );
84
+ });
85
+ FormItem.displayName = "FormItem";
86
+
87
+ const FormLabel = React.forwardRef<
88
+ React.ElementRef<typeof LabelPrimitive.Root>,
89
+ React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
90
+ >(({ className, ...props }, ref) => {
91
+ const { error, formItemId } = useFormField();
92
+
93
+ return (
94
+ <Label
95
+ ref={ref}
96
+ className={cn(error && "text-destructive", className)}
97
+ htmlFor={formItemId}
98
+ {...props}
99
+ />
100
+ );
101
+ });
102
+ FormLabel.displayName = "FormLabel";
103
+
104
+ const FormControl = React.forwardRef<
105
+ React.ElementRef<typeof Slot>,
106
+ React.ComponentPropsWithoutRef<typeof Slot>
107
+ >(({ ...props }, ref) => {
108
+ const { error, formItemId, formDescriptionId, formMessageId } =
109
+ useFormField();
110
+
111
+ return (
112
+ <Slot
113
+ ref={ref}
114
+ id={formItemId}
115
+ aria-describedby={
116
+ !error
117
+ ? `${formDescriptionId}`
118
+ : `${formDescriptionId} ${formMessageId}`
119
+ }
120
+ aria-invalid={!!error}
121
+ {...props}
122
+ />
123
+ );
124
+ });
125
+ FormControl.displayName = "FormControl";
126
+
127
+ const FormDescription = React.forwardRef<
128
+ HTMLParagraphElement,
129
+ React.HTMLAttributes<HTMLParagraphElement>
130
+ >(({ className, ...props }, ref) => {
131
+ const { formDescriptionId } = useFormField();
132
+
133
+ return (
134
+ <p
135
+ ref={ref}
136
+ id={formDescriptionId}
137
+ className={cn("text-sm text-muted-foreground", className)}
138
+ {...props}
139
+ />
140
+ );
141
+ });
142
+ FormDescription.displayName = "FormDescription";
143
+
144
+ const FormMessage = React.forwardRef<
145
+ HTMLParagraphElement,
146
+ React.HTMLAttributes<HTMLParagraphElement>
147
+ >(({ className, children, ...props }, ref) => {
148
+ const { error, formMessageId } = useFormField();
149
+ const body = error ? String(error?.message) : children;
150
+
151
+ if (!body) {
152
+ return null;
153
+ }
154
+
155
+ return (
156
+ <p
157
+ ref={ref}
158
+ id={formMessageId}
159
+ className={cn("text-sm font-medium text-destructive", className)}
160
+ {...props}
161
+ >
162
+ {body}
163
+ </p>
164
+ );
165
+ });
166
+ FormMessage.displayName = "FormMessage";
167
+
168
+ export {
169
+ useFormField,
170
+ Form,
171
+ FormItem,
172
+ FormLabel,
173
+ FormControl,
174
+ FormDescription,
175
+ FormMessage,
176
+ FormField,
177
+ };
src/components/ui/hover-card.tsx ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import * as HoverCardPrimitive from "@radix-ui/react-hover-card";
3
+
4
+ import { cn } from "@/lib/utils";
5
+
6
+ const HoverCard = HoverCardPrimitive.Root;
7
+
8
+ const HoverCardTrigger = HoverCardPrimitive.Trigger;
9
+
10
+ const HoverCardContent = React.forwardRef<
11
+ React.ElementRef<typeof HoverCardPrimitive.Content>,
12
+ React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content>
13
+ >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
14
+ <HoverCardPrimitive.Content
15
+ ref={ref}
16
+ align={align}
17
+ sideOffset={sideOffset}
18
+ className={cn(
19
+ "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",
20
+ className,
21
+ )}
22
+ {...props}
23
+ />
24
+ ));
25
+ HoverCardContent.displayName = HoverCardPrimitive.Content.displayName;
26
+
27
+ export { HoverCard, HoverCardTrigger, HoverCardContent };
src/components/ui/input-otp.tsx ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import { OTPInput, OTPInputContext } from "input-otp";
3
+ import { Dot } from "lucide-react";
4
+
5
+ import { cn } from "@/lib/utils";
6
+
7
+ const InputOTP = React.forwardRef<
8
+ React.ElementRef<typeof OTPInput>,
9
+ React.ComponentPropsWithoutRef<typeof OTPInput>
10
+ >(({ className, containerClassName, ...props }, ref) => (
11
+ <OTPInput
12
+ ref={ref}
13
+ containerClassName={cn(
14
+ "flex items-center gap-2 has-[:disabled]:opacity-50",
15
+ containerClassName,
16
+ )}
17
+ className={cn("disabled:cursor-not-allowed", className)}
18
+ {...props}
19
+ />
20
+ ));
21
+ InputOTP.displayName = "InputOTP";
22
+
23
+ const InputOTPGroup = React.forwardRef<
24
+ React.ElementRef<"div">,
25
+ React.ComponentPropsWithoutRef<"div">
26
+ >(({ className, ...props }, ref) => (
27
+ <div ref={ref} className={cn("flex items-center", className)} {...props} />
28
+ ));
29
+ InputOTPGroup.displayName = "InputOTPGroup";
30
+
31
+ const InputOTPSlot = React.forwardRef<
32
+ React.ElementRef<"div">,
33
+ React.ComponentPropsWithoutRef<"div"> & { index: number }
34
+ >(({ index, className, ...props }, ref) => {
35
+ const inputOTPContext = React.useContext(OTPInputContext);
36
+ const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index];
37
+
38
+ return (
39
+ <div
40
+ ref={ref}
41
+ className={cn(
42
+ "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",
43
+ isActive && "z-10 ring-2 ring-ring ring-offset-background",
44
+ className,
45
+ )}
46
+ {...props}
47
+ >
48
+ {char}
49
+ {hasFakeCaret && (
50
+ <div className="pointer-events-none absolute inset-0 flex items-center justify-center">
51
+ <div className="h-4 w-px animate-caret-blink bg-foreground duration-1000" />
52
+ </div>
53
+ )}
54
+ </div>
55
+ );
56
+ });
57
+ InputOTPSlot.displayName = "InputOTPSlot";
58
+
59
+ const InputOTPSeparator = React.forwardRef<
60
+ React.ElementRef<"div">,
61
+ React.ComponentPropsWithoutRef<"div">
62
+ >(({ ...props }, ref) => (
63
+ <div ref={ref} role="separator" {...props}>
64
+ <Dot />
65
+ </div>
66
+ ));
67
+ InputOTPSeparator.displayName = "InputOTPSeparator";
68
+
69
+ export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator };