Upload 100 files
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .gitattributes +1 -0
- LICENSE +21 -0
- README.md +92 -11
- client/apple-touch-icon.png +0 -0
- client/favicon.png +0 -0
- client/favicon.svg +12 -0
- client/index.html +49 -0
- client/manifest.json +28 -0
- client/og-image.png +0 -0
- client/public/apple-touch-icon.png +0 -0
- client/public/favicon.png +0 -0
- client/public/manifest.json +28 -0
- client/public/pwa-512x512.png +0 -0
- client/public/splash/apple-splash-1125-2436.jpg +0 -0
- client/public/splash/apple-splash-1170-2532.jpg +0 -0
- client/public/splash/apple-splash-1179-2556.jpg +0 -0
- client/public/splash/apple-splash-1290-2796.jpg +0 -0
- client/public/splash/apple-splash-1536-2048.jpg +0 -0
- client/public/splash/apple-splash-1668-2388.jpg +0 -0
- client/public/splash/apple-splash-2048-2732.jpg +0 -0
- client/src/App.tsx +41 -0
- client/src/components/FollowUpInput.tsx +66 -0
- client/src/components/Logo.tsx +130 -0
- client/src/components/SearchInput.tsx +75 -0
- client/src/components/SearchResults.tsx +132 -0
- client/src/components/SourceList.tsx +75 -0
- client/src/components/SourcePreviewModal.tsx +67 -0
- client/src/components/ThemeToggle.tsx +34 -0
- client/src/components/ui/accordion.tsx +56 -0
- client/src/components/ui/alert-dialog.tsx +139 -0
- client/src/components/ui/alert.tsx +59 -0
- client/src/components/ui/aspect-ratio.tsx +5 -0
- client/src/components/ui/avatar.tsx +48 -0
- client/src/components/ui/badge.tsx +36 -0
- client/src/components/ui/breadcrumb.tsx +115 -0
- client/src/components/ui/button.tsx +56 -0
- client/src/components/ui/calendar.tsx +64 -0
- client/src/components/ui/card.tsx +79 -0
- client/src/components/ui/carousel.tsx +260 -0
- client/src/components/ui/chart.tsx +363 -0
- client/src/components/ui/checkbox.tsx +28 -0
- client/src/components/ui/collapsible.tsx +9 -0
- client/src/components/ui/command.tsx +153 -0
- client/src/components/ui/context-menu.tsx +198 -0
- client/src/components/ui/dialog.tsx +120 -0
- client/src/components/ui/drawer.tsx +116 -0
- client/src/components/ui/dropdown-menu.tsx +198 -0
- client/src/components/ui/form.tsx +176 -0
- client/src/components/ui/hover-card.tsx +27 -0
- client/src/components/ui/input-otp.tsx +69 -0
.gitattributes
CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* 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
|
|
|
|
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
|
36 |
+
generated-icon.png filter=lfs diff=lfs merge=lfs -text
|
LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
MIT License
|
2 |
+
|
3 |
+
Copyright (c) 2025 Ammaar Reshi
|
4 |
+
|
5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6 |
+
of this software and associated documentation files (the "Software"), to deal
|
7 |
+
in the Software without restriction, including without limitation the rights
|
8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9 |
+
copies of the Software, and to permit persons to whom the Software is
|
10 |
+
furnished to do so, subject to the following conditions:
|
11 |
+
|
12 |
+
The above copyright notice and this permission notice shall be included in all
|
13 |
+
copies or substantial portions of the Software.
|
14 |
+
|
15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21 |
+
SOFTWARE.
|
README.md
CHANGED
@@ -1,11 +1,92 @@
|
|
1 |
-
|
2 |
-
|
3 |
-
|
4 |
-
|
5 |
-
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Gemini Search
|
2 |
+
|
3 |
+
A Perplexity-style search engine powered by Google's Gemini 2.0 Flash model with grounding through Google Search. Get AI-powered answers to your questions with real-time web sources and citations.
|
4 |
+
|
5 |
+
Created by [@ammaar](https://x.com/ammaar)
|
6 |
+
|
7 |
+

|
8 |
+
|
9 |
+
|
10 |
+
## Features
|
11 |
+
|
12 |
+
- 🔍 Real-time web search integration
|
13 |
+
- 🤖 Powered by Google's latest Gemini 2.0 Flash model
|
14 |
+
- 📚 Source citations and references for answers
|
15 |
+
- 💬 Follow-up questions in the same chat session
|
16 |
+
- 🎨 Clean, modern UI inspired by Perplexity
|
17 |
+
- ⚡ Fast response times
|
18 |
+
|
19 |
+
## Tech Stack
|
20 |
+
|
21 |
+
- Frontend: React + Vite + TypeScript + Tailwind CSS
|
22 |
+
- Backend: Express.js + TypeScript
|
23 |
+
- AI: Google Gemini 2.0 Flash API
|
24 |
+
- Search: Google Search API integration
|
25 |
+
|
26 |
+
## Setup
|
27 |
+
|
28 |
+
### Prerequisites
|
29 |
+
|
30 |
+
- Node.js (v18 or higher recommended)
|
31 |
+
- npm or yarn
|
32 |
+
- A Google API key with access to Gemini API
|
33 |
+
|
34 |
+
### Installation
|
35 |
+
|
36 |
+
1. Clone the repository:
|
37 |
+
|
38 |
+
```bash
|
39 |
+
git clone https://github.com/ammaarreshi/Gemini-Search.git
|
40 |
+
cd Gemini-Search
|
41 |
+
```
|
42 |
+
|
43 |
+
2. Install dependencies:
|
44 |
+
|
45 |
+
```bash
|
46 |
+
npm install
|
47 |
+
```
|
48 |
+
|
49 |
+
3. Create a `.env` file in the root directory:
|
50 |
+
|
51 |
+
```
|
52 |
+
GOOGLE_API_KEY=your_api_key_here
|
53 |
+
```
|
54 |
+
|
55 |
+
4. Start the development server:
|
56 |
+
|
57 |
+
```bash
|
58 |
+
npm run dev
|
59 |
+
```
|
60 |
+
|
61 |
+
5. Open your browser and navigate to:
|
62 |
+
```
|
63 |
+
http://localhost:3000
|
64 |
+
```
|
65 |
+
|
66 |
+
## Environment Variables
|
67 |
+
|
68 |
+
- `GOOGLE_API_KEY`: Your Google API key with access to Gemini API
|
69 |
+
- `NODE_ENV`: Set to "development" by default, use "production" for production builds
|
70 |
+
|
71 |
+
## Development
|
72 |
+
|
73 |
+
- `npm run dev`: Start the development server
|
74 |
+
- `npm run build`: Build for production
|
75 |
+
- `npm run start`: Run the production server
|
76 |
+
- `npm run check`: Run TypeScript type checking
|
77 |
+
|
78 |
+
## Security Notes
|
79 |
+
|
80 |
+
- Never commit your `.env` file or expose your API keys
|
81 |
+
- The `.gitignore` file is configured to exclude sensitive files
|
82 |
+
- If you fork this repository, make sure to use your own API keys
|
83 |
+
|
84 |
+
## License
|
85 |
+
|
86 |
+
MIT License - feel free to use this code for your own projects!
|
87 |
+
|
88 |
+
## Acknowledgments
|
89 |
+
|
90 |
+
- Inspired by [Perplexity](https://www.perplexity.ai/)
|
91 |
+
- Built with [Google's Gemini API](https://ai.google.dev/)
|
92 |
+
- UI components from [shadcn/ui](https://ui.shadcn.com/)
|
client/apple-touch-icon.png
ADDED
![]() |
client/favicon.png
ADDED
![]() |
client/favicon.svg
ADDED
|
client/index.html
ADDED
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
<html lang="en">
|
3 |
+
<head>
|
4 |
+
<meta charset="UTF-8" />
|
5 |
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
6 |
+
<link rel="icon" type="image/png" href="/favicon.png" />
|
7 |
+
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
8 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
9 |
+
|
10 |
+
<!-- PWA Support -->
|
11 |
+
<meta name="apple-mobile-web-app-capable" content="yes" />
|
12 |
+
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
|
13 |
+
<meta name="apple-mobile-web-app-title" content="Gemini Search" />
|
14 |
+
<meta name="theme-color" content="#FCFCF9" />
|
15 |
+
<link rel="manifest" href="/manifest.json" />
|
16 |
+
|
17 |
+
<!-- iOS Splash Screens -->
|
18 |
+
<link rel="apple-touch-startup-image" href="/splash/apple-splash-2048-2732.jpg" media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" />
|
19 |
+
<link rel="apple-touch-startup-image" href="/splash/apple-splash-1668-2388.jpg" media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" />
|
20 |
+
<link rel="apple-touch-startup-image" href="/splash/apple-splash-1536-2048.jpg" media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" />
|
21 |
+
<link rel="apple-touch-startup-image" href="/splash/apple-splash-1290-2796.jpg" media="(device-width: 430px) and (device-height: 932px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" />
|
22 |
+
<link rel="apple-touch-startup-image" href="/splash/apple-splash-1179-2556.jpg" media="(device-width: 393px) and (device-height: 852px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" />
|
23 |
+
<link rel="apple-touch-startup-image" href="/splash/apple-splash-1170-2532.jpg" media="(device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" />
|
24 |
+
<link rel="apple-touch-startup-image" href="/splash/apple-splash-1125-2436.jpg" media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" />
|
25 |
+
|
26 |
+
<!-- Primary Meta Tags -->
|
27 |
+
<title>Gemini Search</title>
|
28 |
+
<meta name="title" content="Gemini Search" />
|
29 |
+
<meta name="description" content="A search engine powered by Google's Gemini Flash, created by @ammaar" />
|
30 |
+
|
31 |
+
<!-- Open Graph / Facebook -->
|
32 |
+
<meta property="og:type" content="website" />
|
33 |
+
<meta property="og:url" content="https://gemini-search.replit.app/" />
|
34 |
+
<meta property="og:title" content="Gemini Search" />
|
35 |
+
<meta property="og:description" content="A search engine powered by Google's Gemini Flash, created by @ammaar" />
|
36 |
+
<meta property="og:image" content="/og-image.png" />
|
37 |
+
|
38 |
+
<!-- Twitter -->
|
39 |
+
<meta property="twitter:card" content="summary_large_image" />
|
40 |
+
<meta property="twitter:url" content="https://gemini-search.replit.app/" />
|
41 |
+
<meta property="twitter:title" content="Gemini Search" />
|
42 |
+
<meta property="twitter:description" content="A search engine powered by Google's Gemini Flash, created by @ammaar" />
|
43 |
+
<meta property="twitter:image" content="/og-image.png" />
|
44 |
+
</head>
|
45 |
+
<body>
|
46 |
+
<div id="root"></div>
|
47 |
+
<script type="module" src="/src/main.tsx"></script>
|
48 |
+
</body>
|
49 |
+
</html>
|
client/manifest.json
ADDED
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"name": "Gemini Search",
|
3 |
+
"short_name": "Gemini",
|
4 |
+
"description": "A search engine powered by Google's Gemini Flash",
|
5 |
+
"start_url": "/",
|
6 |
+
"display": "standalone",
|
7 |
+
"background_color": "#FCFCF9",
|
8 |
+
"theme_color": "#FCFCF9",
|
9 |
+
"orientation": "portrait",
|
10 |
+
"icons": [
|
11 |
+
{
|
12 |
+
"src": "/favicon.png",
|
13 |
+
"sizes": "32x32",
|
14 |
+
"type": "image/png"
|
15 |
+
},
|
16 |
+
{
|
17 |
+
"src": "/apple-touch-icon.png",
|
18 |
+
"sizes": "180x180",
|
19 |
+
"type": "image/png"
|
20 |
+
},
|
21 |
+
{
|
22 |
+
"src": "/pwa-512x512.png",
|
23 |
+
"sizes": "512x512",
|
24 |
+
"type": "image/png",
|
25 |
+
"purpose": "any maskable"
|
26 |
+
}
|
27 |
+
]
|
28 |
+
}
|
client/og-image.png
ADDED
![]() |
client/public/apple-touch-icon.png
ADDED
![]() |
client/public/favicon.png
ADDED
![]() |
client/public/manifest.json
ADDED
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"name": "Gemini Search",
|
3 |
+
"short_name": "Gemini",
|
4 |
+
"description": "A search engine powered by Google's Gemini Flash",
|
5 |
+
"start_url": "/",
|
6 |
+
"display": "standalone",
|
7 |
+
"background_color": "#FCFCF9",
|
8 |
+
"theme_color": "#FCFCF9",
|
9 |
+
"orientation": "portrait",
|
10 |
+
"icons": [
|
11 |
+
{
|
12 |
+
"src": "/favicon.png",
|
13 |
+
"sizes": "32x32",
|
14 |
+
"type": "image/png"
|
15 |
+
},
|
16 |
+
{
|
17 |
+
"src": "/apple-touch-icon.png",
|
18 |
+
"sizes": "180x180",
|
19 |
+
"type": "image/png"
|
20 |
+
},
|
21 |
+
{
|
22 |
+
"src": "/pwa-512x512.png",
|
23 |
+
"sizes": "512x512",
|
24 |
+
"type": "image/png",
|
25 |
+
"purpose": "any maskable"
|
26 |
+
}
|
27 |
+
]
|
28 |
+
}
|
client/public/pwa-512x512.png
ADDED
![]() |
client/public/splash/apple-splash-1125-2436.jpg
ADDED
![]() |
client/public/splash/apple-splash-1170-2532.jpg
ADDED
![]() |
client/public/splash/apple-splash-1179-2556.jpg
ADDED
![]() |
client/public/splash/apple-splash-1290-2796.jpg
ADDED
![]() |
client/public/splash/apple-splash-1536-2048.jpg
ADDED
![]() |
client/public/splash/apple-splash-1668-2388.jpg
ADDED
![]() |
client/public/splash/apple-splash-2048-2732.jpg
ADDED
![]() |
client/src/App.tsx
ADDED
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { Switch, Route, useLocation } from "wouter";
|
2 |
+
import { Home } from "@/pages/Home";
|
3 |
+
import { Search } from "@/pages/Search";
|
4 |
+
import { Card, CardContent } from "@/components/ui/card";
|
5 |
+
import { AlertCircle } from "lucide-react";
|
6 |
+
import { AnimatePresence } from "framer-motion";
|
7 |
+
|
8 |
+
function App() {
|
9 |
+
const [location] = useLocation();
|
10 |
+
|
11 |
+
return (
|
12 |
+
<AnimatePresence mode="wait">
|
13 |
+
<Switch location={location} key={location}>
|
14 |
+
<Route path="/" component={Home} />
|
15 |
+
<Route path="/search" component={Search} />
|
16 |
+
<Route component={NotFound} />
|
17 |
+
</Switch>
|
18 |
+
</AnimatePresence>
|
19 |
+
);
|
20 |
+
}
|
21 |
+
|
22 |
+
function NotFound() {
|
23 |
+
return (
|
24 |
+
<div className="min-h-screen w-full flex items-center justify-center bg-background">
|
25 |
+
<Card className="w-full max-w-md mx-4">
|
26 |
+
<CardContent className="pt-6">
|
27 |
+
<div className="flex mb-4 gap-2">
|
28 |
+
<AlertCircle className="h-8 w-8 text-destructive" />
|
29 |
+
<h1 className="text-2xl font-bold">404 Page Not Found</h1>
|
30 |
+
</div>
|
31 |
+
|
32 |
+
<p className="mt-4 text-muted-foreground">
|
33 |
+
The page you're looking for doesn't exist.
|
34 |
+
</p>
|
35 |
+
</CardContent>
|
36 |
+
</Card>
|
37 |
+
</div>
|
38 |
+
);
|
39 |
+
}
|
40 |
+
|
41 |
+
export default App;
|
client/src/components/FollowUpInput.tsx
ADDED
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { useState, KeyboardEvent } from 'react';
|
2 |
+
import { Input } from '@/components/ui/input';
|
3 |
+
import { Button } from '@/components/ui/button';
|
4 |
+
import { MessageSquarePlus, Loader2 } from 'lucide-react';
|
5 |
+
import { cn } from '@/lib/utils';
|
6 |
+
|
7 |
+
interface FollowUpInputProps {
|
8 |
+
onSubmit: (query: string) => void;
|
9 |
+
isLoading?: boolean;
|
10 |
+
}
|
11 |
+
|
12 |
+
export function FollowUpInput({
|
13 |
+
onSubmit,
|
14 |
+
isLoading = false,
|
15 |
+
}: FollowUpInputProps) {
|
16 |
+
const [query, setQuery] = useState('');
|
17 |
+
|
18 |
+
const handleSubmit = () => {
|
19 |
+
if (query.trim() && !isLoading) {
|
20 |
+
onSubmit(query.trim());
|
21 |
+
setQuery('');
|
22 |
+
}
|
23 |
+
};
|
24 |
+
|
25 |
+
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
|
26 |
+
if (e.key === 'Enter') {
|
27 |
+
e.preventDefault();
|
28 |
+
handleSubmit();
|
29 |
+
}
|
30 |
+
};
|
31 |
+
|
32 |
+
return (
|
33 |
+
<div className="relative flex flex-col sm:flex-row items-stretch sm:items-center gap-2">
|
34 |
+
<div className="flex-1">
|
35 |
+
<Input
|
36 |
+
value={query}
|
37 |
+
onChange={(e) => setQuery(e.target.value)}
|
38 |
+
onKeyDown={handleKeyDown}
|
39 |
+
placeholder="Ask a follow-up question..."
|
40 |
+
className={cn(
|
41 |
+
"transition-all duration-200",
|
42 |
+
"focus-visible:ring-1 focus-visible:ring-primary",
|
43 |
+
"placeholder:text-muted-foreground/70",
|
44 |
+
"w-full"
|
45 |
+
)}
|
46 |
+
disabled={isLoading}
|
47 |
+
/>
|
48 |
+
</div>
|
49 |
+
|
50 |
+
<Button
|
51 |
+
onClick={handleSubmit}
|
52 |
+
disabled={!query.trim() || isLoading}
|
53 |
+
className="flex items-center justify-center gap-2 w-full sm:w-auto"
|
54 |
+
>
|
55 |
+
{isLoading ? (
|
56 |
+
<Loader2 className="h-4 w-4 animate-spin" />
|
57 |
+
) : (
|
58 |
+
<>
|
59 |
+
<MessageSquarePlus className="h-4 w-4" />
|
60 |
+
Ask
|
61 |
+
</>
|
62 |
+
)}
|
63 |
+
</Button>
|
64 |
+
</div>
|
65 |
+
);
|
66 |
+
}
|
client/src/components/Logo.tsx
ADDED
@@ -0,0 +1,130 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { motion } from 'framer-motion';
|
2 |
+
import { cn } from '@/lib/utils';
|
3 |
+
|
4 |
+
interface LogoProps {
|
5 |
+
className?: string;
|
6 |
+
animate?: boolean;
|
7 |
+
}
|
8 |
+
|
9 |
+
export function Logo({ className, animate = false }: LogoProps) {
|
10 |
+
return (
|
11 |
+
<motion.svg
|
12 |
+
xmlns="http://www.w3.org/2000/svg"
|
13 |
+
width="52"
|
14 |
+
height="52"
|
15 |
+
viewBox="0 0 52 52"
|
16 |
+
fill="none"
|
17 |
+
className={cn("w-[52px] h-[52px]", className)}
|
18 |
+
initial={animate ? { rotate: 0 } : undefined}
|
19 |
+
animate={animate ? { rotate: 360 } : undefined}
|
20 |
+
transition={animate ? {
|
21 |
+
duration: 2,
|
22 |
+
repeat: Infinity,
|
23 |
+
ease: "linear"
|
24 |
+
} : undefined}
|
25 |
+
>
|
26 |
+
<motion.path
|
27 |
+
fillRule="evenodd"
|
28 |
+
clipRule="evenodd"
|
29 |
+
d="M23.554 41.2204C24.92 44.3354 25.603 47.6638 25.603 51.2055C25.603 47.6638 26.264 44.3354 27.587 41.2204C28.952 38.1054 30.787 35.3958 33.091 33.0916C35.396 30.7873 38.105 28.9738 41.22 27.651C44.335 26.2855 47.664 25.6028 51.205 25.6028C47.664 25.6028 44.335 24.9414 41.22 23.6185C38.105 22.2531 35.396 20.4182 33.091 18.114C30.787 15.8097 28.952 13.1001 27.587 9.98507C26.264 6.87007 25.603 3.54171 25.603 0C25.603 3.54171 24.92 6.87007 23.554 9.98507C22.232 13.1001 20.418 15.8097 18.114 18.114C15.81 20.4182 13.1 22.2531 9.985 23.6185C6.87 24.9414 3.542 25.6028 0 25.6028C3.542 25.6028 6.87 26.2855 9.985 27.651C13.1 28.9738 15.81 30.7873 18.114 33.0916C20.418 35.3958 22.232 38.1054 23.554 41.2204Z"
|
30 |
+
fill="url(#paint0_linear_39_18)"
|
31 |
+
initial={!animate ? { scale: 0.8, opacity: 0 } : undefined}
|
32 |
+
animate={!animate ? { scale: 1, opacity: 1 } : undefined}
|
33 |
+
transition={!animate ? {
|
34 |
+
duration: 0.8,
|
35 |
+
ease: [0.16, 1, 0.3, 1]
|
36 |
+
} : undefined}
|
37 |
+
/>
|
38 |
+
<defs>
|
39 |
+
<motion.linearGradient
|
40 |
+
id="paint0_linear_39_18"
|
41 |
+
gradientUnits="userSpaceOnUse"
|
42 |
+
animate={!animate ? {
|
43 |
+
x1: ["-20", "30", "-20"],
|
44 |
+
y1: ["60", "0", "60"],
|
45 |
+
x2: ["70", "20", "70"],
|
46 |
+
y2: ["-10", "50", "-10"]
|
47 |
+
} : {
|
48 |
+
x1: "6.20579",
|
49 |
+
y1: "43.7756",
|
50 |
+
x2: "41.9987",
|
51 |
+
y2: "38.2037"
|
52 |
+
}}
|
53 |
+
transition={!animate ? {
|
54 |
+
duration: 6,
|
55 |
+
repeat: Infinity,
|
56 |
+
ease: "easeInOut"
|
57 |
+
} : undefined}
|
58 |
+
>
|
59 |
+
{animate ? (
|
60 |
+
<>
|
61 |
+
<stop stopColor="#439DDF" />
|
62 |
+
<stop offset="0.524208" stopColor="#4F87ED" />
|
63 |
+
<stop offset="0.781452" stopColor="#9476C5" />
|
64 |
+
<stop offset="0.888252" stopColor="#BC688E" />
|
65 |
+
<stop offset="1" stopColor="#D6645D" />
|
66 |
+
</>
|
67 |
+
) : (
|
68 |
+
<>
|
69 |
+
<motion.stop
|
70 |
+
animate={{
|
71 |
+
stopColor: ["#439DDF", "#4F87ED", "#439DDF"],
|
72 |
+
offset: ["0", "0.3", "0"]
|
73 |
+
}}
|
74 |
+
transition={{
|
75 |
+
duration: 6,
|
76 |
+
repeat: Infinity,
|
77 |
+
ease: "easeInOut"
|
78 |
+
}}
|
79 |
+
/>
|
80 |
+
<motion.stop
|
81 |
+
animate={{
|
82 |
+
stopColor: ["#4F87ED", "#9476C5", "#4F87ED"],
|
83 |
+
offset: ["0.4", "0.8", "0.4"]
|
84 |
+
}}
|
85 |
+
transition={{
|
86 |
+
duration: 6,
|
87 |
+
repeat: Infinity,
|
88 |
+
ease: "easeInOut"
|
89 |
+
}}
|
90 |
+
/>
|
91 |
+
<motion.stop
|
92 |
+
animate={{
|
93 |
+
stopColor: ["#9476C5", "#BC688E", "#9476C5"],
|
94 |
+
offset: ["0.65", "0.9", "0.65"]
|
95 |
+
}}
|
96 |
+
transition={{
|
97 |
+
duration: 6,
|
98 |
+
repeat: Infinity,
|
99 |
+
ease: "easeInOut"
|
100 |
+
}}
|
101 |
+
/>
|
102 |
+
<motion.stop
|
103 |
+
animate={{
|
104 |
+
stopColor: ["#BC688E", "#D6645D", "#BC688E"],
|
105 |
+
offset: ["0.8", "1", "0.8"]
|
106 |
+
}}
|
107 |
+
transition={{
|
108 |
+
duration: 6,
|
109 |
+
repeat: Infinity,
|
110 |
+
ease: "easeInOut"
|
111 |
+
}}
|
112 |
+
/>
|
113 |
+
<motion.stop
|
114 |
+
animate={{
|
115 |
+
stopColor: ["#D6645D", "#439DDF", "#D6645D"]
|
116 |
+
}}
|
117 |
+
offset="1"
|
118 |
+
transition={{
|
119 |
+
duration: 6,
|
120 |
+
repeat: Infinity,
|
121 |
+
ease: "easeInOut"
|
122 |
+
}}
|
123 |
+
/>
|
124 |
+
</>
|
125 |
+
)}
|
126 |
+
</motion.linearGradient>
|
127 |
+
</defs>
|
128 |
+
</motion.svg>
|
129 |
+
);
|
130 |
+
}
|
client/src/components/SearchInput.tsx
ADDED
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { useState, KeyboardEvent } from 'react';
|
2 |
+
import { Input } from '@/components/ui/input';
|
3 |
+
import { Button } from '@/components/ui/button';
|
4 |
+
import { Search, Loader2 } from 'lucide-react';
|
5 |
+
import { cn } from '@/lib/utils';
|
6 |
+
|
7 |
+
interface SearchInputProps {
|
8 |
+
onSearch: (query: string) => void;
|
9 |
+
isLoading?: boolean;
|
10 |
+
initialValue?: string;
|
11 |
+
autoFocus?: boolean;
|
12 |
+
large?: boolean;
|
13 |
+
}
|
14 |
+
|
15 |
+
export function SearchInput({
|
16 |
+
onSearch,
|
17 |
+
isLoading = false,
|
18 |
+
initialValue = '',
|
19 |
+
autoFocus = false,
|
20 |
+
large = false,
|
21 |
+
}: SearchInputProps) {
|
22 |
+
const [query, setQuery] = useState(initialValue);
|
23 |
+
|
24 |
+
const handleSubmit = () => {
|
25 |
+
if (query.trim() && !isLoading) {
|
26 |
+
onSearch(query.trim());
|
27 |
+
}
|
28 |
+
};
|
29 |
+
|
30 |
+
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
|
31 |
+
if (e.key === 'Enter') {
|
32 |
+
handleSubmit();
|
33 |
+
}
|
34 |
+
};
|
35 |
+
|
36 |
+
return (
|
37 |
+
<div className="relative flex w-full items-center gap-2">
|
38 |
+
<div className="relative flex-1">
|
39 |
+
<Search className={cn(
|
40 |
+
"absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground",
|
41 |
+
large ? "h-5 w-5" : "h-4 w-4"
|
42 |
+
)} />
|
43 |
+
|
44 |
+
<Input
|
45 |
+
value={query}
|
46 |
+
onChange={(e) => setQuery(e.target.value)}
|
47 |
+
onKeyDown={handleKeyDown}
|
48 |
+
placeholder="Ask anything..."
|
49 |
+
className={cn(
|
50 |
+
"pl-10 pr-4 transition-all duration-200",
|
51 |
+
large && "h-12 text-lg rounded-lg",
|
52 |
+
"focus-visible:ring-2 focus-visible:ring-primary"
|
53 |
+
)}
|
54 |
+
disabled={isLoading}
|
55 |
+
autoFocus={autoFocus}
|
56 |
+
/>
|
57 |
+
</div>
|
58 |
+
|
59 |
+
<Button
|
60 |
+
onClick={handleSubmit}
|
61 |
+
disabled={!query.trim() || isLoading}
|
62 |
+
className={cn(
|
63 |
+
"min-w-[80px] shadow-sm",
|
64 |
+
large && "h-12 px-6 text-lg rounded-lg"
|
65 |
+
)}
|
66 |
+
>
|
67 |
+
{isLoading ? (
|
68 |
+
<Loader2 className={cn("animate-spin", large ? "h-5 w-5" : "h-4 w-4")} />
|
69 |
+
) : (
|
70 |
+
'Search'
|
71 |
+
)}
|
72 |
+
</Button>
|
73 |
+
</div>
|
74 |
+
);
|
75 |
+
}
|
client/src/components/SearchResults.tsx
ADDED
@@ -0,0 +1,132 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { useEffect, useRef } from 'react';
|
2 |
+
import { Card } from '@/components/ui/card';
|
3 |
+
import { Alert, AlertDescription } from '@/components/ui/alert';
|
4 |
+
import { AlertCircle } from 'lucide-react';
|
5 |
+
import { Skeleton } from '@/components/ui/skeleton';
|
6 |
+
import { cn } from '@/lib/utils';
|
7 |
+
import { motion } from 'framer-motion';
|
8 |
+
import { SourceList } from '@/components/SourceList';
|
9 |
+
import { Logo } from '@/components/Logo';
|
10 |
+
|
11 |
+
interface SearchResultsProps {
|
12 |
+
query: string;
|
13 |
+
results: any;
|
14 |
+
isLoading: boolean;
|
15 |
+
error?: Error;
|
16 |
+
isFollowUp?: boolean;
|
17 |
+
originalQuery?: string;
|
18 |
+
}
|
19 |
+
|
20 |
+
export function SearchResults({
|
21 |
+
query,
|
22 |
+
results,
|
23 |
+
isLoading,
|
24 |
+
error,
|
25 |
+
isFollowUp,
|
26 |
+
originalQuery
|
27 |
+
}: SearchResultsProps) {
|
28 |
+
const contentRef = useRef<HTMLDivElement>(null);
|
29 |
+
|
30 |
+
useEffect(() => {
|
31 |
+
if (results && contentRef.current) {
|
32 |
+
contentRef.current.scrollIntoView({ behavior: 'smooth' });
|
33 |
+
}
|
34 |
+
}, [results]);
|
35 |
+
|
36 |
+
if (error) {
|
37 |
+
return (
|
38 |
+
<Alert variant="destructive" className="animate-in fade-in-50">
|
39 |
+
<AlertCircle className="h-4 w-4" />
|
40 |
+
<AlertDescription>
|
41 |
+
{error.message || 'An error occurred while searching. Please try again.'}
|
42 |
+
</AlertDescription>
|
43 |
+
</Alert>
|
44 |
+
);
|
45 |
+
}
|
46 |
+
|
47 |
+
if (isLoading) {
|
48 |
+
return (
|
49 |
+
<div className="space-y-4 animate-in fade-in-50">
|
50 |
+
<div className="flex justify-center mb-8">
|
51 |
+
<Logo animate className="w-12 h-12" />
|
52 |
+
</div>
|
53 |
+
<Card className="p-6">
|
54 |
+
<Skeleton className="h-4 w-3/4 mb-4" />
|
55 |
+
<Skeleton className="h-4 w-full mb-2" />
|
56 |
+
<Skeleton className="h-4 w-full mb-2" />
|
57 |
+
<Skeleton className="h-4 w-2/3" />
|
58 |
+
</Card>
|
59 |
+
<div className="space-y-2">
|
60 |
+
<Skeleton className="h-[100px] w-full" />
|
61 |
+
<Skeleton className="h-[100px] w-full" />
|
62 |
+
</div>
|
63 |
+
</div>
|
64 |
+
);
|
65 |
+
}
|
66 |
+
|
67 |
+
if (!results) return null;
|
68 |
+
|
69 |
+
return (
|
70 |
+
<div ref={contentRef} className="space-y-6 animate-in fade-in-50">
|
71 |
+
{/* Search Query Display */}
|
72 |
+
<motion.div
|
73 |
+
initial={{ opacity: 0, y: -20 }}
|
74 |
+
animate={{ opacity: 1, y: 0 }}
|
75 |
+
className="flex flex-col gap-2"
|
76 |
+
>
|
77 |
+
{isFollowUp && originalQuery && (
|
78 |
+
<>
|
79 |
+
<div className="flex flex-col sm:flex-row sm:items-baseline gap-2 text-xs sm:text-sm text-muted-foreground/70">
|
80 |
+
<span>Original search:</span>
|
81 |
+
<span className="font-medium">"{originalQuery}"</span>
|
82 |
+
</div>
|
83 |
+
<div className="h-px bg-border w-full" />
|
84 |
+
</>
|
85 |
+
)}
|
86 |
+
<div className="flex flex-col sm:flex-row sm:items-baseline gap-2 text-sm sm:text-base text-muted-foreground">
|
87 |
+
<span>{isFollowUp ? 'Follow-up question:' : ''}</span>
|
88 |
+
<h1 className="font-serif text-lg sm:text-3xl text-foreground">"{query}"</h1>
|
89 |
+
</div>
|
90 |
+
</motion.div>
|
91 |
+
|
92 |
+
{/* Sources Section */}
|
93 |
+
{results.sources && results.sources.length > 0 && (
|
94 |
+
<motion.div
|
95 |
+
initial={{ opacity: 0, y: 20 }}
|
96 |
+
animate={{ opacity: 1, y: 0 }}
|
97 |
+
transition={{ delay: 0.2 }}
|
98 |
+
>
|
99 |
+
<SourceList sources={results.sources} />
|
100 |
+
</motion.div>
|
101 |
+
)}
|
102 |
+
|
103 |
+
{/* Main Content */}
|
104 |
+
<Card className="overflow-hidden shadow-md">
|
105 |
+
<motion.div
|
106 |
+
initial={{ opacity: 0, y: 20 }}
|
107 |
+
animate={{ opacity: 1, y: 0 }}
|
108 |
+
transition={{ delay: 0.3, duration: 0.4 }}
|
109 |
+
className="py-4 px-8"
|
110 |
+
>
|
111 |
+
<div
|
112 |
+
className={cn(
|
113 |
+
"prose prose-slate max-w-none",
|
114 |
+
"dark:prose-invert",
|
115 |
+
"prose-headings:font-bold prose-headings:mb-4",
|
116 |
+
"prose-h2:text-2xl prose-h2:mt-8 prose-h2:border-b prose-h2:pb-2 prose-h2:border-border",
|
117 |
+
"prose-h3:text-xl prose-h3:mt-6",
|
118 |
+
"prose-p:text-base prose-p:leading-7 prose-p:my-4",
|
119 |
+
"prose-ul:my-6 prose-ul:list-disc prose-ul:pl-6",
|
120 |
+
"prose-li:my-2 prose-li:marker:text-muted-foreground",
|
121 |
+
"prose-strong:font-semibold",
|
122 |
+
"prose-a:text-primary prose-a:no-underline hover:prose-a:text-primary/80",
|
123 |
+
)}
|
124 |
+
dangerouslySetInnerHTML={{
|
125 |
+
__html: results.summary
|
126 |
+
}}
|
127 |
+
/>
|
128 |
+
</motion.div>
|
129 |
+
</Card>
|
130 |
+
</div>
|
131 |
+
);
|
132 |
+
}
|
client/src/components/SourceList.tsx
ADDED
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { Card } from '@/components/ui/card';
|
2 |
+
import { ExternalLink, Link2 } from 'lucide-react';
|
3 |
+
import { motion } from 'framer-motion';
|
4 |
+
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
|
5 |
+
|
6 |
+
interface Source {
|
7 |
+
title: string;
|
8 |
+
url: string;
|
9 |
+
snippet: string;
|
10 |
+
}
|
11 |
+
|
12 |
+
interface SourceListProps {
|
13 |
+
sources: Source[];
|
14 |
+
}
|
15 |
+
|
16 |
+
export function SourceList({ sources }: SourceListProps) {
|
17 |
+
return (
|
18 |
+
<div className="space-y-4 animate-in fade-in-50">
|
19 |
+
<div className="flex items-center gap-2 mb-2">
|
20 |
+
<Link2 className="h-4 w-4 text-muted-foreground" />
|
21 |
+
<h2 className="text-base font-semibold text-foreground/90">Sources</h2>
|
22 |
+
</div>
|
23 |
+
|
24 |
+
<ScrollArea className="w-full whitespace-nowrap rounded-md">
|
25 |
+
<motion.div
|
26 |
+
className="flex space-x-3 pb-4"
|
27 |
+
initial={{ opacity: 0 }}
|
28 |
+
animate={{ opacity: 1 }}
|
29 |
+
transition={{ duration: 0.4, staggerChildren: 0.1 }}
|
30 |
+
>
|
31 |
+
{sources.map((source, index) => (
|
32 |
+
<motion.div
|
33 |
+
key={index}
|
34 |
+
initial={{ opacity: 0, x: 20 }}
|
35 |
+
animate={{ opacity: 1, x: 0 }}
|
36 |
+
transition={{ duration: 0.3, delay: index * 0.1 }}
|
37 |
+
className="shrink-0"
|
38 |
+
>
|
39 |
+
<Card
|
40 |
+
className="w-[280px] group overflow-hidden transition-all hover:shadow-md cursor-pointer bg-card/50 hover:bg-card"
|
41 |
+
onClick={() => window.open(source.url, '_blank')}
|
42 |
+
>
|
43 |
+
<div className="p-4 hover:bg-muted/30">
|
44 |
+
<div className="flex items-start justify-between gap-3">
|
45 |
+
<div className="flex-1 min-w-0">
|
46 |
+
<h3 className="font-medium text-sm text-foreground line-clamp-1 mb-1">
|
47 |
+
{source.title.replace(/\*\*/g, '')}
|
48 |
+
</h3>
|
49 |
+
|
50 |
+
{source.snippet && (
|
51 |
+
<p className="text-sm text-muted-foreground line-clamp-2 mb-2">
|
52 |
+
{source.snippet.replace(/\*\*/g, '')}
|
53 |
+
</p>
|
54 |
+
)}
|
55 |
+
|
56 |
+
<div className="flex items-center gap-2 text-xs text-muted-foreground/70">
|
57 |
+
<span className="truncate max-w-[200px]">
|
58 |
+
{new URL(source.url).hostname.replace('www.', '')}
|
59 |
+
</span>
|
60 |
+
</div>
|
61 |
+
</div>
|
62 |
+
|
63 |
+
<ExternalLink className="h-4 w-4 flex-shrink-0 text-muted-foreground
|
64 |
+
opacity-50 group-hover:opacity-100 transition-opacity" />
|
65 |
+
</div>
|
66 |
+
</div>
|
67 |
+
</Card>
|
68 |
+
</motion.div>
|
69 |
+
))}
|
70 |
+
</motion.div>
|
71 |
+
<ScrollBar orientation="horizontal" />
|
72 |
+
</ScrollArea>
|
73 |
+
</div>
|
74 |
+
);
|
75 |
+
}
|
client/src/components/SourcePreviewModal.tsx
ADDED
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
2 |
+
import { ScrollArea } from "@/components/ui/scroll-area";
|
3 |
+
import { ExternalLink } from "lucide-react";
|
4 |
+
import { motion, AnimatePresence } from "framer-motion";
|
5 |
+
|
6 |
+
interface Source {
|
7 |
+
title: string;
|
8 |
+
url: string;
|
9 |
+
snippet: string;
|
10 |
+
}
|
11 |
+
|
12 |
+
interface SourcePreviewModalProps {
|
13 |
+
source: Source | null;
|
14 |
+
isOpen: boolean;
|
15 |
+
onClose: () => void;
|
16 |
+
}
|
17 |
+
|
18 |
+
export function SourcePreviewModal({ source, isOpen, onClose }: SourcePreviewModalProps) {
|
19 |
+
if (!source) return null;
|
20 |
+
|
21 |
+
return (
|
22 |
+
<AnimatePresence>
|
23 |
+
{isOpen && (
|
24 |
+
<Dialog open={isOpen} onOpenChange={onClose}>
|
25 |
+
<DialogContent className="max-w-3xl h-[80vh] flex flex-col">
|
26 |
+
<DialogHeader>
|
27 |
+
<motion.div
|
28 |
+
initial={{ opacity: 0, y: -20 }}
|
29 |
+
animate={{ opacity: 1, y: 0 }}
|
30 |
+
transition={{ duration: 0.2 }}
|
31 |
+
>
|
32 |
+
<DialogTitle className="text-xl font-semibold mb-2">
|
33 |
+
{source.title}
|
34 |
+
</DialogTitle>
|
35 |
+
<a
|
36 |
+
href={source.url}
|
37 |
+
target="_blank"
|
38 |
+
rel="noopener noreferrer"
|
39 |
+
className="text-sm text-muted-foreground hover:text-primary flex items-center gap-2"
|
40 |
+
>
|
41 |
+
<span className="truncate">{source.url}</span>
|
42 |
+
<ExternalLink className="h-4 w-4" />
|
43 |
+
</a>
|
44 |
+
</motion.div>
|
45 |
+
</DialogHeader>
|
46 |
+
|
47 |
+
<ScrollArea className="flex-1 mt-4">
|
48 |
+
<motion.div
|
49 |
+
initial={{ opacity: 0 }}
|
50 |
+
animate={{ opacity: 1 }}
|
51 |
+
transition={{ duration: 0.3, delay: 0.1 }}
|
52 |
+
className="prose prose-slate dark:prose-invert max-w-none"
|
53 |
+
>
|
54 |
+
<div className="space-y-4">
|
55 |
+
<p className="text-base leading-relaxed">
|
56 |
+
{source.snippet}
|
57 |
+
</p>
|
58 |
+
{/* TODO: Add full content preview when available */}
|
59 |
+
</div>
|
60 |
+
</motion.div>
|
61 |
+
</ScrollArea>
|
62 |
+
</DialogContent>
|
63 |
+
</Dialog>
|
64 |
+
)}
|
65 |
+
</AnimatePresence>
|
66 |
+
);
|
67 |
+
}
|
client/src/components/ThemeToggle.tsx
ADDED
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
|
2 |
+
import { Moon, Sun } from "lucide-react";
|
3 |
+
import { Button } from "./ui/button";
|
4 |
+
import { useEffect, useState } from "react";
|
5 |
+
|
6 |
+
export function ThemeToggle() {
|
7 |
+
const [theme, setTheme] = useState("light");
|
8 |
+
|
9 |
+
useEffect(() => {
|
10 |
+
const isDark = document.documentElement.classList.contains("dark");
|
11 |
+
setTheme(isDark ? "dark" : "light");
|
12 |
+
}, []);
|
13 |
+
|
14 |
+
function toggleTheme() {
|
15 |
+
const newTheme = theme === "light" ? "dark" : "light";
|
16 |
+
setTheme(newTheme);
|
17 |
+
document.documentElement.classList.toggle("dark");
|
18 |
+
}
|
19 |
+
|
20 |
+
return (
|
21 |
+
<Button
|
22 |
+
variant="ghost"
|
23 |
+
size="icon"
|
24 |
+
onClick={toggleTheme}
|
25 |
+
className="fixed top-4 right-4"
|
26 |
+
>
|
27 |
+
{theme === "light" ? (
|
28 |
+
<Moon className="h-5 w-5" />
|
29 |
+
) : (
|
30 |
+
<Sun className="h-5 w-5" />
|
31 |
+
)}
|
32 |
+
</Button>
|
33 |
+
);
|
34 |
+
}
|
client/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 }
|
client/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 |
+
}
|
client/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 }
|
client/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 }
|
client/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 }
|
client/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 }
|
client/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]:w-3.5 [&>svg]:h-3.5", className)}
|
84 |
+
{...props}
|
85 |
+
>
|
86 |
+
{children ?? <ChevronRight />}
|
87 |
+
</li>
|
88 |
+
)
|
89 |
+
BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
|
90 |
+
|
91 |
+
const BreadcrumbEllipsis = ({
|
92 |
+
className,
|
93 |
+
...props
|
94 |
+
}: React.ComponentProps<"span">) => (
|
95 |
+
<span
|
96 |
+
role="presentation"
|
97 |
+
aria-hidden="true"
|
98 |
+
className={cn("flex h-9 w-9 items-center justify-center", className)}
|
99 |
+
{...props}
|
100 |
+
>
|
101 |
+
<MoreHorizontal className="h-4 w-4" />
|
102 |
+
<span className="sr-only">More</span>
|
103 |
+
</span>
|
104 |
+
)
|
105 |
+
BreadcrumbEllipsis.displayName = "BreadcrumbElipssis"
|
106 |
+
|
107 |
+
export {
|
108 |
+
Breadcrumb,
|
109 |
+
BreadcrumbList,
|
110 |
+
BreadcrumbItem,
|
111 |
+
BreadcrumbLink,
|
112 |
+
BreadcrumbPage,
|
113 |
+
BreadcrumbSeparator,
|
114 |
+
BreadcrumbEllipsis,
|
115 |
+
}
|
client/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 }
|
client/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 }
|
client/src/components/ui/card.tsx
ADDED
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import * as React from "react"
|
2 |
+
|
3 |
+
import { cn } from "@/lib/utils"
|
4 |
+
|
5 |
+
const Card = React.forwardRef<
|
6 |
+
HTMLDivElement,
|
7 |
+
React.HTMLAttributes<HTMLDivElement>
|
8 |
+
>(({ className, ...props }, ref) => (
|
9 |
+
<div
|
10 |
+
ref={ref}
|
11 |
+
className={cn(
|
12 |
+
"rounded-lg border bg-card text-card-foreground shadow-sm",
|
13 |
+
className
|
14 |
+
)}
|
15 |
+
{...props}
|
16 |
+
/>
|
17 |
+
))
|
18 |
+
Card.displayName = "Card"
|
19 |
+
|
20 |
+
const CardHeader = React.forwardRef<
|
21 |
+
HTMLDivElement,
|
22 |
+
React.HTMLAttributes<HTMLDivElement>
|
23 |
+
>(({ className, ...props }, ref) => (
|
24 |
+
<div
|
25 |
+
ref={ref}
|
26 |
+
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
27 |
+
{...props}
|
28 |
+
/>
|
29 |
+
))
|
30 |
+
CardHeader.displayName = "CardHeader"
|
31 |
+
|
32 |
+
const CardTitle = React.forwardRef<
|
33 |
+
HTMLParagraphElement,
|
34 |
+
React.HTMLAttributes<HTMLHeadingElement>
|
35 |
+
>(({ className, ...props }, ref) => (
|
36 |
+
<h3
|
37 |
+
ref={ref}
|
38 |
+
className={cn(
|
39 |
+
"text-2xl font-semibold leading-none tracking-tight",
|
40 |
+
className
|
41 |
+
)}
|
42 |
+
{...props}
|
43 |
+
/>
|
44 |
+
))
|
45 |
+
CardTitle.displayName = "CardTitle"
|
46 |
+
|
47 |
+
const CardDescription = React.forwardRef<
|
48 |
+
HTMLParagraphElement,
|
49 |
+
React.HTMLAttributes<HTMLParagraphElement>
|
50 |
+
>(({ className, ...props }, ref) => (
|
51 |
+
<p
|
52 |
+
ref={ref}
|
53 |
+
className={cn("text-sm text-muted-foreground", className)}
|
54 |
+
{...props}
|
55 |
+
/>
|
56 |
+
))
|
57 |
+
CardDescription.displayName = "CardDescription"
|
58 |
+
|
59 |
+
const CardContent = React.forwardRef<
|
60 |
+
HTMLDivElement,
|
61 |
+
React.HTMLAttributes<HTMLDivElement>
|
62 |
+
>(({ className, ...props }, ref) => (
|
63 |
+
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
64 |
+
))
|
65 |
+
CardContent.displayName = "CardContent"
|
66 |
+
|
67 |
+
const CardFooter = React.forwardRef<
|
68 |
+
HTMLDivElement,
|
69 |
+
React.HTMLAttributes<HTMLDivElement>
|
70 |
+
>(({ className, ...props }, ref) => (
|
71 |
+
<div
|
72 |
+
ref={ref}
|
73 |
+
className={cn("flex items-center p-6 pt-0", className)}
|
74 |
+
{...props}
|
75 |
+
/>
|
76 |
+
))
|
77 |
+
CardFooter.displayName = "CardFooter"
|
78 |
+
|
79 |
+
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
client/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 |
+
}
|
client/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 |
+
}
|
client/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 }
|
client/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 }
|
client/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 |
+
}
|
client/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 |
+
}
|
client/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 |
+
}
|
client/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 |
+
}
|
client/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 gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
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 |
+
}
|
client/src/components/ui/form.tsx
ADDED
@@ -0,0 +1,176 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import * as React from "react"
|
2 |
+
import * as LabelPrimitive from "@radix-ui/react-label"
|
3 |
+
import { Slot } from "@radix-ui/react-slot"
|
4 |
+
import {
|
5 |
+
Controller,
|
6 |
+
ControllerProps,
|
7 |
+
FieldPath,
|
8 |
+
FieldValues,
|
9 |
+
FormProvider,
|
10 |
+
useFormContext,
|
11 |
+
} from "react-hook-form"
|
12 |
+
|
13 |
+
import { cn } from "@/lib/utils"
|
14 |
+
import { Label } from "@/components/ui/label"
|
15 |
+
|
16 |
+
const Form = FormProvider
|
17 |
+
|
18 |
+
type FormFieldContextValue<
|
19 |
+
TFieldValues extends FieldValues = FieldValues,
|
20 |
+
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
|
21 |
+
> = {
|
22 |
+
name: TName
|
23 |
+
}
|
24 |
+
|
25 |
+
const FormFieldContext = React.createContext<FormFieldContextValue>(
|
26 |
+
{} as FormFieldContextValue
|
27 |
+
)
|
28 |
+
|
29 |
+
const FormField = <
|
30 |
+
TFieldValues extends FieldValues = FieldValues,
|
31 |
+
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
|
32 |
+
>({
|
33 |
+
...props
|
34 |
+
}: ControllerProps<TFieldValues, TName>) => {
|
35 |
+
return (
|
36 |
+
<FormFieldContext.Provider value={{ name: props.name }}>
|
37 |
+
<Controller {...props} />
|
38 |
+
</FormFieldContext.Provider>
|
39 |
+
)
|
40 |
+
}
|
41 |
+
|
42 |
+
const useFormField = () => {
|
43 |
+
const fieldContext = React.useContext(FormFieldContext)
|
44 |
+
const itemContext = React.useContext(FormItemContext)
|
45 |
+
const { getFieldState, formState } = useFormContext()
|
46 |
+
|
47 |
+
const fieldState = getFieldState(fieldContext.name, formState)
|
48 |
+
|
49 |
+
if (!fieldContext) {
|
50 |
+
throw new Error("useFormField should be used within <FormField>")
|
51 |
+
}
|
52 |
+
|
53 |
+
const { id } = itemContext
|
54 |
+
|
55 |
+
return {
|
56 |
+
id,
|
57 |
+
name: fieldContext.name,
|
58 |
+
formItemId: `${id}-form-item`,
|
59 |
+
formDescriptionId: `${id}-form-item-description`,
|
60 |
+
formMessageId: `${id}-form-item-message`,
|
61 |
+
...fieldState,
|
62 |
+
}
|
63 |
+
}
|
64 |
+
|
65 |
+
type FormItemContextValue = {
|
66 |
+
id: string
|
67 |
+
}
|
68 |
+
|
69 |
+
const FormItemContext = React.createContext<FormItemContextValue>(
|
70 |
+
{} as FormItemContextValue
|
71 |
+
)
|
72 |
+
|
73 |
+
const FormItem = React.forwardRef<
|
74 |
+
HTMLDivElement,
|
75 |
+
React.HTMLAttributes<HTMLDivElement>
|
76 |
+
>(({ className, ...props }, ref) => {
|
77 |
+
const id = React.useId()
|
78 |
+
|
79 |
+
return (
|
80 |
+
<FormItemContext.Provider value={{ id }}>
|
81 |
+
<div ref={ref} className={cn("space-y-2", className)} {...props} />
|
82 |
+
</FormItemContext.Provider>
|
83 |
+
)
|
84 |
+
})
|
85 |
+
FormItem.displayName = "FormItem"
|
86 |
+
|
87 |
+
const FormLabel = React.forwardRef<
|
88 |
+
React.ElementRef<typeof LabelPrimitive.Root>,
|
89 |
+
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
|
90 |
+
>(({ className, ...props }, ref) => {
|
91 |
+
const { error, formItemId } = useFormField()
|
92 |
+
|
93 |
+
return (
|
94 |
+
<Label
|
95 |
+
ref={ref}
|
96 |
+
className={cn(error && "text-destructive", className)}
|
97 |
+
htmlFor={formItemId}
|
98 |
+
{...props}
|
99 |
+
/>
|
100 |
+
)
|
101 |
+
})
|
102 |
+
FormLabel.displayName = "FormLabel"
|
103 |
+
|
104 |
+
const FormControl = React.forwardRef<
|
105 |
+
React.ElementRef<typeof Slot>,
|
106 |
+
React.ComponentPropsWithoutRef<typeof Slot>
|
107 |
+
>(({ ...props }, ref) => {
|
108 |
+
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
|
109 |
+
|
110 |
+
return (
|
111 |
+
<Slot
|
112 |
+
ref={ref}
|
113 |
+
id={formItemId}
|
114 |
+
aria-describedby={
|
115 |
+
!error
|
116 |
+
? `${formDescriptionId}`
|
117 |
+
: `${formDescriptionId} ${formMessageId}`
|
118 |
+
}
|
119 |
+
aria-invalid={!!error}
|
120 |
+
{...props}
|
121 |
+
/>
|
122 |
+
)
|
123 |
+
})
|
124 |
+
FormControl.displayName = "FormControl"
|
125 |
+
|
126 |
+
const FormDescription = React.forwardRef<
|
127 |
+
HTMLParagraphElement,
|
128 |
+
React.HTMLAttributes<HTMLParagraphElement>
|
129 |
+
>(({ className, ...props }, ref) => {
|
130 |
+
const { formDescriptionId } = useFormField()
|
131 |
+
|
132 |
+
return (
|
133 |
+
<p
|
134 |
+
ref={ref}
|
135 |
+
id={formDescriptionId}
|
136 |
+
className={cn("text-sm text-muted-foreground", className)}
|
137 |
+
{...props}
|
138 |
+
/>
|
139 |
+
)
|
140 |
+
})
|
141 |
+
FormDescription.displayName = "FormDescription"
|
142 |
+
|
143 |
+
const FormMessage = React.forwardRef<
|
144 |
+
HTMLParagraphElement,
|
145 |
+
React.HTMLAttributes<HTMLParagraphElement>
|
146 |
+
>(({ className, children, ...props }, ref) => {
|
147 |
+
const { error, formMessageId } = useFormField()
|
148 |
+
const body = error ? String(error?.message) : children
|
149 |
+
|
150 |
+
if (!body) {
|
151 |
+
return null
|
152 |
+
}
|
153 |
+
|
154 |
+
return (
|
155 |
+
<p
|
156 |
+
ref={ref}
|
157 |
+
id={formMessageId}
|
158 |
+
className={cn("text-sm font-medium text-destructive", className)}
|
159 |
+
{...props}
|
160 |
+
>
|
161 |
+
{body}
|
162 |
+
</p>
|
163 |
+
)
|
164 |
+
})
|
165 |
+
FormMessage.displayName = "FormMessage"
|
166 |
+
|
167 |
+
export {
|
168 |
+
useFormField,
|
169 |
+
Form,
|
170 |
+
FormItem,
|
171 |
+
FormLabel,
|
172 |
+
FormControl,
|
173 |
+
FormDescription,
|
174 |
+
FormMessage,
|
175 |
+
FormField,
|
176 |
+
}
|
client/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 }
|
client/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 }
|