Spaces:
Running
Running
[dyad] commited on
Commit ·
a27839e
0
Parent(s):
Init Dyad app
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .env +4 -0
- .env.example +3 -0
- .gitattributes +35 -0
- .gitignore +24 -0
- AI_RULES.md +19 -0
- Dockerfile +23 -0
- LICENSE +38 -0
- README.md +56 -0
- components.json +20 -0
- eslint.config.js +29 -0
- index.html +17 -0
- package-lock.json +0 -0
- package.json +97 -0
- pnpm-lock.yaml +0 -0
- postcss.config.js +6 -0
- public/front-pilot.svg +1 -0
- src/App.css +42 -0
- src/App.tsx +37 -0
- src/components/AnimatedBackground.tsx +51 -0
- src/components/ChatInterface.tsx +197 -0
- src/components/CodeEditor.tsx +81 -0
- src/components/DiffReviewModal.tsx +131 -0
- src/components/PreviewPanel.tsx +81 -0
- src/components/ProjectHistory.tsx +117 -0
- src/components/VersionsPanel.tsx +115 -0
- src/components/made-with-dyad.tsx +9 -0
- src/components/theme-provider.tsx +9 -0
- src/components/theme-toggle.tsx +40 -0
- src/components/ui/accordion.tsx +56 -0
- src/components/ui/alert-dialog.tsx +139 -0
- src/components/ui/alert.tsx +59 -0
- src/components/ui/aspect-ratio.tsx +5 -0
- src/components/ui/avatar.tsx +48 -0
- src/components/ui/badge.tsx +36 -0
- src/components/ui/breadcrumb.tsx +115 -0
- src/components/ui/button.tsx +56 -0
- src/components/ui/calendar.tsx +64 -0
- src/components/ui/card.tsx +86 -0
- src/components/ui/carousel.tsx +260 -0
- src/components/ui/chart.tsx +363 -0
- src/components/ui/checkbox.tsx +28 -0
- src/components/ui/collapsible.tsx +9 -0
- src/components/ui/command.tsx +153 -0
- src/components/ui/context-menu.tsx +198 -0
- src/components/ui/dialog.tsx +120 -0
- src/components/ui/drawer.tsx +116 -0
- src/components/ui/dropdown-menu.tsx +198 -0
- src/components/ui/form.tsx +177 -0
- src/components/ui/hover-card.tsx +27 -0
- 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)
|
| 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 |
+
© 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 };
|