mirror of
https://github.com/stackrender/stackrender.git
synced 2026-09-10 19:25:44 +00:00
Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7e7f7c7397 | |||
| e342ffa831 | |||
| f1656fafcd | |||
| caca671b36 | |||
| 1b1c8ebdf6 | |||
| c31ab6423b | |||
| aa3688a0e6 | |||
| 39317143b1 | |||
| 14db841617 | |||
| 1d78d05468 | |||
| d9e5333f96 | |||
| f68b0c97bf | |||
| 42699038ad | |||
| f594b4e938 | |||
| f6511df039 | |||
| 9f6b61643f | |||
| 7c443f0edf | |||
| 6bb10b86f2 | |||
| 1348262649 | |||
| c226adfb27 | |||
| 0e82e5b965 | |||
| cd9861bfd8 |
@@ -0,0 +1,53 @@
|
||||
# Dependencies
|
||||
node_modules
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Build outputs
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Environment files
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
# Git
|
||||
.git
|
||||
.gitignore
|
||||
|
||||
# Docker
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
docker-compose.yml
|
||||
|
||||
# Misc
|
||||
README.md
|
||||
LICENSE
|
||||
CODE_OF_CONDUCT.md
|
||||
CONTRIBUTING.md
|
||||
SECURITY.md
|
||||
|
||||
# Lock files (we keep package-lock.json but ignore others)
|
||||
pnpm-lock.yaml
|
||||
yarn.lock
|
||||
bun.lockb
|
||||
|
||||
# Vite cache
|
||||
vite.config.ts.timestamp-*
|
||||
+1
-1
@@ -60,7 +60,7 @@ representative at an online or offline event.
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at
|
||||
tamanikarim50@gmail.com.
|
||||
contact@stackrender.io.
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
|
||||
+2
-2
@@ -27,10 +27,10 @@ If you find a bug:
|
||||
If you have an idea for improvement:
|
||||
- Open a **feature request** in the issues section.
|
||||
- Describe the feature and explain why it would be useful.
|
||||
- You may also discuss ideas with the community on [Discord](https://discord.gg/DsN8RcPR6Y).
|
||||
- You may also discuss ideas with the community on [Discord](https://discord.gg/4dv26jR4Pj).
|
||||
|
||||
## License Agreement
|
||||
By contributing, you agree that your work will be licensed under the **GNU Affero General Public License v3.0**.
|
||||
|
||||
## Questions or Support
|
||||
If you have questions about the contribution process, open a discussion on GitHub or reach out on our [Discord](https://discord.gg/DsN8RcPR6Y).
|
||||
If you have questions about the contribution process, open a discussion on GitHub or reach out on our [Discord](https://discord.gg/4dv26jR4Pj).
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
# Docker Deployment Guide
|
||||
|
||||
This guide explains how to run StackRender using Docker.
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Using Docker Compose (Recommended)
|
||||
|
||||
The easiest way to run StackRender:
|
||||
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
The application will be available at `http://localhost:8080`
|
||||
|
||||
To stop the application:
|
||||
|
||||
```bash
|
||||
docker-compose down
|
||||
```
|
||||
|
||||
### Using Docker CLI
|
||||
|
||||
Build the image:
|
||||
|
||||
```bash
|
||||
docker build -t stackrender .
|
||||
```
|
||||
|
||||
Run the container:
|
||||
|
||||
```bash
|
||||
docker run -d -p 8080:80 --name stackrender stackrender
|
||||
```
|
||||
|
||||
Stop and remove the container:
|
||||
|
||||
```bash
|
||||
docker stop stackrender
|
||||
docker rm stackrender
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Port Mapping
|
||||
|
||||
By default, the application runs on port 80 inside the container and is mapped to port 8080 on the host. You can change this in `docker-compose.yml`:
|
||||
|
||||
```yaml
|
||||
ports:
|
||||
- "YOUR_PORT:80"
|
||||
```
|
||||
|
||||
Or when using Docker CLI:
|
||||
|
||||
```bash
|
||||
docker run -d -p YOUR_PORT:80 --name stackrender stackrender
|
||||
```
|
||||
|
||||
### Health Check
|
||||
|
||||
The Docker Compose configuration includes a health check that verifies the application is responding correctly:
|
||||
|
||||
```yaml
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:80/"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
### Multi-Stage Build
|
||||
|
||||
The Dockerfile uses a multi-stage build approach:
|
||||
|
||||
1. **Builder Stage**: Uses Node.js 20 to install dependencies and build the application
|
||||
2. **Production Stage**: Uses Nginx Alpine to serve the static files
|
||||
|
||||
This approach minimizes the final image size by excluding build tools and dependencies.
|
||||
|
||||
### Nginx Configuration
|
||||
|
||||
The application uses a custom Nginx configuration (`nginx.conf`) that:
|
||||
|
||||
- Enables gzip compression for better performance
|
||||
- Sets security headers (X-Frame-Options, X-Content-Type-Options, X-XSS-Protection)
|
||||
- Handles client-side routing (React Router)
|
||||
- Configures caching for static assets
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Build Issues
|
||||
|
||||
If you encounter SSL certificate issues during the build, the Dockerfile includes a workaround:
|
||||
|
||||
```dockerfile
|
||||
RUN npm config set strict-ssl false && npm install
|
||||
```
|
||||
|
||||
This is necessary in some build environments with certificate validation issues.
|
||||
|
||||
### Container Not Starting
|
||||
|
||||
Check the logs:
|
||||
|
||||
```bash
|
||||
docker logs stackrender
|
||||
```
|
||||
|
||||
Or with Docker Compose:
|
||||
|
||||
```bash
|
||||
docker-compose logs
|
||||
```
|
||||
|
||||
### Port Already in Use
|
||||
|
||||
If port 8080 is already in use, you can change it in `docker-compose.yml` or use a different port with Docker CLI:
|
||||
|
||||
```bash
|
||||
docker run -d -p 8081:80 --name stackrender stackrender
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
For development, it's recommended to run the application directly with Node.js instead of Docker:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Docker is primarily intended for production deployments or testing the production build locally.
|
||||
|
||||
## Production Deployment
|
||||
|
||||
For production deployments:
|
||||
|
||||
1. Build the image:
|
||||
```bash
|
||||
docker build -t stackrender:production .
|
||||
```
|
||||
|
||||
2. Push to your container registry:
|
||||
```bash
|
||||
docker tag stackrender:production your-registry/stackrender:latest
|
||||
docker push your-registry/stackrender:latest
|
||||
```
|
||||
|
||||
3. Deploy using your orchestration tool (Kubernetes, Docker Swarm, etc.)
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- [Docker Documentation](https://docs.docker.com/)
|
||||
- [Docker Compose Documentation](https://docs.docker.com/compose/)
|
||||
- [Nginx Documentation](https://nginx.org/en/docs/)
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
# Build stage - using standard debian-based node image for better compatibility
|
||||
FROM node:20 AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package files
|
||||
COPY package*.json ./
|
||||
|
||||
# Install dependencies
|
||||
# Note: strict-ssl is disabled to handle potential certificate issues in some build environments
|
||||
RUN npm config set strict-ssl false && npm install
|
||||
|
||||
# Copy source files
|
||||
COPY . .
|
||||
|
||||
# Increase Node memory to prevent out-of-memory errors during build
|
||||
ENV NODE_OPTIONS=--max-old-space-size=4096
|
||||
|
||||
|
||||
# Build the application
|
||||
RUN npm run build
|
||||
|
||||
# Production stage
|
||||
FROM nginx:alpine
|
||||
|
||||
# Copy built files from builder stage
|
||||
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||
|
||||
# Copy nginx configuration
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
# Expose port 80
|
||||
EXPOSE 80
|
||||
|
||||
# Start nginx
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
@@ -6,7 +6,7 @@
|
||||
<img src="https://img.shields.io/badge/Start%20Building!-gray.svg?logo=data:image/svg+xml;base64,PHN2ZyBmaWxsPSIjRkZENzAwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMiIgaGVpZ2h0PSIxMiIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMTMgMyBMMiAxMyBoNyBMMTEgMjEgTDIyIDExIGgtNyBMIDEzIDMgeiIvPjwvc3ZnPg==" alt="Start Building!" />
|
||||
</a>
|
||||
|
||||
<a href="https://discord.com/invite/DsN8RcPR6Y">
|
||||
<a href="https://discord.com/invite/4dv26jR4Pj">
|
||||
<img src="https://img.shields.io/discord/1352085267535761448?color=5865F2&label=Discord&logo=discord&logoColor=white" alt="Discord community channel" />
|
||||
</a>
|
||||
<a href="https://x.com/intent/follow?screen_name=Iam_The_Dev">
|
||||
@@ -45,18 +45,36 @@ And more coming very soon!
|
||||
|
||||
Use the [cloud version](https://www.stackrender.io) or deploy locally to start designing your database schemas in minutes.
|
||||
|
||||
### How to Use
|
||||
### How to Use Locally
|
||||
|
||||
#### Using Docker (Recommended)
|
||||
The easiest way to run StackRender locally is using Docker:
|
||||
|
||||
```bash
|
||||
# Build and run using Docker Compose
|
||||
docker-compose up
|
||||
|
||||
# Or build and run using Docker directly
|
||||
docker build -t stackrender .
|
||||
docker run -p 8080:80 stackrender
|
||||
```
|
||||
|
||||
Then visit `http://localhost:8080` in your browser.
|
||||
|
||||
#### Using Node.js
|
||||
Install dependencies and start the development server:
|
||||
```text
|
||||
```bash
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### How to Build
|
||||
Install dependencies and start the production build:
|
||||
```text
|
||||
Install dependencies and create a production build:
|
||||
```bash
|
||||
npm install
|
||||
npm run start
|
||||
npm run build
|
||||
```
|
||||
|
||||
## Try Using It in the Cloud
|
||||
|
||||
1. Visit [StackRender.io](https://www.stackrender.io)
|
||||
@@ -67,8 +85,8 @@ npm run start
|
||||
6. Export your Database SQL DDL and run it!
|
||||
|
||||
## 🤝 Community
|
||||
|
||||
- [**Discord**](https://discord.com/invite/DsN8RcPR6Y) – Live discussions about upcoming releases and the future of StackRender.
|
||||
- [**Docs**](https://www.stackrender.io/docs) – Explore the StackRender docs to learn more about its features.
|
||||
- [**Discord**](https://discord.com/invite/4dv26jR4Pj) – Live discussions about upcoming releases and the future of StackRender.
|
||||
- [**GitHub Issues**](https://github.com/stackrender/stackrender/issues) – Report bugs and errors to help us improve your experience.
|
||||
- [**X (Twitter)**](https://x.com/intent/follow?screen_name=Iam_The_Dev) – Get the latest StackRender news and updates.
|
||||
|
||||
@@ -76,7 +94,7 @@ npm run start
|
||||
|
||||
We welcome all contributions, whether small bug fixes or major feature additions.
|
||||
|
||||
- Discuss your ideas and contributions in our [Discord](https://discord.com/invite/DsN8RcPR6Y) community.
|
||||
- Discuss your ideas and contributions in our [Discord](https://discord.com/invite/4dv26jR4Pj) community.
|
||||
- Follow the [Contributing Guide](./CONTRIBUTING.md) to get started.
|
||||
- Agree to the [Code of Conduct](./CODE_OF_CONDUCT.md) to ensure a positive and respectful environment.
|
||||
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ We currently provide security updates for the latest stable release of StackRend
|
||||
## Reporting a Vulnerability
|
||||
If you discover a security vulnerability in StackRender, please report it to us directly.
|
||||
|
||||
- Email: security@stackrender.com
|
||||
- Email: contact@stackrender.io
|
||||
- GitHub Security Advisory: Use the "Report a vulnerability" option in the repository's Security tab.
|
||||
|
||||
When reporting, please include:
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "new-york",
|
||||
"rsc": false,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"css": "src/index.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"iconLibrary": "lucide",
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks",
|
||||
"providers": "@/providers"
|
||||
},
|
||||
"registries": {}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
services:
|
||||
stackrender:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
image: stackrender:latest
|
||||
container_name: stackrender
|
||||
ports:
|
||||
- "8080:80"
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:80/"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
+31
-26
@@ -1,28 +1,33 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>StackRender Studio</title>
|
||||
<meta key="title" content="StackRender Studio" property="og:title" />
|
||||
<meta
|
||||
content="StackRender is an AI-powered tool that helps you design and edit database diagrams effortlessly with a smooth, beautiful UI. Generate clean SQL DDL for PostgreSQL, MySQL, MariaDB, and SQLite in seconds."
|
||||
property="og:description"
|
||||
/>
|
||||
<meta
|
||||
content="StackRender is an AI-powered tool that helps you design and edit database diagrams effortlessly with a smooth, beautiful UI. Generate clean SQL DDL for PostgreSQL, MySQL, MariaDB, and SQLite in seconds."
|
||||
name="description"
|
||||
/>
|
||||
<meta
|
||||
key="viewport"
|
||||
content="viewport-fit=cover, width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"
|
||||
name="viewport"
|
||||
/>
|
||||
<link href="/favicon.png" rel="icon" sizes="32x32" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>StackRender Studio</title>
|
||||
<meta key="title" content="StackRender Studio" property="og:title" />
|
||||
<meta
|
||||
content="StackRender is an AI-powered tool that helps you design and edit database diagrams effortlessly with a smooth, beautiful UI. Generate clean SQL DDL for PostgreSQL, MySQL, MariaDB, and SQLite in seconds."
|
||||
property="og:description" />
|
||||
<meta
|
||||
content="StackRender is an AI-powered tool that helps you design and edit database diagrams effortlessly with a smooth, beautiful UI. Generate clean SQL DDL for PostgreSQL, MySQL, MariaDB, and SQLite in seconds."
|
||||
name="description" />
|
||||
<meta key="viewport"
|
||||
content="viewport-fit=cover, width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"
|
||||
name="viewport" />
|
||||
<link href="/favicon.png" rel="icon" sizes="32x32" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&family=Manrope:wght@200..800&display=swap"
|
||||
rel="stylesheet" />
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
# Gzip compression
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_min_length 1024;
|
||||
gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json;
|
||||
|
||||
# Security headers
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
|
||||
# Handle client-side routing
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
# Cache static assets
|
||||
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|wasm)$ {
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
}
|
||||
Generated
+3238
-8139
File diff suppressed because it is too large
Load Diff
+34
-11
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "vite-template",
|
||||
"name": "StackRender",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"version": "1.0.2",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -13,19 +13,39 @@
|
||||
"@codemirror/lang-sql": "^6.9.0",
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@heroui/react": "^2.7.6",
|
||||
"@internationalized/date": "^3.8.2",
|
||||
"@nextui-org/react": "^2.6.11",
|
||||
"@powersync/drizzle-driver": "^0.4.0",
|
||||
"@powersync/react": "^1.5.3",
|
||||
"@powersync/web": "^1.20.0",
|
||||
"@radix-ui/react-tooltip": "^1.2.3",
|
||||
"@radix-ui/react-accordion": "^1.2.12",
|
||||
"@radix-ui/react-avatar": "^1.1.10",
|
||||
"@radix-ui/react-collapsible": "^1.1.12",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||
"@radix-ui/react-label": "^2.1.7",
|
||||
"@radix-ui/react-menubar": "^1.1.16",
|
||||
"@radix-ui/react-popover": "^1.1.15",
|
||||
"@radix-ui/react-radio-group": "^1.3.8",
|
||||
"@radix-ui/react-scroll-area": "^1.2.10",
|
||||
"@radix-ui/react-select": "^2.2.6",
|
||||
"@radix-ui/react-separator": "^1.1.7",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-switch": "^1.2.6",
|
||||
"@radix-ui/react-toggle": "^1.1.10",
|
||||
"@radix-ui/react-toggle-group": "^1.1.11",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@react-aria/visually-hidden": "3.8.21",
|
||||
"@react-types/shared": "3.28.0",
|
||||
"@reecelucas/react-use-hotkeys": "^2.0.0",
|
||||
"@types/react-resizable": "^3.0.8",
|
||||
"@tabler/icons-react": "^3.34.1",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@tailwindcss/vite": "^4.1.12",
|
||||
"@uiw/react-codemirror": "^4.23.12",
|
||||
"@xyflow/react": "^12.6.3",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"drizzle-orm": "^0.43.1",
|
||||
@@ -35,23 +55,26 @@
|
||||
"i18next-browser-languagedetector": "^8.0.5",
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.501.0",
|
||||
"next-themes": "^0.4.6",
|
||||
"node-sql-parser": "^5.3.9",
|
||||
"object-hash": "^3.0.0",
|
||||
"pgsql-ast-parser": "^12.0.1",
|
||||
"pluralize": "^8.0.0",
|
||||
"react": "18.3.1",
|
||||
"react-day-picker": "^9.10.0",
|
||||
"react-dom": "18.3.1",
|
||||
"react-i18next": "^15.5.1",
|
||||
"react-resizable": "^3.0.5",
|
||||
"react-resizable-panels": "^2.1.9",
|
||||
"react-router-dom": "6.23.0",
|
||||
"react-tag-input": "^6.10.6",
|
||||
"sonner": "^2.0.7",
|
||||
"sql-formatter": "^15.6.3",
|
||||
"tailwind-merge": "^3.2.0",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tailwind-variants": "0.3.0",
|
||||
"tailwindcss": "3.4.16",
|
||||
"tailwindcss": "^4.1.13",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"use-undo": "^1.1.1",
|
||||
"uuid": "^11.1.0"
|
||||
"uuid": "^11.1.0",
|
||||
"vaul": "^1.1.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/better-sqlite3": "^7.6.13",
|
||||
@@ -73,8 +96,8 @@
|
||||
"eslint-plugin-react": "^7.23.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-unused-imports": "4.1.4",
|
||||
"postcss": "8.4.38",
|
||||
"prettier": "3.3.3",
|
||||
"tw-animate-css": "^1.3.6",
|
||||
"typescript": "5.6.3",
|
||||
"vite": "^5.2.0",
|
||||
"vite-plugin-top-level-await": "^1.5.0",
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 4.8 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 79 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 157 KiB After Width: | Height: | Size: 75 KiB |
+3
-9
@@ -1,14 +1,12 @@
|
||||
|
||||
|
||||
import "@/styles/globals.css"
|
||||
import { TooltipProvider } from "./components/tooltip/tooltip";
|
||||
|
||||
import { ReactFlowProvider } from "@xyflow/react";
|
||||
import useAppRoutes from "./routes/app-route";
|
||||
import { SyncProvider } from "./providers/sync-provider/sync-provider";
|
||||
import DatabaseProvider from "./providers/database-provider/database-provider";
|
||||
import DiagramProvider from "./providers/diagram-provider/diagram-provider";
|
||||
|
||||
import { ToastProvider } from "@heroui/react";
|
||||
import { ModalProvider } from "./providers/modal-provider/modal-provider";
|
||||
import DatabaseHotkeysProvider from "./providers/database-hotkeys/database-hotkeys-provider";
|
||||
|
||||
@@ -18,21 +16,17 @@ function App() {
|
||||
|
||||
const appRoutes = useAppRoutes();
|
||||
return (
|
||||
<>
|
||||
<ToastProvider placement="bottom-right" />
|
||||
<>
|
||||
<SyncProvider>
|
||||
<ReactFlowProvider>
|
||||
<DatabaseProvider>
|
||||
<DiagramProvider>
|
||||
|
||||
<TooltipProvider delayDuration={0}>
|
||||
|
||||
<ModalProvider>
|
||||
<DatabaseHotkeysProvider>
|
||||
{appRoutes}
|
||||
</DatabaseHotkeysProvider>
|
||||
</ModalProvider>
|
||||
</TooltipProvider>
|
||||
|
||||
</DiagramProvider>
|
||||
</DatabaseProvider>
|
||||
</ReactFlowProvider>
|
||||
|
||||
@@ -1,107 +0,0 @@
|
||||
|
||||
import { Autocomplete as HeroUiAutocomplete, AutocompleteItem, AutocompleteSection } from "@heroui/react";
|
||||
import { Key, useState } from "react";
|
||||
|
||||
interface AutocompleteProps {
|
||||
|
||||
items?: any[]
|
||||
label?: string,
|
||||
defaultSelection?: string | undefined
|
||||
onSelectionChange?: (item: any) => void,
|
||||
placeholder?: string
|
||||
isDisabled?: boolean
|
||||
selectedItem?: Key,
|
||||
grouped?: boolean
|
||||
}
|
||||
|
||||
|
||||
const headingClasses =
|
||||
"flex w-full py-1.5 px-2 bg-default shadow-md border-1 font-medium rounded-md dark:border-font/5 dark:bg-background-100";
|
||||
|
||||
|
||||
const Autocomplete: React.FC<AutocompleteProps> = ({ items, label = "name", onSelectionChange, defaultSelection, placeholder, isDisabled, selectedItem, grouped = false }) => {
|
||||
|
||||
|
||||
|
||||
const onItemChange = (item: Key | null) => {
|
||||
onSelectionChange && onSelectionChange(item);
|
||||
}
|
||||
if (!grouped)
|
||||
return (
|
||||
<HeroUiAutocomplete
|
||||
radius="sm"
|
||||
isDisabled={isDisabled}
|
||||
|
||||
defaultItems={items || []}
|
||||
size="sm"
|
||||
onSelectionChange={onItemChange}
|
||||
defaultSelectedKey={defaultSelection as any}
|
||||
variant="bordered"
|
||||
aria-label={placeholder}
|
||||
placeholder={placeholder}
|
||||
selectedKey={selectedItem as any}
|
||||
className="h-8 w-full focus-visible:ring-0 shadow-none "
|
||||
classNames={{
|
||||
clearButton: "text-icon",
|
||||
selectorButton: "text-icon",
|
||||
popoverContent: "rounded-md "
|
||||
}}
|
||||
inputProps={{
|
||||
classNames: {
|
||||
inputWrapper: " border-divider group-hover:border-primary group-data-[focus=true]:border-primary",
|
||||
},
|
||||
}}
|
||||
|
||||
>
|
||||
{(item: any) => <AutocompleteItem key={item.id}>{item[label]}</AutocompleteItem>}
|
||||
</HeroUiAutocomplete>
|
||||
)
|
||||
if (grouped) {
|
||||
return <HeroUiAutocomplete
|
||||
radius="sm"
|
||||
isDisabled={isDisabled}
|
||||
size="sm"
|
||||
onSelectionChange={onItemChange}
|
||||
defaultSelectedKey={defaultSelection as any}
|
||||
variant="bordered"
|
||||
aria-label={placeholder}
|
||||
placeholder={placeholder}
|
||||
className="h-8 w-full focus-visible:ring-0 shadow-none "
|
||||
selectedKey={selectedItem as any}
|
||||
classNames={{
|
||||
clearButton: "text-icon",
|
||||
selectorButton: "text-icon",
|
||||
popoverContent: "rounded-md "
|
||||
}}
|
||||
inputProps={{
|
||||
classNames: {
|
||||
inputWrapper: "border-divider group-hover:border-primary group-data-[focus=true]:border-primary",
|
||||
},
|
||||
}}
|
||||
|
||||
>
|
||||
{
|
||||
items ? Object.keys(items).map((key: string) => (
|
||||
<AutocompleteSection
|
||||
key={key.toUpperCase()}
|
||||
classNames={{
|
||||
heading: headingClasses,
|
||||
}}
|
||||
title={key.toUpperCase()}
|
||||
>
|
||||
{
|
||||
items[key as any].map((item: any) => (
|
||||
<AutocompleteItem key={item.id}>{item[label]}</AutocompleteItem>
|
||||
))
|
||||
}
|
||||
</AutocompleteSection>
|
||||
|
||||
)) : []
|
||||
}
|
||||
|
||||
|
||||
</HeroUiAutocomplete>
|
||||
}
|
||||
}
|
||||
|
||||
export default Autocomplete;
|
||||
@@ -1,37 +0,0 @@
|
||||
import { DatabaseType } from "@/lib/database"
|
||||
import { Checkbox, Image } from "@heroui/react";
|
||||
|
||||
|
||||
|
||||
|
||||
interface DatabaseCheckboxProps {
|
||||
database: DatabaseType,
|
||||
}
|
||||
|
||||
const DatabaseCheckbox: React.FC<DatabaseCheckboxProps> = ({ database }) => {
|
||||
|
||||
return (
|
||||
<Checkbox
|
||||
aria-label={database.name}
|
||||
value={database.dialect}
|
||||
className="database-checkbox"
|
||||
classNames={{
|
||||
base: "flex min-w-[128px] min-h-[128px] max-w-[128px] max-h-[128px] hover:bg-default rounded-md relative data-[selected=true]:border-1 data-[selected=true]:border-primary data-[selected=true]:bg-primary/5",
|
||||
wrapper: "absolute top-2 left-2 before:border-divider group-data-[hover=true]:before:bg-default " ,
|
||||
label : "flex items-center justify-center w-full h-full p-2 "
|
||||
}}
|
||||
|
||||
|
||||
>
|
||||
<div className="min-w-full h-full flex items-center justify-center ">
|
||||
<Image
|
||||
src={database.logo}
|
||||
className="w-full rounded-none"
|
||||
/>
|
||||
</div>
|
||||
</Checkbox>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export default DatabaseCheckbox;
|
||||
@@ -1,54 +0,0 @@
|
||||
|
||||
import { Checkbox, Chip, cn, useCheckbox } from "@heroui/react";
|
||||
import { useTheme } from "next-themes";
|
||||
import React from "react";
|
||||
|
||||
interface OptionCheckboxProps {
|
||||
value: string;
|
||||
label: string;
|
||||
icon?: React.ReactNode;
|
||||
logo?: string;
|
||||
isSelected?: boolean;
|
||||
}
|
||||
|
||||
|
||||
const OptionCheckbox: React.FC<OptionCheckboxProps> = (props) => {
|
||||
const { value, icon, logo, label, isSelected } = props;
|
||||
|
||||
let variant : any = isSelected ? {
|
||||
variant: "solid",
|
||||
color: "default"
|
||||
} : {
|
||||
variant: "borderd",
|
||||
color: "default"
|
||||
}
|
||||
|
||||
return (
|
||||
<Checkbox
|
||||
aria-label={value}
|
||||
value={value}
|
||||
size="sm"
|
||||
className="option-checkbox"
|
||||
classNames={{
|
||||
wrapper: "hidden",
|
||||
label: "flex items-center justify-center w-full h-full "
|
||||
}}
|
||||
>
|
||||
<Chip radius="sm" {...variant as any} className={cn("dark:bg-background px-3 h-9 border-1 transition-all duration-300 border-divider text-font/90 ",
|
||||
!isSelected ? "dark:bg-default" : undefined
|
||||
)}
|
||||
avatar={logo ? <img src={logo} height={12} /> : undefined}
|
||||
startContent={
|
||||
icon
|
||||
}
|
||||
>
|
||||
<span className="text-xs font-medium ">
|
||||
{label}
|
||||
</span>
|
||||
</Chip>
|
||||
</Checkbox>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export default React.memo(OptionCheckbox);
|
||||
@@ -1,8 +1,14 @@
|
||||
import React, { useCallback, useState } from "react";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "../tooltip/tooltip";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button } from "@heroui/react";
|
||||
|
||||
|
||||
import { Copy, CopyCheck } from "lucide-react";
|
||||
import { Tooltip , TooltipContent, TooltipTrigger} from "./ui/tooltip";
|
||||
|
||||
|
||||
import { Button } from "./ui/button";
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -17,6 +23,8 @@ const Clipboard: React.FC<ClipboardProps> = ({ text }) => {
|
||||
const [isCopied, setIsCopied] = useState<boolean>(false);
|
||||
|
||||
const copyToClipboard = useCallback(async () => {
|
||||
|
||||
|
||||
if (text)
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
@@ -34,14 +42,14 @@ const Clipboard: React.FC<ClipboardProps> = ({ text }) => {
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<TooltipTrigger asChild>
|
||||
<span>
|
||||
<Button
|
||||
size="sm"
|
||||
isIconOnly
|
||||
variant="bordered"
|
||||
className="text-font/90 border-1 border-divider"
|
||||
onPressEnd={copyToClipboard}
|
||||
size="icon"
|
||||
|
||||
variant="outline"
|
||||
className="size-8"
|
||||
onClick={copyToClipboard}
|
||||
>
|
||||
{
|
||||
!isCopied ? <Copy className="size-4" /> : <CopyCheck className="size-4" />
|
||||
@@ -1,9 +1,10 @@
|
||||
import { colorOptions } from "@/lib/colors";
|
||||
import { Button, Popover, PopoverContent, PopoverTrigger } from "@heroui/react";
|
||||
import { Slash } from "lucide-react";
|
||||
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "../tooltip/tooltip";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";
|
||||
import { Button } from "./ui/button";
|
||||
import { IconSlash } from "@tabler/icons-react";
|
||||
|
||||
interface ColorPickerProps {
|
||||
defaultColor?: string;
|
||||
@@ -15,11 +16,12 @@ interface ColorPickerProps {
|
||||
const ColorPicker: React.FC<ColorPickerProps> = ({ defaultColor, onChange }) => {
|
||||
const [color, setColor] = useState<string | undefined>(defaultColor as string);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
setColor(defaultColor) ;
|
||||
} , [defaultColor])
|
||||
setColor(defaultColor);
|
||||
}, [defaultColor])
|
||||
|
||||
const onSelect = useCallback((color: string | undefined) => {
|
||||
setIsOpen(false);
|
||||
@@ -28,6 +30,44 @@ const ColorPicker: React.FC<ColorPickerProps> = ({ defaultColor, onChange }) =>
|
||||
}, [onChange])
|
||||
|
||||
return (
|
||||
<Popover open={isOpen} onOpenChange={setIsOpen}>
|
||||
<PopoverTrigger asChild >
|
||||
<Button
|
||||
size="sm"
|
||||
className="size-7 cursor-pointer rounded-md border-1 transition-shadow hover:shadow-md border-border rounded-sm bg-secondary hover:bg-secondary"
|
||||
style={{
|
||||
backgroundColor: color ? color : undefined
|
||||
}}
|
||||
>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-2 w-fit">
|
||||
<div className="grid grid-cols-4 gap-2">
|
||||
{colorOptions.map(color => (
|
||||
<div
|
||||
key={color}
|
||||
className="size-7 cursor-pointer rounded-sm border-1 transition-shadow hover:shadow-md border-border"
|
||||
style={{
|
||||
backgroundColor: color
|
||||
}}
|
||||
onClick={() => onSelect(color)}
|
||||
>
|
||||
</div>
|
||||
))}
|
||||
<div
|
||||
key={undefined}
|
||||
className="size-7 cursor-pointer rounded-sm border-2 border-border transition-shadow hover:shadow-md bg-background dark:bg-secondary"
|
||||
onClick={() => onSelect(undefined)}
|
||||
>
|
||||
<IconSlash className="size-8 text-destructive -ml-1 -mt-1" />
|
||||
</div>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
|
||||
|
||||
/*
|
||||
<Popover placement="bottom" radius="sm" isOpen={isOpen} onOpenChange={(open) => setIsOpen(open)} shadow="sm">
|
||||
<PopoverTrigger>
|
||||
<Button size="sm"
|
||||
@@ -41,42 +81,10 @@ const ColorPicker: React.FC<ColorPickerProps> = ({ defaultColor, onChange }) =>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent>
|
||||
<div className="grid grid-cols-4 gap-2">
|
||||
{colorOptions.map(color => (
|
||||
<div
|
||||
key={color}
|
||||
className="size-8 cursor-pointer rounded-md border-2 transition-shadow hover:shadow-md border-divider"
|
||||
style={{
|
||||
backgroundColor: color
|
||||
}}
|
||||
onClick={() => onSelect(color)}
|
||||
>
|
||||
|
||||
</div>
|
||||
))}
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<div
|
||||
key={undefined}
|
||||
className="size-8 cursor-pointer rounded-md border-2 border-muted transition-shadow hover:shadow-md dark:border-default-100"
|
||||
style={{
|
||||
backgroundColor: "white"
|
||||
}}
|
||||
onClick={() => onSelect(undefined)}
|
||||
>
|
||||
<Slash className="size-full text-danger-500" />
|
||||
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{t("color_picker.default_color")}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
*/
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { CheckIcon, ChevronsUpDownIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@/components/ui/command"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover"
|
||||
|
||||
|
||||
|
||||
interface ComboboxProps {
|
||||
items: any[]
|
||||
label?: string;
|
||||
placeholder?: string;
|
||||
selectedItem?: string;
|
||||
onSelectionChange?: (id: string) => void;
|
||||
className?: string;
|
||||
isDisabled?: boolean
|
||||
}
|
||||
|
||||
|
||||
export const Combobox = (props: ComboboxProps) => {
|
||||
|
||||
const { items, label = "label", placeholder, onSelectionChange, selectedItem, className, isDisabled } = props;
|
||||
const [query, setQuery] = React.useState<string>("");
|
||||
|
||||
const [open, setOpen] = React.useState(false)
|
||||
const [value, setValue] = React.useState<string | undefined>("")
|
||||
|
||||
React.useEffect(() => {
|
||||
setValue(selectedItem);
|
||||
}, [selectedItem]);
|
||||
|
||||
React.useEffect(() => {
|
||||
onSelectionChange && onSelectionChange(value as string);
|
||||
}, [value])
|
||||
|
||||
|
||||
React.useEffect(() => {
|
||||
if (open)
|
||||
setQuery("");
|
||||
}, [open])
|
||||
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild disabled={isDisabled}>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={open}
|
||||
className={cn("justify-between", className)}
|
||||
>
|
||||
<label className="truncate">
|
||||
{
|
||||
value
|
||||
? items.find((item) => item.id === value)?.[label]
|
||||
: (placeholder ? placeholder : "Select items...")}
|
||||
</label>
|
||||
<ChevronsUpDownIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[200px] p-0">
|
||||
<Command
|
||||
filter={(value, search) => {
|
||||
|
||||
return 1
|
||||
}}
|
||||
|
||||
>
|
||||
<CommandInput placeholder={placeholder ? placeholder : "Select items..."}
|
||||
|
||||
onValueChange={setQuery}
|
||||
|
||||
/>
|
||||
<CommandList>
|
||||
<CommandEmpty>No type found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{items.filter((item: any) => (item[label] as string).toLocaleLowerCase().includes(query.toLocaleLowerCase())).map((item) => (
|
||||
<CommandItem
|
||||
key={item.id}
|
||||
value={item.id}
|
||||
onSelect={(currentValue) => {
|
||||
setValue(currentValue);
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
<CheckIcon
|
||||
className={cn(
|
||||
"mr-2 h-4 w-4",
|
||||
value === item.id ? "opacity-100" : "opacity-0"
|
||||
)}
|
||||
/>
|
||||
{item[label]}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
import React from 'react'
|
||||
|
||||
import {
|
||||
IconArrowRightDashed,
|
||||
IconChevronRight,
|
||||
IconDeviceLaptop,
|
||||
IconMoon,
|
||||
IconSun,
|
||||
} from '@tabler/icons-react'
|
||||
import { useSearch } from '@/providers/search-provider/search-provider'
|
||||
import { useTheme } from '@/providers/theme-provider/theme-provider'
|
||||
import {
|
||||
CommandDialog,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
CommandSeparator,
|
||||
} from '@/components/ui/command'
|
||||
import { useSidebarData } from '@/components/layout/app-sidebar/data/sidebar-data'
|
||||
import { ScrollArea } from './ui/scroll-area'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export function CommandMenu() {
|
||||
const navigate = useNavigate()
|
||||
const { setTheme } = useTheme()
|
||||
const { open, setOpen } = useSearch()
|
||||
|
||||
const runCommand = React.useCallback(
|
||||
(command: () => unknown) => {
|
||||
setOpen(false)
|
||||
command()
|
||||
},
|
||||
[setOpen]
|
||||
)
|
||||
|
||||
|
||||
const { t } = useTranslation() ;
|
||||
|
||||
|
||||
const sidebarData = useSidebarData();
|
||||
|
||||
return (
|
||||
<CommandDialog modal open={open} onOpenChange={setOpen}>
|
||||
<CommandInput placeholder={t("navbar.command")} />
|
||||
<CommandList>
|
||||
<ScrollArea type='hover' className='h-72 pr-1'>
|
||||
<CommandEmpty>No results found.</CommandEmpty>
|
||||
{sidebarData.navGroups.map((group: any) => (
|
||||
<CommandGroup key={group.title} heading={group.title}>
|
||||
{group.items.map((navItem: any, i: number) => {
|
||||
if (navItem.url)
|
||||
return (
|
||||
<CommandItem
|
||||
key={`${navItem.url}-${i}`}
|
||||
value={navItem.title}
|
||||
onSelect={() => {
|
||||
runCommand(() => navigate(navItem.url))
|
||||
}}
|
||||
>
|
||||
<div className='mr-2 flex h-4 w-4 items-center justify-center'>
|
||||
<IconArrowRightDashed className='text-muted-foreground/80 size-2' />
|
||||
</div>
|
||||
{navItem.title}
|
||||
</CommandItem>
|
||||
)
|
||||
|
||||
return navItem.items?.map((subItem: any, i: number) => (
|
||||
<CommandItem
|
||||
key={`${navItem.title}-${subItem.url}-${i}`}
|
||||
value={`${navItem.title}-${subItem.url}`}
|
||||
onSelect={() => {
|
||||
runCommand(() => navigate(subItem.url))
|
||||
}}
|
||||
>
|
||||
<div className='mr-2 flex h-4 w-4 items-center justify-center'>
|
||||
<IconArrowRightDashed className='text-muted-foreground/80 size-2' />
|
||||
</div>
|
||||
{navItem.title} <IconChevronRight /> {subItem.title}
|
||||
</CommandItem>
|
||||
))
|
||||
})}
|
||||
</CommandGroup>
|
||||
))}
|
||||
<CommandSeparator />
|
||||
<CommandGroup heading='Theme'>
|
||||
<CommandItem onSelect={() => runCommand(() => setTheme('light'))}>
|
||||
<IconSun /> <span>{t("menu.light")}</span>
|
||||
</CommandItem>
|
||||
<CommandItem onSelect={() => runCommand(() => setTheme('dark'))}>
|
||||
<IconMoon className='scale-90' />
|
||||
<span>{t("menu.dark")}</span>
|
||||
</CommandItem>
|
||||
<CommandItem onSelect={() => runCommand(() => setTheme('system'))}>
|
||||
<IconDeviceLaptop />
|
||||
<span>{t("menu.system")}</span>
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
</ScrollArea>
|
||||
</CommandList>
|
||||
</CommandDialog>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
|
||||
|
||||
import * as React from "react"
|
||||
import { ChevronDownIcon } from "lucide-react"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Calendar } from "@/components/ui/calendar"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover"
|
||||
|
||||
|
||||
interface DatePickerProps {
|
||||
value?: Date | undefined;
|
||||
onValueChange?: (value?: Date) => void
|
||||
}
|
||||
|
||||
const DatePicker: React.FC<DatePickerProps> = ({value, onValueChange}) => {
|
||||
const [open, setOpen] = React.useState(false)
|
||||
const [date, setDate] = React.useState<Date | undefined>(value)
|
||||
|
||||
React.useEffect(() => {
|
||||
onValueChange && onValueChange(date)
|
||||
}, [date])
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
<Label htmlFor="date" className="px-1">
|
||||
Date of birth
|
||||
</Label>
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
id="date"
|
||||
className="w-full justify-between font-normal"
|
||||
>
|
||||
{date ? date.toLocaleDateString() : "Select date"}
|
||||
<ChevronDownIcon />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto overflow-hidden p-0" align="start">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={date}
|
||||
captionLayout="dropdown"
|
||||
onSelect={(date) => {
|
||||
setDate(date)
|
||||
setOpen(false)
|
||||
}}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export { DatePicker }
|
||||
@@ -1,28 +1,17 @@
|
||||
import { Image } from "@heroui/react";
|
||||
import { useTheme } from "next-themes";
|
||||
|
||||
|
||||
|
||||
|
||||
interface EmptyListProps {
|
||||
title?: string;
|
||||
description?: string;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
const EmptyList: React.FC<EmptyListProps> = ({ title, description }) => {
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-1 h-full pt-[86px] main-linear-background rounded-lg">
|
||||
{
|
||||
<Image
|
||||
<img
|
||||
width={64}
|
||||
src={"/stackrender.png"}
|
||||
|
||||
/>
|
||||
}
|
||||
{
|
||||
@@ -32,7 +21,6 @@ const EmptyList: React.FC<EmptyListProps> = ({ title, description }) => {
|
||||
</h1>
|
||||
}
|
||||
{
|
||||
|
||||
description &&
|
||||
<p className="text-sm text-font/70">
|
||||
{description}
|
||||
@@ -0,0 +1,95 @@
|
||||
import React, { useCallback } from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { SidebarTrigger } from '@/components/ui/sidebar'
|
||||
import Menu from './layout/menu/menu'
|
||||
import { Search } from './search'
|
||||
import { Button } from './ui/button'
|
||||
|
||||
//import { ProfileDropdown } from './profile-dropdown'
|
||||
|
||||
import { useModal } from '@/providers/modal-provider/modal-provider'
|
||||
import { Modals } from '@/providers/modal-provider/modal-contxet'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ThemeSwitch } from './theme-switch'
|
||||
import RenameDb from '@/features/database/components/rename-db'
|
||||
import LanguagesDropdown from './languages-dropdown'
|
||||
import { useIsMobile } from '@/hooks/use-mobile'
|
||||
|
||||
interface HeaderProps extends React.HTMLAttributes<HTMLElement> {
|
||||
fixed?: boolean
|
||||
ref?: React.Ref<HTMLElement>
|
||||
}
|
||||
|
||||
export const Header = ({
|
||||
className,
|
||||
fixed,
|
||||
children,
|
||||
...props
|
||||
}: HeaderProps) => {
|
||||
const [offset, setOffset] = React.useState(0)
|
||||
|
||||
React.useEffect(() => {
|
||||
const onScroll = () => {
|
||||
setOffset(document.body.scrollTop || document.documentElement.scrollTop)
|
||||
}
|
||||
// Add scroll listener to the body
|
||||
document.addEventListener('scroll', onScroll, { passive: true })
|
||||
// Clean up the event listener on unmount
|
||||
return () => document.removeEventListener('scroll', onScroll)
|
||||
}, []);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const isMobile = useIsMobile();
|
||||
return (
|
||||
<header
|
||||
className={cn(
|
||||
'bg-background flex-col md:flex items-center gap-3 p-2 sm:gap-4 ',
|
||||
fixed && 'header-fixed peer/header fixed z-50 w-[inherit] rounded-md ',
|
||||
offset > 10 && fixed ? 'shadow-sm' : 'shadow-none',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className='grid grid-cols-3 items-center w-full '>
|
||||
<div className="justify-self-start flex h-8 gap-3 items-center ">
|
||||
<SidebarTrigger variant='outline' className='scale-125 sm:scale-100' />
|
||||
<Separator orientation='vertical' className='h-6' />
|
||||
{!isMobile && <Menu />}
|
||||
</div>
|
||||
|
||||
|
||||
<div className=" justify-self-center ">
|
||||
<Search className='hidden lg:flex' placeholder={t("navbar.search")} />
|
||||
{
|
||||
isMobile &&
|
||||
<div className='w-full'>
|
||||
<RenameDb />
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div className="justify-self-end flex gap-2">
|
||||
{
|
||||
!isMobile &&
|
||||
<div className='w-full'>
|
||||
<RenameDb />
|
||||
</div>
|
||||
}
|
||||
|
||||
<ThemeSwitch />
|
||||
<LanguagesDropdown />
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{isMobile && <div className='flex justify-center'>
|
||||
<Menu />
|
||||
|
||||
</div>
|
||||
}
|
||||
</header>
|
||||
)
|
||||
}
|
||||
|
||||
Header.displayName = 'Header'
|
||||
@@ -0,0 +1,90 @@
|
||||
// input-tags.tsx
|
||||
|
||||
|
||||
|
||||
import * as React from "react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { XIcon } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type InputTagsProps = Omit<React.ComponentProps<"input">, "value" | "onChange"> & {
|
||||
value: string[];
|
||||
onChange: React.Dispatch<React.SetStateAction<string[]>>;
|
||||
};
|
||||
|
||||
const InputTags = React.forwardRef<HTMLInputElement, InputTagsProps>(
|
||||
({ className, value, onChange, ...props }, ref) => {
|
||||
const [pendingDataPoint, setPendingDataPoint] = React.useState("");
|
||||
|
||||
React.useEffect(() => {
|
||||
if (pendingDataPoint.includes(",")) {
|
||||
const newDataPoints = new Set([
|
||||
...value,
|
||||
...pendingDataPoint.split(",").map((chunk) => chunk.trim()),
|
||||
]);
|
||||
onChange(Array.from(newDataPoints));
|
||||
setPendingDataPoint("");
|
||||
}
|
||||
}, [pendingDataPoint, onChange, value]);
|
||||
|
||||
const addPendingDataPoint = () => {
|
||||
if (pendingDataPoint) {
|
||||
const newDataPoints = new Set([...value, pendingDataPoint]);
|
||||
onChange(Array.from(newDataPoints));
|
||||
setPendingDataPoint("");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
// caveat: :has() variant requires tailwind v3.4 or above: https://tailwindcss.com/blog/tailwindcss-v3-4#new-has-variant
|
||||
"has-[:focus-visible]:outline-none has-[:focus-visible]:ring-offset-2 dark:has-[:focus-visible]:ring-neutral-300 min-h-10 flex w-full flex-wrap gap-2 rounded-md border border-neutral-200 bg-white px-3 py-2 text-sm ring-offset-white disabled:cursor-not-allowed disabled:opacity-50 dark:border-border dark:bg-background ",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{value.map((item) => (
|
||||
<Badge key={item} variant="secondary">
|
||||
{item}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="ml-2 h-3 w-3"
|
||||
onClick={() => {
|
||||
onChange(value.filter((i) => i !== item));
|
||||
}}
|
||||
>
|
||||
<XIcon className="w-3" />
|
||||
</Button>
|
||||
</Badge>
|
||||
))}
|
||||
<input
|
||||
className="flex-1 outline-none "
|
||||
value={pendingDataPoint}
|
||||
onChange={(e) => setPendingDataPoint(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === ",") {
|
||||
e.preventDefault();
|
||||
addPendingDataPoint();
|
||||
} else if (
|
||||
e.key === "Backspace" &&
|
||||
pendingDataPoint.length === 0 &&
|
||||
value.length > 0
|
||||
) {
|
||||
e.preventDefault();
|
||||
onChange(value.slice(0, -1));
|
||||
}
|
||||
}}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
InputTags.displayName = "InputTags";
|
||||
|
||||
export { InputTags };
|
||||
@@ -0,0 +1,84 @@
|
||||
import { languages } from "@/i18";
|
||||
import { Language } from "@/i18/types";
|
||||
|
||||
import { Check } from "lucide-react";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
CommandShortcut,
|
||||
} from "@/components/ui/command"
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";
|
||||
import { Button } from "./ui/button";
|
||||
import { IconLanguage } from "@tabler/icons-react";
|
||||
|
||||
const LanguagesDropDonw: React.FC = ({ }) => {
|
||||
const { t, i18n } = useTranslation();
|
||||
const [currentLanguage, setCurrentLanguage] = useState<Language>(() => languages.find((language: Language) => language.code == i18n.language) as Language);
|
||||
|
||||
|
||||
const changeLanguage = useCallback((language: Language) => {
|
||||
|
||||
i18n.changeLanguage(language.code);
|
||||
}, [i18n])
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentLanguage(languages.find((language: Language) => language.code == i18n.language) as Language)
|
||||
}, [i18n.language])
|
||||
|
||||
|
||||
return (
|
||||
|
||||
|
||||
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
size={"icon"}
|
||||
variant={"outline"}
|
||||
>
|
||||
|
||||
<IconLanguage className="size-5"/>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-60 p-1 rounded-md">
|
||||
<Command className="rounded-lg border shadow-md ">
|
||||
<CommandInput
|
||||
aria-label={t("navbar.search")}
|
||||
placeholder={`${t("navbar.search")}`}
|
||||
/>
|
||||
<CommandList>
|
||||
<CommandEmpty>No language found.</CommandEmpty>
|
||||
{
|
||||
languages.map((language: Language) => (
|
||||
<CommandItem className="flex justify-between cursor-pointer" key={language.code} onSelect={() => changeLanguage(language)}>
|
||||
<span>{language.name} <span className="text-muted-foreground">({language.nativeName})</span></span>
|
||||
<CommandShortcut>
|
||||
{
|
||||
currentLanguage.code == language.code && <Check className="text-icon size-4" />
|
||||
}
|
||||
</CommandShortcut>
|
||||
</CommandItem>
|
||||
))
|
||||
}
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export default React.memo(LanguagesDropDonw);
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarFooter,
|
||||
SidebarHeader,
|
||||
SidebarRail,
|
||||
} from '@/components/ui/sidebar'
|
||||
|
||||
|
||||
import { NavGroup } from '../nav-group'
|
||||
import { useSidebarData } from './data/sidebar-data'
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { useDiagramOps } from '@/providers/diagram-provider/diagram-provider';
|
||||
|
||||
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
const sidebarData = useSidebarData();
|
||||
const { openController } = useDiagramOps();
|
||||
return (
|
||||
<Sidebar collapsible='icon' variant='floating' {...props} className='bg-card border-r' >
|
||||
<SidebarHeader>
|
||||
<div className='flex items-center'>
|
||||
<img
|
||||
className='w-8 h-8 p-[6px] rounded-md '
|
||||
src='/stackrender.png'
|
||||
/>
|
||||
<h3 className='data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground truncate font-semibold text-sm '>
|
||||
StackRender
|
||||
</h3>
|
||||
</div>
|
||||
</SidebarHeader>
|
||||
<SidebarContent>
|
||||
{sidebarData.navGroups.map((props: any, index: number) => (
|
||||
<div key={props.title}>
|
||||
<NavGroup key={props.title} {...props} onClick={() => openController(true)} />
|
||||
{
|
||||
index != sidebarData.navGroups.length - 1 &&
|
||||
<div className='px-2'>
|
||||
<Separator />
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
))}
|
||||
</SidebarContent>
|
||||
<SidebarFooter className='p-0'>
|
||||
{sidebarData.footerNavGroups.map((props: any, index: number) => (
|
||||
<div key={props.title}>
|
||||
<NavGroup key={props.title} {...props} />
|
||||
{
|
||||
index != sidebarData.navGroups.length - 1 &&
|
||||
<div className='px-2'>
|
||||
<Separator />
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
))}
|
||||
</SidebarFooter>
|
||||
<SidebarRail />
|
||||
</Sidebar>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import {
|
||||
IconBrandDiscord,
|
||||
IconBrandGithub,
|
||||
IconTableAlias,
|
||||
IconVectorSpline,
|
||||
} from '@tabler/icons-react'
|
||||
|
||||
import type { SidebarData } from '../../types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useMemo } from 'react';
|
||||
import { BookIcon } from 'lucide-react';
|
||||
|
||||
export const useSidebarData = () => {
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const sidebarData: SidebarData = useMemo(() => ({
|
||||
navGroups: [
|
||||
{
|
||||
title: t("sidebar.database"),
|
||||
items: [
|
||||
{
|
||||
title: t("sidebar.tables"),
|
||||
url: '/database/tables',
|
||||
icon: IconTableAlias,
|
||||
},
|
||||
{
|
||||
title: t("sidebar.relationships"),
|
||||
url: '/database/relationships',
|
||||
icon: IconVectorSpline,
|
||||
},
|
||||
|
||||
],
|
||||
},
|
||||
|
||||
],
|
||||
footerNavGroups:[ {
|
||||
title: '',
|
||||
items: [
|
||||
{
|
||||
title: "Discord",
|
||||
url: "https://discord.gg/4dv26jR4Pj",
|
||||
icon: IconBrandDiscord,
|
||||
newTab : true
|
||||
},
|
||||
{
|
||||
title: "Github",
|
||||
url: "https://github.com/stackrender/stackrender",
|
||||
icon: IconBrandGithub,
|
||||
newTab : true
|
||||
},
|
||||
{
|
||||
title : t("sidebar.documentation") ,
|
||||
url : "https://www.stackrender.io/docs" ,
|
||||
icon : BookIcon,
|
||||
newTab : true
|
||||
}
|
||||
|
||||
],
|
||||
}]
|
||||
}), [t])
|
||||
|
||||
return sidebarData;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Spinner } from "../ui/shadcn-io/spinner";
|
||||
|
||||
|
||||
|
||||
|
||||
interface LoadingProps {
|
||||
text?: string;
|
||||
}
|
||||
|
||||
|
||||
export const Loading: React.FC<LoadingProps> = ({ text }) => {
|
||||
|
||||
return (
|
||||
<div className="fixed bg-black/50 backdrop-opacity-disabled w-screen h-screen fixed flex items-center justify-center z-[50] left-0 top-0 text-white flex-col gap-2">
|
||||
<Spinner size={48} variant="ellipsis" className="text-primary" />
|
||||
<span className="font-medium">
|
||||
Loading ...
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,29 +1,29 @@
|
||||
|
||||
import { useTheme } from "next-themes";
|
||||
import DropdownMenu, { MenuDropdownProps } from "./menu-dropdown";
|
||||
import React, { useEffect, useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useModal } from "@/providers/modal-provider/modal-provider";
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { MenuItem } from "../../types";
|
||||
import { useMemo } from "react";
|
||||
import { Modals } from "@/providers/modal-provider/modal-contxet";
|
||||
|
||||
|
||||
import { useDatabaseHistory } from "@/providers/database-history/database-history-provider";
|
||||
import { useDatabase, useDatabaseOperations } from "@/providers/database-provider/database-provider";
|
||||
import { TableType } from "@/lib/schemas/table-schema";
|
||||
import { useModal } from "@/providers/modal-provider/modal-provider";
|
||||
import { useDiagramOps } from "@/providers/diagram-provider/diagram-provider";
|
||||
import { TableType } from "@/lib/schemas/table-schema";
|
||||
import { CardinalityStyle } from "@/lib/database";
|
||||
import { useTheme } from "@/providers/theme-provider/theme-provider";
|
||||
|
||||
|
||||
const Menu: React.FC = ({ }) => {
|
||||
|
||||
const { setTheme, resolvedTheme } = useTheme()
|
||||
export const useMenuData = () => {
|
||||
|
||||
const { t } = useTranslation();
|
||||
const { setTheme, theme } = useTheme()
|
||||
|
||||
const { open } = useModal();
|
||||
const { canRedo, canUndo, undo, redo } = useDatabaseHistory();
|
||||
const { deleteMultiTables } = useDatabaseOperations();
|
||||
const { database } = useDatabase();
|
||||
const { openController, showController, cardinalityStyle, changeCardinalityStyle } = useDiagramOps();
|
||||
|
||||
const menu: MenuDropdownProps[] = useMemo(() => [
|
||||
const menuData: MenuItem[] = useMemo(() => [
|
||||
{
|
||||
id: "menu.file",
|
||||
title: t("menu.file"),
|
||||
@@ -53,13 +53,15 @@ const Menu: React.FC = ({ }) => {
|
||||
id: "menu.export_sql",
|
||||
title: t("menu.export_sql"),
|
||||
clickHandler: () => {
|
||||
|
||||
|
||||
open(Modals.EXPORT_SQL)
|
||||
}
|
||||
},
|
||||
//{ title: t("menu.export_orm_models"), divide: true },
|
||||
{
|
||||
id: "menu.delete_project",
|
||||
title: t("menu.delete_project"), theme: "danger", clickHandler: () => {
|
||||
title: t("menu.delete_project"), theme: "destructive", clickHandler: () => {
|
||||
open(Modals.DELETE_DATABASE)
|
||||
}
|
||||
},
|
||||
@@ -93,7 +95,7 @@ const Menu: React.FC = ({ }) => {
|
||||
{
|
||||
id: "menu.symbolic",
|
||||
selected: cardinalityStyle == CardinalityStyle.SYMBOLIC,
|
||||
title: t("menu.symbolic"), clickHandler: () => {
|
||||
title: t("menu.symbolic"), clickHandler: () => {
|
||||
changeCardinalityStyle(CardinalityStyle.SYMBOLIC)
|
||||
}
|
||||
},
|
||||
@@ -108,6 +110,8 @@ const Menu: React.FC = ({ }) => {
|
||||
id: "menu.hidden",
|
||||
selected: cardinalityStyle == CardinalityStyle.HIDDEN,
|
||||
title: t("menu.hidden"), clickHandler: () => {
|
||||
|
||||
|
||||
changeCardinalityStyle(CardinalityStyle.HIDDEN)
|
||||
}
|
||||
},
|
||||
@@ -120,23 +124,21 @@ const Menu: React.FC = ({ }) => {
|
||||
openController(!showController)
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
id: "menu.theme",
|
||||
title: t("menu.theme"),
|
||||
|
||||
children: [
|
||||
{
|
||||
id: "light",
|
||||
title: t("menu.light"),
|
||||
clickHandler: () => setTheme("light"),
|
||||
selected: resolvedTheme == "light",
|
||||
selected: theme != "dark",
|
||||
},
|
||||
{
|
||||
id: "dark",
|
||||
title: t("menu.dark"),
|
||||
clickHandler: () => setTheme("dark"),
|
||||
selected: resolvedTheme == "dark"
|
||||
selected: theme == "dark"
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -146,23 +148,25 @@ const Menu: React.FC = ({ }) => {
|
||||
id: "menu.help",
|
||||
title: t("menu.help"),
|
||||
children: [
|
||||
{ id: "menu.join_discord", title: t("menu.join_discord") ,
|
||||
clickHandler : () => {
|
||||
window.open("https://discord.gg/DsN8RcPR6Y", "_blank")
|
||||
{
|
||||
id: "menu.documentation", title: t("sidebar.documentation"),
|
||||
clickHandler: () => {
|
||||
window.open("https://www.stackrender.io/docs", "_blank")
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "menu.join_discord", title: t("menu.join_discord"),
|
||||
clickHandler: () => {
|
||||
window.open("https://discord.gg/4dv26jR4Pj", "_blank")
|
||||
}
|
||||
},
|
||||
],
|
||||
},
|
||||
], [t, canRedo, canUndo, undo, redo, deleteMultiTables, database, showController, openController ,changeCardinalityStyle , cardinalityStyle, resolvedTheme]);
|
||||
|
||||
return <div className="gap-1 flex">
|
||||
{
|
||||
menu.map((menuItem, index) => (
|
||||
<DropdownMenu {...menuItem} key={menuItem.id} />
|
||||
))
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
], [t, canRedo, canUndo, undo, redo , deleteMultiTables, database, showController, openController, changeCardinalityStyle, cardinalityStyle, theme]);
|
||||
|
||||
|
||||
|
||||
export default React.memo(Menu);
|
||||
|
||||
return menuData ;
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import type React from "react";
|
||||
import {
|
||||
Menubar,
|
||||
MenubarMenu,
|
||||
MenubarTrigger,
|
||||
MenubarContent,
|
||||
MenubarItem,
|
||||
MenubarSeparator,
|
||||
MenubarShortcut,
|
||||
MenubarSub,
|
||||
MenubarSubTrigger,
|
||||
MenubarSubContent,
|
||||
} from "@/components/ui/menubar"
|
||||
import { useMenuData } from "./data/menu-data";
|
||||
import { MenuItem } from "../types";
|
||||
import { Check } from "lucide-react"
|
||||
const Menu: React.FC = ({ }) => {
|
||||
const menuData = useMenuData();
|
||||
return (
|
||||
<Menubar className="bg-card">
|
||||
{
|
||||
menuData.map((menuItem: MenuItem) => (
|
||||
<MenubarMenu key={menuItem.id}>
|
||||
<MenubarTrigger
|
||||
onClick={menuItem.clickHandler}
|
||||
>
|
||||
{menuItem.title}
|
||||
</MenubarTrigger>
|
||||
{
|
||||
menuItem.children &&
|
||||
<MenubarContent>
|
||||
{
|
||||
menuItem.children.map((subMenuItem: MenuItem) => (
|
||||
<div key={subMenuItem.id}>
|
||||
{
|
||||
!subMenuItem.children ?
|
||||
<>
|
||||
<MenubarItem
|
||||
variant={subMenuItem.theme}
|
||||
disabled={subMenuItem.isDisabled}
|
||||
onClick={subMenuItem.clickHandler}
|
||||
>
|
||||
{subMenuItem.title}
|
||||
{
|
||||
subMenuItem.shortcut &&
|
||||
<MenubarShortcut>{subMenuItem.shortcut}</MenubarShortcut>
|
||||
}
|
||||
</MenubarItem>
|
||||
{
|
||||
subMenuItem.divide &&
|
||||
<MenubarSeparator />
|
||||
}
|
||||
</>
|
||||
:
|
||||
<>
|
||||
<MenubarSub>
|
||||
<MenubarSubTrigger>{subMenuItem.title}</MenubarSubTrigger>
|
||||
|
||||
<MenubarSubContent>
|
||||
{
|
||||
subMenuItem.children.map((child: MenuItem) => (
|
||||
|
||||
<MenubarItem
|
||||
|
||||
disabled={child.isDisabled}
|
||||
onClick={child.clickHandler}
|
||||
key={child.id}
|
||||
className="flex items-center justify-between"
|
||||
|
||||
>
|
||||
{
|
||||
child.title
|
||||
}
|
||||
{
|
||||
child.selected &&
|
||||
<Check className="h-4 w-4" />
|
||||
}
|
||||
|
||||
</MenubarItem>
|
||||
))
|
||||
}
|
||||
</MenubarSubContent>
|
||||
</MenubarSub>
|
||||
{
|
||||
subMenuItem.divide &&
|
||||
<MenubarSeparator />
|
||||
}
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
|
||||
))
|
||||
}
|
||||
</MenubarContent>
|
||||
}
|
||||
</MenubarMenu>
|
||||
))
|
||||
}
|
||||
</Menubar>
|
||||
)
|
||||
}
|
||||
|
||||
export default Menu;
|
||||
@@ -0,0 +1,183 @@
|
||||
import { type ReactNode } from 'react'
|
||||
|
||||
import { Link, useLocation } from "react-router-dom";
|
||||
import { ChevronRight } from 'lucide-react'
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from '@/components/ui/collapsible'
|
||||
import {
|
||||
SidebarGroup,
|
||||
SidebarGroupLabel,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
SidebarMenuSub,
|
||||
SidebarMenuSubButton,
|
||||
SidebarMenuSubItem,
|
||||
useSidebar,
|
||||
} from '@/components/ui/sidebar'
|
||||
import { Badge } from '../ui/badge'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '../ui/dropdown-menu'
|
||||
import { NavCollapsible, NavItem, NavLink, NavGroup as NavGroupType } from './types.ts'
|
||||
|
||||
export function NavGroup({ title, items , onClick }: NavGroupType) {
|
||||
const { state, isMobile } = useSidebar();
|
||||
const location = useLocation();
|
||||
const href = location.pathname
|
||||
return (
|
||||
<SidebarGroup>
|
||||
<SidebarGroupLabel>{title}</SidebarGroupLabel>
|
||||
<SidebarMenu>
|
||||
{items.map((item) => {
|
||||
const key = `${item.title}-${item.url}`
|
||||
|
||||
if (!item.items)
|
||||
return <SidebarMenuLink key={key} item={item} href={href} target={item.newTab ? "_blank" : undefined} onClick={onClick}/>
|
||||
|
||||
if (state === 'collapsed' && !isMobile)
|
||||
return (
|
||||
<SidebarMenuCollapsedDropdown key={key} item={item} href={href} />
|
||||
)
|
||||
|
||||
return <SidebarMenuCollapsible key={key} item={item} href={href} />
|
||||
})}
|
||||
</SidebarMenu>
|
||||
</SidebarGroup>
|
||||
)
|
||||
}
|
||||
|
||||
const NavBadge = ({ children }: { children: ReactNode }) => (
|
||||
<Badge className='rounded-full px-1 py-0 text-xs'>{children}</Badge>
|
||||
)
|
||||
|
||||
const SidebarMenuLink = ({ item, href , target , onClick}: { item: NavLink; href: string; target?: string , onClick? : ()=> void }) => {
|
||||
const { setOpenMobile } = useSidebar()
|
||||
return (
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton
|
||||
asChild
|
||||
isActive={checkIsActive(href, item)}
|
||||
tooltip={item.title}
|
||||
onClick={onClick}
|
||||
>
|
||||
<Link to={item.url} onClick={() => setOpenMobile(false)} target={target}>
|
||||
{item.icon && <item.icon />}
|
||||
<span>{item.title}</span>
|
||||
{item.badge && <NavBadge>{item.badge}</NavBadge>}
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
)
|
||||
}
|
||||
|
||||
const SidebarMenuCollapsible = ({
|
||||
item,
|
||||
href,
|
||||
}: {
|
||||
item: NavCollapsible
|
||||
href: string
|
||||
}) => {
|
||||
const { setOpenMobile } = useSidebar()
|
||||
return (
|
||||
<Collapsible
|
||||
asChild
|
||||
defaultOpen={checkIsActive(href, item, true)}
|
||||
className='group/collapsible'
|
||||
>
|
||||
<SidebarMenuItem>
|
||||
<CollapsibleTrigger asChild>
|
||||
<SidebarMenuButton tooltip={item.title}>
|
||||
{item.icon && <item.icon />}
|
||||
<span>{item.title}</span>
|
||||
{item.badge && <NavBadge>{item.badge}</NavBadge>}
|
||||
<ChevronRight className='ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90' />
|
||||
</SidebarMenuButton>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className='CollapsibleContent'>
|
||||
<SidebarMenuSub>
|
||||
{item.items.map((subItem) => (
|
||||
<SidebarMenuSubItem key={subItem.title}>
|
||||
<SidebarMenuSubButton
|
||||
asChild
|
||||
isActive={checkIsActive(href, subItem)}
|
||||
>
|
||||
<Link to={subItem.url} onClick={() => setOpenMobile(false)}>
|
||||
{subItem.icon && <subItem.icon />}
|
||||
<span>{subItem.title}</span>
|
||||
{subItem.badge && <NavBadge>{subItem.badge}</NavBadge>}
|
||||
</Link>
|
||||
</SidebarMenuSubButton>
|
||||
</SidebarMenuSubItem>
|
||||
))}
|
||||
</SidebarMenuSub>
|
||||
</CollapsibleContent>
|
||||
</SidebarMenuItem>
|
||||
</Collapsible>
|
||||
)
|
||||
}
|
||||
|
||||
const SidebarMenuCollapsedDropdown = ({
|
||||
item,
|
||||
href,
|
||||
}: {
|
||||
item: NavCollapsible
|
||||
href: string
|
||||
}) => {
|
||||
return (
|
||||
<SidebarMenuItem>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<SidebarMenuButton
|
||||
tooltip={item.title}
|
||||
isActive={checkIsActive(href, item)}
|
||||
>
|
||||
{item.icon && <item.icon />}
|
||||
<span>{item.title}</span>
|
||||
{item.badge && <NavBadge>{item.badge}</NavBadge>}
|
||||
<ChevronRight className='ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90' />
|
||||
</SidebarMenuButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent side='right' align='start' sideOffset={4}>
|
||||
<DropdownMenuLabel>
|
||||
{item.title} {item.badge ? `(${item.badge})` : ''}
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
{item.items.map((sub) => (
|
||||
<DropdownMenuItem key={`${sub.title}-${sub.url}`} asChild>
|
||||
<Link
|
||||
to={sub.url}
|
||||
className={`${checkIsActive(href, sub) ? 'bg-secondary' : ''}`}
|
||||
>
|
||||
{sub.icon && <sub.icon />}
|
||||
<span className='max-w-52 text-wrap'>{sub.title}</span>
|
||||
{sub.badge && (
|
||||
<span className='ml-auto text-xs'>{sub.badge}</span>
|
||||
)}
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</SidebarMenuItem>
|
||||
)
|
||||
}
|
||||
|
||||
function checkIsActive(href: string, item: NavItem, mainNav = false) {
|
||||
return (
|
||||
href === item.url || // /endpint?search=param
|
||||
href.split('?')[0] === item.url || // endpoint
|
||||
!!item?.items?.filter((i) => i.url === href).length || // if child nav is active
|
||||
(mainNav &&
|
||||
href.split('/')[1] !== '' &&
|
||||
href.split('/')[1] === item?.url?.split('/')[1])
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
|
||||
|
||||
|
||||
|
||||
interface BaseNavItem {
|
||||
title: string
|
||||
badge?: string
|
||||
icon?: React.ElementType
|
||||
}
|
||||
|
||||
type NavLink = BaseNavItem & {
|
||||
url: string
|
||||
items?: never;
|
||||
newTab?: boolean
|
||||
}
|
||||
|
||||
type NavCollapsible = BaseNavItem & {
|
||||
items: (BaseNavItem & { url: string })[]
|
||||
url?: never
|
||||
}
|
||||
|
||||
type NavItem = NavCollapsible | NavLink
|
||||
|
||||
interface NavGroup {
|
||||
title: string
|
||||
items: NavItem[];
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
interface SidebarData {
|
||||
|
||||
|
||||
footerNavGroups: NavGroup[]
|
||||
navGroups: NavGroup[]
|
||||
}
|
||||
|
||||
|
||||
interface MenuItem {
|
||||
id: string;
|
||||
title?: string;
|
||||
children?: MenuItem[];
|
||||
theme?: "default" | "destructive",
|
||||
isDisabled?: boolean,
|
||||
divide?: boolean,
|
||||
shortcut?: string,
|
||||
isOpen?: boolean,
|
||||
selected?: boolean;
|
||||
clickHandler?: () => void
|
||||
}
|
||||
|
||||
export type { SidebarData, NavGroup, NavItem, NavCollapsible, NavLink, MenuItem }
|
||||
@@ -1,91 +0,0 @@
|
||||
import { Dropdown, DropdownMenu as HeroDropdownMenu, DropdownItem, DropdownTrigger, Button, Popover, PopoverTrigger, PopoverContent, cn } from "@heroui/react";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import SubmenuDropdown from "./submenu-dropdown";
|
||||
import { Check } from "lucide-react";
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
export interface MenuDropdownProps {
|
||||
id: string;
|
||||
title?: string;
|
||||
children?: MenuDropdownProps[];
|
||||
theme?: "default" | "danger",
|
||||
isDisabled?: boolean,
|
||||
divide?: boolean,
|
||||
shortcut?: string,
|
||||
isOpen?: boolean,
|
||||
selected?: boolean;
|
||||
clickHandler?: () => void
|
||||
}
|
||||
const DropdownMenu: React.FC<MenuDropdownProps> = ({ title, children, clickHandler, selected = false }) => {
|
||||
|
||||
const disabledChilds: string[] = useMemo(() => {
|
||||
return children ? children?.filter((child: MenuDropdownProps) => child.isDisabled).map((child: MenuDropdownProps) => child.id as string) : []
|
||||
}, [children]);
|
||||
|
||||
return <Dropdown radius="sm" shadow="sm" showArrow>
|
||||
{
|
||||
title &&
|
||||
<DropdownTrigger onPressEnd={clickHandler}>
|
||||
<Button size="sm" variant="light" className="min-w-[42px]" aria-label={title} >
|
||||
<span className=" text-left flex justify-between text-small font-semibold text-font/90">
|
||||
{title}
|
||||
</span>
|
||||
</Button>
|
||||
</DropdownTrigger>
|
||||
}
|
||||
<HeroDropdownMenu
|
||||
disabledKeys={disabledChilds}
|
||||
>
|
||||
{
|
||||
children ? children?.map((child: MenuDropdownProps) => (
|
||||
<DropdownItem
|
||||
key={child.id as string}
|
||||
color={child.theme}
|
||||
shortcut={child.shortcut}
|
||||
classNames={{
|
||||
shortcut: "dark:border-font/10",
|
||||
base: child.children ? "p-0" : "p-2"
|
||||
}}
|
||||
textValue={child.title}
|
||||
endContent={
|
||||
child.selected ? <Check className="text-icon size-4" /> : undefined
|
||||
}
|
||||
className={child.theme == "danger" ? "text-danger" : "text-font/90"}
|
||||
onPressEnd={child.clickHandler}
|
||||
>
|
||||
{
|
||||
!child.children ?
|
||||
<div >
|
||||
{child.title}
|
||||
{
|
||||
child.divide &&
|
||||
<div className="w-full h-[0.5px] absolute bottom-[-0.5px] left-0 bg-divider "></div>
|
||||
}
|
||||
</div>
|
||||
:
|
||||
<div >
|
||||
|
||||
<SubmenuDropdown {...child} />
|
||||
{
|
||||
child.divide &&
|
||||
<div className="w-full h-[0.5px] absolute bottom-[-0.5px] left-0 bg-divider"></div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</DropdownItem>
|
||||
)) : []
|
||||
}
|
||||
</HeroDropdownMenu>
|
||||
</Dropdown>
|
||||
}
|
||||
|
||||
|
||||
export default DropdownMenu;
|
||||
@@ -1,40 +0,0 @@
|
||||
import { Button, Popover, PopoverContent, PopoverTrigger, useDisclosure } from "@heroui/react";
|
||||
import { MenuDropdownProps } from "./menu-dropdown";
|
||||
import { Check, ChevronRight } from "lucide-react";
|
||||
|
||||
import DropdownMenu from "./menu-dropdown";
|
||||
|
||||
|
||||
|
||||
const SubmenuDropdown: React.FC<MenuDropdownProps> = ({ id , title, children }) => {
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
|
||||
|
||||
return (
|
||||
|
||||
<div onMouseEnter={onOpen} onMouseLeave={onClose} className="p-2">
|
||||
<Popover placement="right" isOpen={isOpen} radius="sm" shadow="sm" >
|
||||
<PopoverTrigger >
|
||||
<Button className="w-full h-6 bg-transparent p-0 text-font/90 " size="sm" value={"light"}>
|
||||
<span className="w-full text-left flex justify-between text-small font-normal">
|
||||
{title}
|
||||
<ChevronRight className="size-4 text-icon" />
|
||||
</span>
|
||||
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0 border-0 shadow-none block w-[200px] ">
|
||||
<div className="h-8">
|
||||
<DropdownMenu
|
||||
id = { id }
|
||||
children={children}
|
||||
/>
|
||||
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
)
|
||||
};
|
||||
|
||||
export default SubmenuDropdown;
|
||||
@@ -0,0 +1,206 @@
|
||||
|
||||
import React, { ReactNode, useState } from "react";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "./ui/dialog";
|
||||
import { Button } from "./ui/button";
|
||||
import { Spinner } from "./ui/shadcn-io/spinner";
|
||||
|
||||
|
||||
export interface ModalProps {
|
||||
isOpen?: boolean,
|
||||
onOpenChange?: (open: boolean) => void,
|
||||
className?: string,
|
||||
backdrop?: "blur" | "transparent" | "opaque",
|
||||
title: string,
|
||||
children: ReactNode,
|
||||
actionName?: string,
|
||||
actionHandler?: () => void,
|
||||
isDisabled?: boolean,
|
||||
description?: string,
|
||||
variant?: "default" | "danger",
|
||||
closable?: boolean,
|
||||
footer?: boolean
|
||||
}
|
||||
|
||||
const Modal: React.FC<ModalProps> = ({
|
||||
isOpen,
|
||||
onOpenChange,
|
||||
className,
|
||||
title,
|
||||
description,
|
||||
children,
|
||||
actionName = "Action",
|
||||
actionHandler,
|
||||
isDisabled,
|
||||
closable = true,
|
||||
footer = true,
|
||||
variant = "default"
|
||||
}) => {
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const { t } = useTranslation();
|
||||
const handleAction = async () => {
|
||||
setIsLoading(true)
|
||||
try {
|
||||
actionHandler && await actionHandler();
|
||||
} catch (error) {
|
||||
setIsLoading(false);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={ closable ? onOpenChange : () => {}}>
|
||||
<DialogContent className={className} showCloseButton={closable}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
{
|
||||
description &&
|
||||
<DialogDescription>
|
||||
{description}
|
||||
</DialogDescription>
|
||||
}
|
||||
</DialogHeader>
|
||||
{children}
|
||||
{
|
||||
footer &&
|
||||
<DialogFooter >
|
||||
<div className="flex justify-between w-full flex-col gap-4 sm:flex-row">
|
||||
|
||||
|
||||
{
|
||||
variant == "default" &&
|
||||
<>
|
||||
{
|
||||
closable &&
|
||||
<DialogClose asChild >
|
||||
<Button variant="outline">{t("modals.close")}</Button>
|
||||
</DialogClose>
|
||||
}
|
||||
{
|
||||
!closable && <div></div>
|
||||
}
|
||||
{
|
||||
actionHandler &&
|
||||
<Button type="submit" onClick={handleAction} disabled={isDisabled || isLoading} >
|
||||
{actionName}
|
||||
{isLoading && <Spinner />}
|
||||
|
||||
</Button>
|
||||
}
|
||||
</>
|
||||
}
|
||||
{
|
||||
variant == "danger" &&
|
||||
<>
|
||||
{
|
||||
closable &&
|
||||
<DialogClose asChild >
|
||||
<Button variant="outline">{t("modals.close")}</Button>
|
||||
</DialogClose>
|
||||
}
|
||||
{
|
||||
|
||||
actionHandler &&
|
||||
<Button type="submit" variant="destructive" onClick={handleAction} disabled={isDisabled || isLoading} >
|
||||
{actionName}
|
||||
</Button>
|
||||
}
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
</DialogFooter>
|
||||
}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
/*
|
||||
<HeroUiModal
|
||||
ref={targetRef}
|
||||
isOpen={isOpen}
|
||||
onOpenChange={onOpenChange && closable ? onOpenChange : undefined}
|
||||
className={className}
|
||||
backdrop={backdrop}
|
||||
radius="sm"
|
||||
|
||||
classNames={{
|
||||
base: "dark:bg-background-100",
|
||||
closeButton: cn("dark:bg-background-100 dark:hover:bg-background rounded-md", !closable ? "hidden" : "")
|
||||
}}
|
||||
|
||||
>
|
||||
<ModalContent>
|
||||
{(onClose) => (
|
||||
<>
|
||||
<ModalHeader {...moveProps} className=" flex flex-col gap-1" >
|
||||
|
||||
{title}
|
||||
{
|
||||
header &&
|
||||
<p className="text-sm text-font/70 block">
|
||||
{header}
|
||||
</p>
|
||||
}
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
{children}
|
||||
</ModalBody>
|
||||
{footer &&
|
||||
<ModalFooter>
|
||||
<div className="flex w-full justify-between">
|
||||
{
|
||||
variant == "default" &&
|
||||
<>
|
||||
{
|
||||
closable &&
|
||||
<Button color="danger" variant="light" onPress={onClose} size="sm">
|
||||
{t("modals.close")}
|
||||
</Button>
|
||||
}
|
||||
{
|
||||
!closable && <div></div>
|
||||
}
|
||||
{
|
||||
actionHandler &&
|
||||
<Button color="primary" onPress={() => handleAction(onClose)} size="sm" isDisabled={isDisabled || isLoading} isLoading={isLoading}>
|
||||
{actionName}
|
||||
</Button>
|
||||
}
|
||||
</>
|
||||
}
|
||||
{
|
||||
variant == "danger" &&
|
||||
<>
|
||||
{
|
||||
closable &&
|
||||
<Button color="default" variant="light" onPress={onClose} size="sm">
|
||||
{t("modals.close")}
|
||||
</Button>
|
||||
}
|
||||
{
|
||||
!closable && <div></div>
|
||||
}
|
||||
{
|
||||
actionHandler &&
|
||||
|
||||
<Button color="danger" onPress={() => handleAction(onClose)} size="sm" isDisabled={isDisabled || isLoading} isLoading={isLoading}>
|
||||
{actionName}
|
||||
</Button>
|
||||
}
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
</ModalFooter>
|
||||
}
|
||||
</>
|
||||
)}
|
||||
</ModalContent>
|
||||
</HeroUiModal>
|
||||
*/
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
|
||||
export default Modal;
|
||||
@@ -1,22 +0,0 @@
|
||||
import { Spinner } from "@heroui/react";
|
||||
|
||||
|
||||
|
||||
|
||||
interface LoadingProps {
|
||||
text?: string;
|
||||
}
|
||||
|
||||
|
||||
export const Loading: React.FC<LoadingProps> = ({ text }) => {
|
||||
|
||||
return (
|
||||
<div className="fixed bg-overlay/50 backdrop-opacity-disabled w-screen h-screen fixed flex items-center justify-center z-[99] left-0 top-0 text-white flex-col gap-2">
|
||||
<Spinner size="lg" />
|
||||
<span className="font-medium">
|
||||
Loading ...
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,140 +0,0 @@
|
||||
|
||||
import React, { ReactNode, useState } from "react";
|
||||
import {
|
||||
Modal as HeroUiModal,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
Button,
|
||||
useDraggable,
|
||||
cn,
|
||||
} from "@heroui/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
|
||||
export interface ModalProps {
|
||||
isOpen?: boolean,
|
||||
onOpenChange?: (open: boolean) => void,
|
||||
className?: string,
|
||||
backdrop?: "blur" | "transparent" | "opaque",
|
||||
title: string,
|
||||
children: ReactNode,
|
||||
actionName?: string,
|
||||
actionHandler?: () => void,
|
||||
isDisabled?: boolean,
|
||||
header?: string,
|
||||
variant?: "default" | "danger",
|
||||
closable?: boolean
|
||||
}
|
||||
|
||||
const Modal: React.FC<ModalProps> = ({ isOpen, onOpenChange, className, backdrop = "opaque", title, children, actionName = "Action", header, actionHandler, isDisabled, variant = "default", closable = true }) => {
|
||||
|
||||
|
||||
const targetRef = React.useRef(null);
|
||||
const { moveProps } = useDraggable({ targetRef, canOverflow: true, isDisabled: !isOpen });
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleAction = async (onClose: () => void) => {
|
||||
setIsLoading(true)
|
||||
try {
|
||||
actionHandler && await actionHandler();
|
||||
} catch (error) {
|
||||
setIsLoading(false);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<HeroUiModal
|
||||
ref={targetRef}
|
||||
isOpen={isOpen}
|
||||
onOpenChange={onOpenChange && closable ? onOpenChange : undefined}
|
||||
className={className}
|
||||
backdrop={backdrop}
|
||||
radius="sm"
|
||||
|
||||
classNames={{
|
||||
base: "dark:bg-background-100",
|
||||
closeButton: cn("dark:bg-background-100 dark:hover:bg-background rounded-md", !closable ? "hidden" : "")
|
||||
}}
|
||||
|
||||
>
|
||||
<ModalContent>
|
||||
{(onClose) => (
|
||||
<>
|
||||
<ModalHeader {...moveProps} className=" flex flex-col gap-1" >
|
||||
|
||||
{title}
|
||||
{
|
||||
header &&
|
||||
<p className="text-sm text-font/70 block">
|
||||
{header}
|
||||
</p>
|
||||
}
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
{children}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<div className="flex w-full justify-between">
|
||||
{
|
||||
variant == "default" &&
|
||||
<>
|
||||
{
|
||||
closable &&
|
||||
<Button color="danger" variant="light" onPress={onClose} size="sm">
|
||||
{t("modals.close")}
|
||||
</Button>
|
||||
}
|
||||
{
|
||||
!closable && <div></div>
|
||||
}
|
||||
{
|
||||
actionHandler &&
|
||||
<Button color="primary" onPress={() => handleAction(onClose)} size="sm" isDisabled={isDisabled || isLoading} isLoading={isLoading}>
|
||||
{actionName}
|
||||
</Button>
|
||||
}
|
||||
</>
|
||||
}
|
||||
{
|
||||
variant == "danger" &&
|
||||
<>
|
||||
{
|
||||
closable &&
|
||||
<Button color="default" variant="light" onPress={onClose} size="sm">
|
||||
{t("modals.close")}
|
||||
</Button>
|
||||
}
|
||||
{
|
||||
!closable && <div></div>
|
||||
}
|
||||
{
|
||||
actionHandler &&
|
||||
|
||||
<Button color="danger" onPress={() => handleAction(onClose)} size="sm" isDisabled={isDisabled || isLoading} isLoading={isLoading}>
|
||||
{actionName}
|
||||
</Button>
|
||||
}
|
||||
</>
|
||||
|
||||
}
|
||||
</div>
|
||||
</ModalFooter>
|
||||
</>
|
||||
)}
|
||||
</ModalContent>
|
||||
</HeroUiModal>
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
|
||||
export default Modal;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,108 +0,0 @@
|
||||
import { languages } from "@/i18";
|
||||
import { Language } from "@/i18/types";
|
||||
import { Button, Input, Listbox, ListboxItem, Popover, PopoverContent, PopoverTrigger } from "@heroui/react";
|
||||
import { Check, Languages, Search } from "lucide-react";
|
||||
import React, { Ref, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const LanguagesDropDonw: React.FC = ({ }) => {
|
||||
const { t, i18n } = useTranslation();
|
||||
const [currentLanguage, setCurrentLanguage] = useState<Language>(() => languages.find((language: Language) => language.code == i18n.language) as Language);
|
||||
const [languageList, setLanguageList] = useState<Language[]>(languages);
|
||||
const searchRef: Ref<HTMLInputElement> = useRef<HTMLInputElement>(null);
|
||||
const changeLanguage = useCallback((language: Language) => {
|
||||
i18n.changeLanguage(language.code);
|
||||
}, [i18n])
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentLanguage(languages.find((language: Language) => language.code == i18n.language) as Language)
|
||||
}, [i18n.language])
|
||||
|
||||
const searchLanguage = () => {
|
||||
|
||||
if (!searchRef.current?.value || searchRef.current?.value.trim().length == 0) {
|
||||
|
||||
setLanguageList(languages);
|
||||
}else
|
||||
setLanguageList(languages.filter((language: Language) =>
|
||||
language.code.toLowerCase().includes(searchRef.current?.value.toLowerCase() as string) ||
|
||||
language.name.toLowerCase().includes(searchRef.current?.value.toLowerCase() as string) ||
|
||||
language.nativeName.toLowerCase().includes(searchRef.current?.value.toLowerCase() as string)
|
||||
))
|
||||
console.log(searchRef.current?.value);
|
||||
}
|
||||
|
||||
const openChange = useCallback((isOpen : boolean) => {
|
||||
|
||||
if (isOpen)
|
||||
setLanguageList(languages)
|
||||
} , [])
|
||||
|
||||
return (
|
||||
<Popover showArrow placement="bottom" radius="sm" shadow="sm" onOpenChange={openChange}>
|
||||
<PopoverTrigger>
|
||||
<Button
|
||||
variant="bordered"
|
||||
radius="sm"
|
||||
|
||||
startContent={
|
||||
<Languages className="size-4 text-font/90 dark:text-white" />
|
||||
}
|
||||
className="p-0 h-9 min-w-[142px] border-1 border-divider dark:border-font/10 text-xs bg-transparent hover:bg-default text-font/90 font-semibold "
|
||||
>
|
||||
<span>
|
||||
{currentLanguage.nativeName} <span className="text-font/70">({currentLanguage.name})</span>
|
||||
</span>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-2">
|
||||
<Input
|
||||
variant="flat"
|
||||
size="sm"
|
||||
aria-label={t("navbar.search")}
|
||||
placeholder={`${t("navbar.search")}`}
|
||||
ref={searchRef}
|
||||
onKeyUp={searchLanguage}
|
||||
startContent={
|
||||
<Search className="size-4 text-icon" />
|
||||
}
|
||||
className="h-8 w-full focus-visible:ring-0 shadow-none "
|
||||
classNames={{
|
||||
inputWrapper: "dark:bg-default border-divider group-hover:border-primary group-data-[focus=true]:border-primary group-data-[focus=true]:bg-default",
|
||||
|
||||
}}
|
||||
/>
|
||||
<hr className="my-2 w-full border-divider" />
|
||||
|
||||
<Listbox aria-label="Languages"
|
||||
|
||||
>
|
||||
{
|
||||
languageList.map((language: Language) => (
|
||||
<ListboxItem
|
||||
|
||||
key={language.code}
|
||||
textValue={language.code}
|
||||
endContent={
|
||||
currentLanguage.code == language.code && <Check className="text-icon size-4" />
|
||||
}
|
||||
onPressEnd={() => changeLanguage(language)}
|
||||
className="text-font/90"
|
||||
>
|
||||
|
||||
{language.nativeName} <span className="text-font/70"> ({language.name})</span>
|
||||
</ListboxItem>
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
</Listbox>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export default React.memo(LanguagesDropDonw);
|
||||
@@ -1,44 +0,0 @@
|
||||
|
||||
import { useDatabase } from "@/providers/database-provider/database-provider";
|
||||
import Menu from "../menu/menu";
|
||||
import RenameDatabase from "./rename-database";
|
||||
import LanguagesDropdown from "./languages-dropdown";
|
||||
|
||||
|
||||
|
||||
const Navbar: React.FC = ({ }) => {
|
||||
const { database } = useDatabase();
|
||||
return (
|
||||
<nav className="h-12 fixed z-50 bg-background w-full flex items-center p-4 border-b border-divider ">
|
||||
<img
|
||||
src={`/stackrender.png`}
|
||||
width={22}
|
||||
alt="logo"
|
||||
/>
|
||||
<h3 className="font-semibold ml-2 text-slate-900 text-sm dark:text-white">StackRender</h3>
|
||||
<div className="ml-4 w-full">
|
||||
<div className="flex w-full justify-between items-center gap-2">
|
||||
|
||||
<Menu />
|
||||
<div className=" absolute left-[50%] -translate-x-[50%]">
|
||||
{database && <RenameDatabase database={database} />}
|
||||
</div>
|
||||
<div className="flex gap-2 items-center">
|
||||
<a className="gh-button text-font font-medium bg-background text-xs p-4 px-2 pr-2.5 flex justif-center items-center"
|
||||
target="_blank"
|
||||
href="https://github.com/stackrender/stackrender">
|
||||
<span className="gh-button__icon"></span>
|
||||
|
||||
</a>
|
||||
|
||||
<LanguagesDropdown />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export default Navbar;
|
||||
@@ -0,0 +1,33 @@
|
||||
import { IconSearch } from '@tabler/icons-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useSearch } from '@/providers/search-provider/search-provider'
|
||||
import { Button } from './ui/button'
|
||||
|
||||
interface Props {
|
||||
className?: string
|
||||
type?: React.HTMLInputTypeAttribute
|
||||
placeholder?: string
|
||||
}
|
||||
|
||||
export function Search({ className = '', placeholder = 'Search' }: Props) {
|
||||
const { setOpen } = useSearch()
|
||||
return (
|
||||
<Button
|
||||
variant='outline'
|
||||
className={cn(
|
||||
'bg-muted/25 text-muted-foreground hover:bg-muted/50 relative h-8 w-full flex-1 justify-start rounded-md text-sm font-normal shadow-none sm:pr-12 md:w-40 md:flex-none lg:w-56 xl:w-64',
|
||||
className
|
||||
)}
|
||||
onClick={() => setOpen(true)}
|
||||
>
|
||||
<IconSearch
|
||||
aria-hidden='true'
|
||||
className='absolute top-1/2 left-1.5 -translate-y-1/2'
|
||||
/>
|
||||
<span className='ml-3'>{placeholder}</span>
|
||||
<kbd className='bg-muted pointer-events-none absolute top-[0.3rem] right-[0.3rem] hidden h-5 items-center gap-1 rounded border px-1.5 font-mono text-[10px] font-medium opacity-100 select-none sm:flex'>
|
||||
<span className='text-xs'>⌘</span>K
|
||||
</kbd>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
import { Link } from "react-router-dom";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "../tooltip/tooltip";
|
||||
import { Divider } from "@heroui/react";
|
||||
import React from "react";
|
||||
|
||||
|
||||
|
||||
export interface SidebarItemProps {
|
||||
type?: "item" | "divider"
|
||||
title?: string;
|
||||
icon?: React.ReactNode;
|
||||
href?: string;
|
||||
isActive?: boolean,
|
||||
newTab?: boolean
|
||||
}
|
||||
|
||||
|
||||
const SidebarItem: React.FC<SidebarItemProps> = ({ title, icon, href, type = "item", isActive, newTab }) => {
|
||||
|
||||
|
||||
if (type == "item")
|
||||
|
||||
|
||||
return (
|
||||
<Tooltip delayDuration={0} >
|
||||
{
|
||||
!newTab &&
|
||||
<TooltipTrigger asChild>
|
||||
|
||||
<Link
|
||||
to={href as string}
|
||||
target=""
|
||||
className="p-2 rounded-md hover:bg-default-100 dark:hover:bg-background-50 sidebar-item " data-active={isActive}>
|
||||
{icon}
|
||||
</Link>
|
||||
</TooltipTrigger>
|
||||
}
|
||||
{
|
||||
newTab &&
|
||||
<TooltipTrigger asChild>
|
||||
<a
|
||||
href={href as string}
|
||||
target="_blank"
|
||||
className="p-2 rounded-md hover:bg-default-100 dark:hover:bg-background-50 sidebar-item " data-active={isActive}>
|
||||
{icon}
|
||||
</a>
|
||||
</TooltipTrigger>
|
||||
}
|
||||
|
||||
<TooltipContent
|
||||
side="left"
|
||||
align="center"
|
||||
>
|
||||
{title}
|
||||
</TooltipContent>
|
||||
</Tooltip >
|
||||
|
||||
)
|
||||
else {
|
||||
return <Divider className="bg-default-200 dark:bg-divider" />
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default SidebarItem
|
||||
@@ -1,84 +0,0 @@
|
||||
import { Github, TableProperties, Workflow } from "lucide-react";
|
||||
import SidebarItem, { SidebarItemProps } from "./sidebar-item";
|
||||
import { useMemo } from "react";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Discord } from "../icon/discord";
|
||||
import { X } from "../icon/x";
|
||||
|
||||
interface SidebarProps {
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
const sidebarItemClass: string = "text-font size-4 data-[active=true]:text-primary-900";
|
||||
|
||||
const Sidebar: React.FC<SidebarProps> = ({ }) => {
|
||||
|
||||
const { t } = useTranslation();
|
||||
const location = useLocation();
|
||||
|
||||
const sidebarItems: SidebarItemProps[] = useMemo(() => [
|
||||
{
|
||||
title: t("sidebar.tables"),
|
||||
icon: <TableProperties className={sidebarItemClass}></TableProperties>,
|
||||
href: "/database/tables",
|
||||
isActive: location.pathname.endsWith("/database/tables")
|
||||
},
|
||||
{
|
||||
title: t("sidebar.relationships"),
|
||||
icon: <Workflow className={sidebarItemClass}></Workflow>,
|
||||
href: "/database/relationships",
|
||||
isActive: location.pathname.endsWith("/database/relationships")
|
||||
},
|
||||
], [location , t])
|
||||
|
||||
const bottomSidebarItems: SidebarItemProps[] = useMemo(() => [
|
||||
{
|
||||
title: "X",
|
||||
icon: <X className={sidebarItemClass}></X>,
|
||||
href: "https://x.com/Iam_The_Dev",
|
||||
newTab : true ,
|
||||
},
|
||||
{
|
||||
title: "Discord",
|
||||
icon: <Discord className={sidebarItemClass}></Discord>,
|
||||
href: "https://discord.gg/DsN8RcPR6Y",
|
||||
newTab : true ,
|
||||
},
|
||||
{
|
||||
title: "Github",
|
||||
icon: <Github className={sidebarItemClass}></Github>,
|
||||
href: "https://github.com/stackrender/stackrender",
|
||||
newTab : true ,
|
||||
},
|
||||
], []);
|
||||
|
||||
return (
|
||||
<aside className="h-full z-[20] flex flex-col items-between py-2 justify-between sticky top-0 duration-500 w-12 bg-sidebar pt-[56px] dark:bg-background">
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
{
|
||||
sidebarItems.map((item: SidebarItemProps, index: number) => (
|
||||
<SidebarItem
|
||||
key={`top-${index}`}
|
||||
{...item}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
<div className="flex flex-col items-center gap-2 ">
|
||||
{
|
||||
bottomSidebarItems.map((item: SidebarItemProps, index: number) => (
|
||||
<SidebarItem
|
||||
key={`bottom-${index}`}
|
||||
{...item}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</aside>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
export default Sidebar;
|
||||
@@ -1,68 +0,0 @@
|
||||
|
||||
|
||||
import React, { useEffect, useState } from "react"
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { WithContext as ReactTagInput, Tag } from 'react-tag-input';
|
||||
|
||||
|
||||
interface TagInputProps {
|
||||
defaultItems?: string[];
|
||||
onItemsChange? : (items: string[]) => void
|
||||
}
|
||||
|
||||
|
||||
const KeyCodes = {
|
||||
comma: 188,
|
||||
enter: 13,
|
||||
};
|
||||
|
||||
const delimiters = [KeyCodes.comma, KeyCodes.enter];
|
||||
|
||||
|
||||
const TagInput: React.FC<TagInputProps> = ({ defaultItems = [], onItemsChange }) => {
|
||||
const { t } = useTranslation();
|
||||
const [tags, setTags] = useState<Tag[]>(defaultItems.map((item: string) => ({
|
||||
id: item.toLowerCase(),
|
||||
text: item,
|
||||
className: ""
|
||||
}) as Tag));
|
||||
|
||||
const handleDelete = (i: number) => {
|
||||
setTags(tags.filter((_, index) => index !== i));
|
||||
};
|
||||
|
||||
const handleAddition = (tag: Tag) => {
|
||||
setTags([...tags, tag]);
|
||||
};
|
||||
|
||||
const handleDrag = (tag: Tag, currPos: number, newPos: number) => {
|
||||
const newTags = tags.slice();
|
||||
newTags.splice(currPos, 1);
|
||||
newTags.splice(newPos, 0, tag);
|
||||
setTags(newTags);
|
||||
};
|
||||
useEffect(() => onItemsChange && onItemsChange(tags.map((tag: Tag) => tag.text)), [tags])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ReactTagInput
|
||||
tags={tags}
|
||||
classNames={{
|
||||
tag: "font-normal inline-block m-0.5 border-1 border-default dark:border-divider rounded-full px-2 py-1 flex-row ",
|
||||
remove: " bg-default-900 dark:bg-default-500 rounded-full text-xs text-center ml-1 min-w-[15px] max-w-[15px] min-h-[15px] max-h-[15px] transition-colors duration-300 hover:bg-black",
|
||||
tagInputField: "relative w-full inline-flex flex-row items-center bg-default-100 border-1 border-divider hover:border-primary focus-within:border-primary h-8 min-h-8 px-2 rounded-small transition-background !duration-150 transition-colors outline-none dark:bg-default placeholder:text-foreground-500 mt-2" ,
|
||||
tags : "max-h-[256px] overflow-auto "
|
||||
}}
|
||||
handleDelete={handleDelete}
|
||||
autoFocus={false}
|
||||
handleAddition={handleAddition}
|
||||
handleDrag={handleDrag}
|
||||
delimiters={delimiters}
|
||||
placeholder={t("db_controller.field_settings.type_enter")}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export default React.memo(TagInput)
|
||||
@@ -0,0 +1,60 @@
|
||||
import { useEffect } from 'react'
|
||||
import { IconCheck, IconMoon, IconSun } from '@tabler/icons-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useTheme } from '@/providers/theme-provider/theme-provider'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export function ThemeSwitch() {
|
||||
const { theme, setTheme } = useTheme() ;
|
||||
const { t } = useTranslation() ;
|
||||
|
||||
/* Update theme-color meta tag
|
||||
* when theme is updated */
|
||||
useEffect(() => {
|
||||
const themeColor = theme === 'dark' ? '#020817' : '#fff'
|
||||
const metaThemeColor = document.querySelector("meta[name='theme-color']")
|
||||
if (metaThemeColor) metaThemeColor.setAttribute('content', themeColor)
|
||||
}, [theme])
|
||||
|
||||
return (
|
||||
<DropdownMenu modal={false}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant='ghost' size='icon' className='scale-95 rounded-full'>
|
||||
<IconSun className='size-[1.2rem] scale-100 rotate-0 transition-all dark:scale-0 dark:-rotate-90' />
|
||||
<IconMoon className='absolute size-[1.2rem] scale-0 rotate-90 transition-all dark:scale-100 dark:rotate-0' />
|
||||
<span className='sr-only'>Toggle theme</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align='end'>
|
||||
<DropdownMenuItem onClick={() => setTheme('light')}>
|
||||
{t("menu.light")}{' '}
|
||||
<IconCheck
|
||||
size={14}
|
||||
className={cn('ml-auto', theme !== 'light' && 'hidden')}
|
||||
/>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setTheme('dark')}>
|
||||
{t("menu.dark")}
|
||||
<IconCheck
|
||||
size={14}
|
||||
className={cn('ml-auto', theme !== 'dark' && 'hidden')}
|
||||
/>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setTheme('system')}>
|
||||
{t("menu.system")}
|
||||
<IconCheck
|
||||
size={14}
|
||||
className={cn('ml-auto', theme !== 'system' && 'hidden')}
|
||||
/>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
import { cn } from "@heroui/react"
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "../tooltip/tooltip"
|
||||
|
||||
interface ToggleProps {
|
||||
active?: boolean,
|
||||
children: React.ReactNode,
|
||||
onToggle?: (value: boolean) => void,
|
||||
className?: string ,
|
||||
label? : string
|
||||
}
|
||||
|
||||
|
||||
const ToggleButton: React.FC<ToggleProps> = ({ active = false, children, onToggle, className , label }) => {
|
||||
|
||||
const toggle = () => {
|
||||
onToggle && onToggle(!active)
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button className={
|
||||
cn("p-1 px-2 transition-all hover:bg-default hover:text-font/90 rounded duration-200 ",
|
||||
className ? className : "",
|
||||
active ?
|
||||
"text-font/90 bg-default dark:text-font/90 white dark:bg-default/40" :
|
||||
"text-icon"
|
||||
)
|
||||
}
|
||||
|
||||
onClick={toggle}
|
||||
>
|
||||
<span className="text-sm">
|
||||
{children}
|
||||
</span>
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{label}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export default ToggleButton
|
||||
@@ -1,30 +0,0 @@
|
||||
import React from 'react';
|
||||
import * as TooltipPrimitive from '@radix-ui/react-tooltip';
|
||||
import { cn } from '@heroui/react';
|
||||
|
||||
|
||||
|
||||
const TooltipProvider = TooltipPrimitive.Provider;
|
||||
|
||||
const Tooltip = TooltipPrimitive.Root;
|
||||
|
||||
const TooltipTrigger = TooltipPrimitive.Trigger;
|
||||
|
||||
const TooltipContent = React.forwardRef<
|
||||
React.ElementRef<typeof TooltipPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<TooltipPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
|
||||
className={cn(
|
||||
'z-50 shadow-lg overflow-hidden rounded-md bg-foreground px-3 py-2 text-xs text-default animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-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',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
|
||||
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
|
||||
@@ -0,0 +1,90 @@
|
||||
import * as React from "react"
|
||||
import * as AccordionPrimitive from "@radix-ui/react-accordion"
|
||||
import { ChevronDownIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Accordion({
|
||||
...props
|
||||
}: React.ComponentProps<typeof AccordionPrimitive.Root>) {
|
||||
return <AccordionPrimitive.Root data-slot="accordion" {...props} />
|
||||
}
|
||||
|
||||
function AccordionItem({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AccordionPrimitive.Item>) {
|
||||
return (
|
||||
<AccordionPrimitive.Item
|
||||
data-slot="accordion-item"
|
||||
className={cn("border-b last:border-b-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
type AccordionTriggerProps = React.ComponentProps<typeof AccordionPrimitive.Trigger> & {
|
||||
position?: "left" | "right" ;
|
||||
leftContent? : React.ReactNode
|
||||
}
|
||||
|
||||
|
||||
function AccordionTrigger({
|
||||
className,
|
||||
children,
|
||||
position = "left",
|
||||
leftContent ,
|
||||
...props
|
||||
}: AccordionTriggerProps) {
|
||||
return (
|
||||
<AccordionPrimitive.Header className="flex">
|
||||
<AccordionPrimitive.Trigger
|
||||
data-slot="accordion-trigger"
|
||||
className={cn(
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-center gap-2 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{
|
||||
leftContent
|
||||
}
|
||||
{
|
||||
position === "left" &&
|
||||
<>
|
||||
<ChevronDownIcon className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" />
|
||||
|
||||
{children}
|
||||
</>
|
||||
}
|
||||
{
|
||||
position == "right" &&
|
||||
<>
|
||||
{children}
|
||||
<ChevronDownIcon className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" />
|
||||
|
||||
</>
|
||||
}
|
||||
</AccordionPrimitive.Trigger>
|
||||
</AccordionPrimitive.Header>
|
||||
)
|
||||
}
|
||||
|
||||
function AccordionContent({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AccordionPrimitive.Content>) {
|
||||
return (
|
||||
<AccordionPrimitive.Content
|
||||
data-slot="accordion-content"
|
||||
className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
|
||||
{...props}
|
||||
>
|
||||
<div className={cn("pt-0 pb-4", className)}>{children}</div>
|
||||
</AccordionPrimitive.Content>
|
||||
)
|
||||
}
|
||||
|
||||
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
|
||||
@@ -0,0 +1,66 @@
|
||||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const alertVariants = cva(
|
||||
"relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-card text-card-foreground",
|
||||
destructive:
|
||||
"text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Alert({
|
||||
className,
|
||||
variant,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert"
|
||||
role="alert"
|
||||
className={cn(alertVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-title"
|
||||
className={cn(
|
||||
"col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-description"
|
||||
className={cn(
|
||||
"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Alert, AlertTitle, AlertDescription }
|
||||
@@ -0,0 +1,51 @@
|
||||
import * as React from "react"
|
||||
import * as AvatarPrimitive from "@radix-ui/react-avatar"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Avatar({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AvatarPrimitive.Root>) {
|
||||
return (
|
||||
<AvatarPrimitive.Root
|
||||
data-slot="avatar"
|
||||
className={cn(
|
||||
"relative flex size-8 shrink-0 overflow-hidden rounded-full",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AvatarImage({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
|
||||
return (
|
||||
<AvatarPrimitive.Image
|
||||
data-slot="avatar-image"
|
||||
className={cn("aspect-square size-full", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AvatarFallback({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
|
||||
return (
|
||||
<AvatarPrimitive.Fallback
|
||||
data-slot="avatar-fallback"
|
||||
className={cn(
|
||||
"bg-muted flex size-full items-center justify-center rounded-full",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Avatar, AvatarImage, AvatarFallback }
|
||||
@@ -0,0 +1,46 @@
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
|
||||
secondary:
|
||||
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
|
||||
destructive:
|
||||
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||
outline:
|
||||
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Badge({
|
||||
className,
|
||||
variant,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"span"> &
|
||||
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
||||
const Comp = asChild ? Slot : "span"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="badge"
|
||||
className={cn(badgeVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants }
|
||||
@@ -0,0 +1,59 @@
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||
outline:
|
||||
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
|
||||
ghost:
|
||||
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
||||
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||
icon: "size-9",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
const Button = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
React.ComponentProps<"button"> & VariantProps<typeof buttonVariants> & {
|
||||
asChild?: boolean
|
||||
}
|
||||
>(({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
ref={ref} // ✅ now Button can accept refs
|
||||
data-slot="button"
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
Button.displayName = "Button"
|
||||
|
||||
export { Button, buttonVariants }
|
||||
@@ -0,0 +1,211 @@
|
||||
import * as React from "react"
|
||||
import {
|
||||
ChevronDownIcon,
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
} from "lucide-react"
|
||||
import { DayButton, DayPicker, getDefaultClassNames } from "react-day-picker"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button, buttonVariants } from "@/components/ui/button"
|
||||
|
||||
function Calendar({
|
||||
className,
|
||||
classNames,
|
||||
showOutsideDays = true,
|
||||
captionLayout = "label",
|
||||
buttonVariant = "ghost",
|
||||
formatters,
|
||||
components,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DayPicker> & {
|
||||
buttonVariant?: React.ComponentProps<typeof Button>["variant"]
|
||||
}) {
|
||||
const defaultClassNames = getDefaultClassNames()
|
||||
|
||||
return (
|
||||
<DayPicker
|
||||
showOutsideDays={showOutsideDays}
|
||||
className={cn(
|
||||
"bg-background group/calendar p-3 [--cell-size:--spacing(8)] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent",
|
||||
String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
|
||||
String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
|
||||
className
|
||||
)}
|
||||
captionLayout={captionLayout}
|
||||
formatters={{
|
||||
formatMonthDropdown: (date) =>
|
||||
date.toLocaleString("default", { month: "short" }),
|
||||
...formatters,
|
||||
}}
|
||||
classNames={{
|
||||
root: cn("w-fit", defaultClassNames.root),
|
||||
months: cn(
|
||||
"flex gap-4 flex-col md:flex-row relative",
|
||||
defaultClassNames.months
|
||||
),
|
||||
month: cn("flex flex-col w-full gap-4", defaultClassNames.month),
|
||||
nav: cn(
|
||||
"flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between",
|
||||
defaultClassNames.nav
|
||||
),
|
||||
button_previous: cn(
|
||||
buttonVariants({ variant: buttonVariant }),
|
||||
"size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
|
||||
defaultClassNames.button_previous
|
||||
),
|
||||
button_next: cn(
|
||||
buttonVariants({ variant: buttonVariant }),
|
||||
"size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
|
||||
defaultClassNames.button_next
|
||||
),
|
||||
month_caption: cn(
|
||||
"flex items-center justify-center h-(--cell-size) w-full px-(--cell-size)",
|
||||
defaultClassNames.month_caption
|
||||
),
|
||||
dropdowns: cn(
|
||||
"w-full flex items-center text-sm font-medium justify-center h-(--cell-size) gap-1.5",
|
||||
defaultClassNames.dropdowns
|
||||
),
|
||||
dropdown_root: cn(
|
||||
"relative has-focus:border-ring border border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] rounded-md",
|
||||
defaultClassNames.dropdown_root
|
||||
),
|
||||
dropdown: cn(
|
||||
"absolute bg-popover inset-0 opacity-0",
|
||||
defaultClassNames.dropdown
|
||||
),
|
||||
caption_label: cn(
|
||||
"select-none font-medium",
|
||||
captionLayout === "label"
|
||||
? "text-sm"
|
||||
: "rounded-md pl-2 pr-1 flex items-center gap-1 text-sm h-8 [&>svg]:text-muted-foreground [&>svg]:size-3.5",
|
||||
defaultClassNames.caption_label
|
||||
),
|
||||
table: "w-full border-collapse",
|
||||
weekdays: cn("flex", defaultClassNames.weekdays),
|
||||
weekday: cn(
|
||||
"text-muted-foreground rounded-md flex-1 font-normal text-[0.8rem] select-none",
|
||||
defaultClassNames.weekday
|
||||
),
|
||||
week: cn("flex w-full mt-2", defaultClassNames.week),
|
||||
week_number_header: cn(
|
||||
"select-none w-(--cell-size)",
|
||||
defaultClassNames.week_number_header
|
||||
),
|
||||
week_number: cn(
|
||||
"text-[0.8rem] select-none text-muted-foreground",
|
||||
defaultClassNames.week_number
|
||||
),
|
||||
day: cn(
|
||||
"relative w-full h-full p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none",
|
||||
defaultClassNames.day
|
||||
),
|
||||
range_start: cn(
|
||||
"rounded-l-md bg-accent",
|
||||
defaultClassNames.range_start
|
||||
),
|
||||
range_middle: cn("rounded-none", defaultClassNames.range_middle),
|
||||
range_end: cn("rounded-r-md bg-accent", defaultClassNames.range_end),
|
||||
today: cn(
|
||||
"bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none",
|
||||
defaultClassNames.today
|
||||
),
|
||||
outside: cn(
|
||||
"text-muted-foreground aria-selected:text-muted-foreground",
|
||||
defaultClassNames.outside
|
||||
),
|
||||
disabled: cn(
|
||||
"text-muted-foreground opacity-50",
|
||||
defaultClassNames.disabled
|
||||
),
|
||||
hidden: cn("invisible", defaultClassNames.hidden),
|
||||
...classNames,
|
||||
}}
|
||||
components={{
|
||||
Root: ({ className, rootRef, ...props }) => {
|
||||
return (
|
||||
<div
|
||||
data-slot="calendar"
|
||||
ref={rootRef}
|
||||
className={cn(className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
},
|
||||
Chevron: ({ className, orientation, ...props }) => {
|
||||
if (orientation === "left") {
|
||||
return (
|
||||
<ChevronLeftIcon className={cn("size-4", className)} {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
if (orientation === "right") {
|
||||
return (
|
||||
<ChevronRightIcon
|
||||
className={cn("size-4", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<ChevronDownIcon className={cn("size-4", className)} {...props} />
|
||||
)
|
||||
},
|
||||
DayButton: CalendarDayButton,
|
||||
WeekNumber: ({ children, ...props }) => {
|
||||
return (
|
||||
<td {...props}>
|
||||
<div className="flex size-(--cell-size) items-center justify-center text-center">
|
||||
{children}
|
||||
</div>
|
||||
</td>
|
||||
)
|
||||
},
|
||||
...components,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CalendarDayButton({
|
||||
className,
|
||||
day,
|
||||
modifiers,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DayButton>) {
|
||||
const defaultClassNames = getDefaultClassNames()
|
||||
|
||||
const ref = React.useRef<HTMLButtonElement>(null)
|
||||
React.useEffect(() => {
|
||||
if (modifiers.focused) ref.current?.focus()
|
||||
}, [modifiers.focused])
|
||||
|
||||
return (
|
||||
<Button
|
||||
ref={ref}
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
data-day={day.date.toLocaleDateString()}
|
||||
data-selected-single={
|
||||
modifiers.selected &&
|
||||
!modifiers.range_start &&
|
||||
!modifiers.range_end &&
|
||||
!modifiers.range_middle
|
||||
}
|
||||
data-range-start={modifiers.range_start}
|
||||
data-range-end={modifiers.range_end}
|
||||
data-range-middle={modifiers.range_middle}
|
||||
className={cn(
|
||||
"data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-ring/50 dark:hover:text-accent-foreground flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-[3px] data-[range-end=true]:rounded-md data-[range-end=true]:rounded-r-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md data-[range-start=true]:rounded-l-md [&>span]:text-xs [&>span]:opacity-70",
|
||||
defaultClassNames.day,
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Calendar, CalendarDayButton }
|
||||
@@ -0,0 +1,92 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card"
|
||||
className={cn(
|
||||
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-header"
|
||||
className={cn(
|
||||
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-title"
|
||||
className={cn("leading-none font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-action"
|
||||
className={cn(
|
||||
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-content"
|
||||
className={cn("px-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-footer"
|
||||
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardFooter,
|
||||
CardTitle,
|
||||
CardAction,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
|
||||
|
||||
function Collapsible({
|
||||
...props
|
||||
}: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
|
||||
return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />
|
||||
}
|
||||
|
||||
function CollapsibleTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {
|
||||
return (
|
||||
<CollapsiblePrimitive.CollapsibleTrigger
|
||||
data-slot="collapsible-trigger"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CollapsibleContent({
|
||||
...props
|
||||
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) {
|
||||
return (
|
||||
<CollapsiblePrimitive.CollapsibleContent
|
||||
data-slot="collapsible-content"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Collapsible, CollapsibleTrigger, CollapsibleContent }
|
||||
@@ -0,0 +1,184 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Command as CommandPrimitive } from "cmdk"
|
||||
import { SearchIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog"
|
||||
|
||||
function Command({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive>) {
|
||||
return (
|
||||
<CommandPrimitive
|
||||
data-slot="command"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CommandDialog({
|
||||
title = "Command Palette",
|
||||
description = "Search for a command to run...",
|
||||
children,
|
||||
className,
|
||||
showCloseButton = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof Dialog> & {
|
||||
title?: string
|
||||
description?: string
|
||||
className?: string
|
||||
showCloseButton?: boolean
|
||||
}) {
|
||||
return (
|
||||
<Dialog {...props}>
|
||||
<DialogHeader className="sr-only">
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
<DialogDescription>{description}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogContent
|
||||
className={cn("overflow-hidden p-0", className)}
|
||||
showCloseButton={showCloseButton}
|
||||
>
|
||||
<Command className="[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[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">
|
||||
{children}
|
||||
</Command>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
function CommandInput({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Input>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="command-input-wrapper"
|
||||
className="flex h-9 items-center gap-2 border-b px-3"
|
||||
>
|
||||
<SearchIcon className="size-4 shrink-0 opacity-50" />
|
||||
<CommandPrimitive.Input
|
||||
data-slot="command-input"
|
||||
className={cn(
|
||||
"placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function CommandList({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.List>) {
|
||||
return (
|
||||
<CommandPrimitive.List
|
||||
data-slot="command-list"
|
||||
className={cn(
|
||||
"max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CommandEmpty({
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Empty>) {
|
||||
return (
|
||||
<CommandPrimitive.Empty
|
||||
data-slot="command-empty"
|
||||
className="py-6 text-center text-sm"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CommandGroup({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Group>) {
|
||||
return (
|
||||
<CommandPrimitive.Group
|
||||
data-slot="command-group"
|
||||
className={cn(
|
||||
"text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CommandSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Separator>) {
|
||||
return (
|
||||
<CommandPrimitive.Separator
|
||||
data-slot="command-separator"
|
||||
className={cn("bg-border -mx-1 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CommandItem({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Item>) {
|
||||
return (
|
||||
<CommandPrimitive.Item
|
||||
data-slot="command-item"
|
||||
className={cn(
|
||||
"data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CommandShortcut({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="command-shortcut"
|
||||
className={cn(
|
||||
"text-muted-foreground ml-auto text-xs tracking-widest",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Command,
|
||||
CommandDialog,
|
||||
CommandInput,
|
||||
CommandList,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandItem,
|
||||
CommandShortcut,
|
||||
CommandSeparator,
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
import * as React from "react"
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||
import { XIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Dialog({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
||||
return <DialogPrimitive.Root data-slot="dialog" {...props} />
|
||||
}
|
||||
|
||||
function DialogTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
||||
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
|
||||
}
|
||||
|
||||
function DialogPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
||||
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
|
||||
}
|
||||
|
||||
function DialogClose({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
||||
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
|
||||
}
|
||||
|
||||
const DialogOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Overlay
|
||||
ref={ref}
|
||||
data-slot="dialog-overlay"
|
||||
className={cn(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
|
||||
DialogOverlay.displayName = "DialogOverlay"
|
||||
|
||||
function DialogContent({
|
||||
className,
|
||||
children,
|
||||
showCloseButton = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
||||
showCloseButton?: boolean
|
||||
}) {
|
||||
return (
|
||||
<DialogPortal data-slot="dialog-portal">
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
data-slot="dialog-content"
|
||||
className={cn(
|
||||
"bg-background 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 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{showCloseButton && (
|
||||
<DialogPrimitive.Close
|
||||
data-slot="dialog-close"
|
||||
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
|
||||
>
|
||||
<XIcon />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
)}
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-header"
|
||||
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-footer"
|
||||
className={cn(
|
||||
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogTitle({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
||||
return (
|
||||
<DialogPrimitive.Title
|
||||
data-slot="dialog-title"
|
||||
className={cn("text-lg leading-none font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
|
||||
return (
|
||||
<DialogPrimitive.Description
|
||||
data-slot="dialog-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogOverlay,
|
||||
DialogPortal,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
import * as React from "react"
|
||||
import { Drawer as DrawerPrimitive } from "vaul"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Drawer({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Root>) {
|
||||
return <DrawerPrimitive.Root data-slot="drawer" {...props} />
|
||||
}
|
||||
|
||||
function DrawerTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Trigger>) {
|
||||
return <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...props} />
|
||||
}
|
||||
|
||||
function DrawerPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Portal>) {
|
||||
return <DrawerPrimitive.Portal data-slot="drawer-portal" {...props} />
|
||||
}
|
||||
|
||||
function DrawerClose({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Close>) {
|
||||
return <DrawerPrimitive.Close data-slot="drawer-close" {...props} />
|
||||
}
|
||||
|
||||
function DrawerOverlay({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Overlay>) {
|
||||
return (
|
||||
<DrawerPrimitive.Overlay
|
||||
data-slot="drawer-overlay"
|
||||
className={cn(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DrawerContent({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Content>) {
|
||||
return (
|
||||
<DrawerPortal data-slot="drawer-portal">
|
||||
<DrawerOverlay />
|
||||
<DrawerPrimitive.Content
|
||||
data-slot="drawer-content"
|
||||
className={cn(
|
||||
"group/drawer-content bg-background fixed z-50 flex h-auto flex-col",
|
||||
"data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b-lg data-[vaul-drawer-direction=top]:border-b",
|
||||
"data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-lg data-[vaul-drawer-direction=bottom]:border-t",
|
||||
"data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:border-l data-[vaul-drawer-direction=right]:sm:max-w-sm",
|
||||
"data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:border-r data-[vaul-drawer-direction=left]:sm:max-w-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="bg-muted mx-auto mt-4 hidden h-2 w-[100px] shrink-0 rounded-full group-data-[vaul-drawer-direction=bottom]/drawer-content:block" />
|
||||
{children}
|
||||
</DrawerPrimitive.Content>
|
||||
</DrawerPortal>
|
||||
)
|
||||
}
|
||||
|
||||
function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="drawer-header"
|
||||
className={cn(
|
||||
"flex flex-col gap-0.5 p-4 group-data-[vaul-drawer-direction=bottom]/drawer-content:text-center group-data-[vaul-drawer-direction=top]/drawer-content:text-center md:gap-1.5 md:text-left",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="drawer-footer"
|
||||
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DrawerTitle({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Title>) {
|
||||
return (
|
||||
<DrawerPrimitive.Title
|
||||
data-slot="drawer-title"
|
||||
className={cn("text-foreground font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DrawerDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Description>) {
|
||||
return (
|
||||
<DrawerPrimitive.Description
|
||||
data-slot="drawer-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Drawer,
|
||||
DrawerPortal,
|
||||
DrawerOverlay,
|
||||
DrawerTrigger,
|
||||
DrawerClose,
|
||||
DrawerContent,
|
||||
DrawerHeader,
|
||||
DrawerFooter,
|
||||
DrawerTitle,
|
||||
DrawerDescription,
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
import * as React from "react"
|
||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
||||
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function DropdownMenu({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
|
||||
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
|
||||
}
|
||||
|
||||
function DropdownMenuPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Trigger
|
||||
data-slot="dropdown-menu-trigger"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuContent({
|
||||
className,
|
||||
sideOffset = 4,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
data-slot="dropdown-menu-content"
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuItem({
|
||||
className,
|
||||
inset,
|
||||
variant = "default",
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
|
||||
inset?: boolean
|
||||
variant?: "default" | "destructive"
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Item
|
||||
data-slot="dropdown-menu-item"
|
||||
data-inset={inset}
|
||||
data-variant={variant}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuCheckboxItem({
|
||||
className,
|
||||
children,
|
||||
checked,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
data-slot="dropdown-menu-checkbox-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<CheckIcon className="size-4" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuRadioGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.RadioGroup
|
||||
data-slot="dropdown-menu-radio-group"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuRadioItem({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
data-slot="dropdown-menu-radio-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<CircleIcon className="size-2 fill-current" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuLabel({
|
||||
className,
|
||||
inset,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
|
||||
inset?: boolean
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Label
|
||||
data-slot="dropdown-menu-label"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
data-slot="dropdown-menu-separator"
|
||||
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuShortcut({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="dropdown-menu-shortcut"
|
||||
className={cn(
|
||||
"text-muted-foreground ml-auto text-xs tracking-widest",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuSub({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
|
||||
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
|
||||
}
|
||||
|
||||
function DropdownMenuSubTrigger({
|
||||
className,
|
||||
inset,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
data-slot="dropdown-menu-sub-trigger"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRightIcon className="ml-auto size-4" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
)
|
||||
}
|
||||
|
||||
const DropdownMenuSubContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
ref={ref}
|
||||
data-slot="dropdown-menu-sub-content"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
|
||||
DropdownMenuSubContent.displayName = "DropdownMenuSubContent"
|
||||
|
||||
|
||||
|
||||
export {
|
||||
DropdownMenu,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuSubContent,
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import * as React from "react"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Input = React.forwardRef<
|
||||
HTMLInputElement,
|
||||
React.ComponentProps<"input">
|
||||
>(({ className, type, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
ref={ref} // ✅ allow refs
|
||||
type={type}
|
||||
data-slot="input"
|
||||
className={cn(
|
||||
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
Input.displayName = "Input"
|
||||
|
||||
export { Input }
|
||||
@@ -0,0 +1,26 @@
|
||||
import * as React from "react"
|
||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
|
||||
const Label = React.forwardRef<
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<LabelPrimitive.Root
|
||||
ref={ref}
|
||||
data-slot="label"
|
||||
className={cn(
|
||||
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50 text-foreground/75",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
|
||||
|
||||
Label.displayName = LabelPrimitive.Root.displayName
|
||||
|
||||
|
||||
export { Label }
|
||||
@@ -0,0 +1,274 @@
|
||||
import * as React from "react"
|
||||
import * as MenubarPrimitive from "@radix-ui/react-menubar"
|
||||
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Menubar({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof MenubarPrimitive.Root>) {
|
||||
return (
|
||||
<MenubarPrimitive.Root
|
||||
data-slot="menubar"
|
||||
className={cn(
|
||||
"bg-background flex h-9 items-center gap-1 rounded-md p-1 ",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function MenubarMenu({
|
||||
...props
|
||||
}: React.ComponentProps<typeof MenubarPrimitive.Menu>) {
|
||||
return <MenubarPrimitive.Menu data-slot="menubar-menu" {...props} />
|
||||
}
|
||||
|
||||
function MenubarGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof MenubarPrimitive.Group>) {
|
||||
return <MenubarPrimitive.Group data-slot="menubar-group" {...props} />
|
||||
}
|
||||
|
||||
function MenubarPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof MenubarPrimitive.Portal>) {
|
||||
return <MenubarPrimitive.Portal data-slot="menubar-portal" {...props} />
|
||||
}
|
||||
|
||||
function MenubarRadioGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof MenubarPrimitive.RadioGroup>) {
|
||||
return (
|
||||
<MenubarPrimitive.RadioGroup data-slot="menubar-radio-group" {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
function MenubarTrigger({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof MenubarPrimitive.Trigger>) {
|
||||
return (
|
||||
<MenubarPrimitive.Trigger
|
||||
data-slot="menubar-trigger"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex items-center rounded-sm px-2 py-1 text-sm font-medium outline-hidden select-none",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function MenubarContent({
|
||||
className,
|
||||
align = "start",
|
||||
alignOffset = -4,
|
||||
sideOffset = 8,
|
||||
...props
|
||||
}: React.ComponentProps<typeof MenubarPrimitive.Content>) {
|
||||
return (
|
||||
<MenubarPortal>
|
||||
<MenubarPrimitive.Content
|
||||
data-slot="menubar-content"
|
||||
align={align}
|
||||
alignOffset={alignOffset}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[12rem] origin-(--radix-menubar-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-md",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</MenubarPortal>
|
||||
)
|
||||
}
|
||||
|
||||
function MenubarItem({
|
||||
className,
|
||||
inset,
|
||||
variant = "default",
|
||||
...props
|
||||
}: React.ComponentProps<typeof MenubarPrimitive.Item> & {
|
||||
inset?: boolean
|
||||
variant?: "default" | "destructive"
|
||||
}) {
|
||||
return (
|
||||
<MenubarPrimitive.Item
|
||||
data-slot="menubar-item"
|
||||
data-inset={inset}
|
||||
data-variant={variant}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function MenubarCheckboxItem({
|
||||
className,
|
||||
children,
|
||||
checked,
|
||||
...props
|
||||
}: React.ComponentProps<typeof MenubarPrimitive.CheckboxItem>) {
|
||||
return (
|
||||
<MenubarPrimitive.CheckboxItem
|
||||
data-slot="menubar-checkbox-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-xs py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<MenubarPrimitive.ItemIndicator>
|
||||
<CheckIcon className="size-4" />
|
||||
</MenubarPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</MenubarPrimitive.CheckboxItem>
|
||||
)
|
||||
}
|
||||
|
||||
function MenubarRadioItem({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof MenubarPrimitive.RadioItem>) {
|
||||
return (
|
||||
<MenubarPrimitive.RadioItem
|
||||
data-slot="menubar-radio-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-xs py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<MenubarPrimitive.ItemIndicator>
|
||||
<CircleIcon className="size-2 fill-current" />
|
||||
</MenubarPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</MenubarPrimitive.RadioItem>
|
||||
)
|
||||
}
|
||||
|
||||
function MenubarLabel({
|
||||
className,
|
||||
inset,
|
||||
...props
|
||||
}: React.ComponentProps<typeof MenubarPrimitive.Label> & {
|
||||
inset?: boolean
|
||||
}) {
|
||||
return (
|
||||
<MenubarPrimitive.Label
|
||||
data-slot="menubar-label"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function MenubarSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof MenubarPrimitive.Separator>) {
|
||||
return (
|
||||
<MenubarPrimitive.Separator
|
||||
data-slot="menubar-separator"
|
||||
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function MenubarShortcut({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="menubar-shortcut"
|
||||
className={cn(
|
||||
"text-muted-foreground ml-auto text-xs tracking-widest",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function MenubarSub({
|
||||
...props
|
||||
}: React.ComponentProps<typeof MenubarPrimitive.Sub>) {
|
||||
return <MenubarPrimitive.Sub data-slot="menubar-sub" {...props} />
|
||||
}
|
||||
|
||||
function MenubarSubTrigger({
|
||||
className,
|
||||
inset,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof MenubarPrimitive.SubTrigger> & {
|
||||
inset?: boolean
|
||||
}) {
|
||||
return (
|
||||
<MenubarPrimitive.SubTrigger
|
||||
data-slot="menubar-sub-trigger"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-none select-none data-[inset]:pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRightIcon className="ml-auto h-4 w-4" />
|
||||
</MenubarPrimitive.SubTrigger>
|
||||
)
|
||||
}
|
||||
|
||||
function MenubarSubContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof MenubarPrimitive.SubContent>) {
|
||||
return (
|
||||
<MenubarPrimitive.SubContent
|
||||
data-slot="menubar-sub-content"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-menubar-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Menubar,
|
||||
MenubarPortal,
|
||||
MenubarMenu,
|
||||
MenubarTrigger,
|
||||
MenubarContent,
|
||||
MenubarGroup,
|
||||
MenubarSeparator,
|
||||
MenubarLabel,
|
||||
MenubarItem,
|
||||
MenubarShortcut,
|
||||
MenubarCheckboxItem,
|
||||
MenubarRadioGroup,
|
||||
MenubarRadioItem,
|
||||
MenubarSub,
|
||||
MenubarSubTrigger,
|
||||
MenubarSubContent,
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import * as React from "react"
|
||||
import * as PopoverPrimitive from "@radix-ui/react-popover"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Popover({
|
||||
...props
|
||||
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
|
||||
return <PopoverPrimitive.Root data-slot="popover" {...props} />
|
||||
}
|
||||
|
||||
function PopoverTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
|
||||
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
|
||||
}
|
||||
|
||||
function PopoverContent({
|
||||
className,
|
||||
align = "center",
|
||||
sideOffset = 4,
|
||||
...props
|
||||
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
|
||||
return (
|
||||
<PopoverPrimitive.Portal>
|
||||
<PopoverPrimitive.Content
|
||||
data-slot="popover-content"
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</PopoverPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
|
||||
function PopoverAnchor({
|
||||
...props
|
||||
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
|
||||
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />
|
||||
}
|
||||
|
||||
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
|
||||
@@ -0,0 +1,43 @@
|
||||
import * as React from "react"
|
||||
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
|
||||
import { CircleIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function RadioGroup({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof RadioGroupPrimitive.Root>) {
|
||||
return (
|
||||
<RadioGroupPrimitive.Root
|
||||
data-slot="radio-group"
|
||||
className={cn("grid gap-3", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function RadioGroupItem({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof RadioGroupPrimitive.Item>) {
|
||||
return (
|
||||
<RadioGroupPrimitive.Item
|
||||
data-slot="radio-group-item"
|
||||
className={cn(
|
||||
"border-input text-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 aspect-square size-4 shrink-0 rounded-full border shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<RadioGroupPrimitive.Indicator
|
||||
data-slot="radio-group-indicator"
|
||||
className="relative flex items-center justify-center"
|
||||
>
|
||||
<CircleIcon className="fill-primary absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2" />
|
||||
</RadioGroupPrimitive.Indicator>
|
||||
</RadioGroupPrimitive.Item>
|
||||
)
|
||||
}
|
||||
|
||||
export { RadioGroup, RadioGroupItem }
|
||||
@@ -0,0 +1,56 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { GripVerticalIcon } from "lucide-react"
|
||||
import * as ResizablePrimitive from "react-resizable-panels"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function ResizablePanelGroup({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) {
|
||||
return (
|
||||
<ResizablePrimitive.PanelGroup
|
||||
data-slot="resizable-panel-group"
|
||||
className={cn(
|
||||
"flex h-full w-full data-[panel-group-direction=vertical]:flex-col",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function ResizablePanel({
|
||||
...props
|
||||
}: React.ComponentProps<typeof ResizablePrimitive.Panel>) {
|
||||
return <ResizablePrimitive.Panel data-slot="resizable-panel" {...props} />
|
||||
}
|
||||
|
||||
function ResizableHandle({
|
||||
withHandle,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {
|
||||
withHandle?: boolean
|
||||
}) {
|
||||
return (
|
||||
<ResizablePrimitive.PanelResizeHandle
|
||||
data-slot="resizable-handle"
|
||||
className={cn(
|
||||
"bg-border focus-visible:ring-ring relative flex w-px items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:outline-hidden data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:translate-x-0 data-[panel-group-direction=vertical]:after:-translate-y-1/2 [&[data-panel-group-direction=vertical]>div]:rotate-90",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{withHandle && (
|
||||
<div className="bg-border z-10 flex h-4 w-3 items-center justify-center rounded-xs border">
|
||||
<GripVerticalIcon className="size-2.5" />
|
||||
</div>
|
||||
)}
|
||||
</ResizablePrimitive.PanelResizeHandle>
|
||||
)
|
||||
}
|
||||
|
||||
export { ResizablePanelGroup, ResizablePanel, ResizableHandle }
|
||||
@@ -0,0 +1,56 @@
|
||||
import * as React from "react"
|
||||
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function ScrollArea({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
|
||||
return (
|
||||
<ScrollAreaPrimitive.Root
|
||||
data-slot="scroll-area"
|
||||
className={cn("relative", className)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.Viewport
|
||||
data-slot="scroll-area-viewport"
|
||||
className="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1"
|
||||
>
|
||||
{children}
|
||||
</ScrollAreaPrimitive.Viewport>
|
||||
<ScrollBar />
|
||||
<ScrollAreaPrimitive.Corner />
|
||||
</ScrollAreaPrimitive.Root>
|
||||
)
|
||||
}
|
||||
|
||||
function ScrollBar({
|
||||
className,
|
||||
orientation = "vertical",
|
||||
...props
|
||||
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
|
||||
return (
|
||||
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||
data-slot="scroll-area-scrollbar"
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"flex touch-none p-px transition-colors select-none",
|
||||
orientation === "vertical" &&
|
||||
"h-full w-2 border-l border-l-transparent",
|
||||
orientation === "horizontal" &&
|
||||
"h-2.5 flex-col border-t border-t-transparent",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.ScrollAreaThumb
|
||||
data-slot="scroll-area-thumb"
|
||||
className="bg-border relative flex-1 rounded-full"
|
||||
/>
|
||||
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
)
|
||||
}
|
||||
|
||||
export { ScrollArea, ScrollBar }
|
||||
@@ -0,0 +1,186 @@
|
||||
import * as React from "react"
|
||||
import * as SelectPrimitive from "@radix-ui/react-select"
|
||||
import { CheckIcon, ChevronDownIcon, ChevronUpIcon, X } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Select({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
|
||||
return <SelectPrimitive.Root data-slot="select" {...props} />
|
||||
}
|
||||
|
||||
function SelectGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
|
||||
return <SelectPrimitive.Group data-slot="select-group" {...props} />
|
||||
}
|
||||
|
||||
function SelectValue({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
|
||||
return <SelectPrimitive.Value data-slot="select-value" {...props} />
|
||||
}
|
||||
|
||||
function SelectTrigger({
|
||||
className,
|
||||
size = "default",
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
|
||||
size?: "sm" | "default"
|
||||
}) {
|
||||
return (
|
||||
<SelectPrimitive.Trigger
|
||||
data-slot="select-trigger"
|
||||
data-size={size}
|
||||
className={cn(
|
||||
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild >
|
||||
|
||||
<ChevronDownIcon className="size-4 opacity-50" />
|
||||
|
||||
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectContent({
|
||||
className,
|
||||
children,
|
||||
position = "popper",
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
|
||||
return (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
data-slot="select-content"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
|
||||
position === "popper" &&
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className
|
||||
)}
|
||||
position={position}
|
||||
{...props}
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport
|
||||
className={cn(
|
||||
"p-1",
|
||||
position === "popper" &&
|
||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectLabel({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
|
||||
return (
|
||||
<SelectPrimitive.Label
|
||||
data-slot="select-label"
|
||||
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectItem({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
|
||||
return (
|
||||
<SelectPrimitive.Item
|
||||
data-slot="select-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute right-2 flex size-3.5 items-center justify-center">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<CheckIcon className="size-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
|
||||
return (
|
||||
<SelectPrimitive.Separator
|
||||
data-slot="select-separator"
|
||||
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectScrollUpButton({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
|
||||
return (
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
data-slot="select-scroll-up-button"
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronUpIcon className="size-4" />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectScrollDownButton({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
|
||||
return (
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
data-slot="select-scroll-down-button"
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronDownIcon className="size-4" />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectScrollDownButton,
|
||||
SelectScrollUpButton,
|
||||
SelectSeparator,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import * as React from "react"
|
||||
import * as SeparatorPrimitive from "@radix-ui/react-separator"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Separator({
|
||||
className,
|
||||
orientation = "horizontal",
|
||||
decorative = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
|
||||
return (
|
||||
<SeparatorPrimitive.Root
|
||||
data-slot="separator"
|
||||
decorative={decorative}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Separator }
|
||||
@@ -0,0 +1,272 @@
|
||||
import {
|
||||
LoaderCircleIcon,
|
||||
LoaderIcon,
|
||||
LoaderPinwheelIcon,
|
||||
type LucideProps,
|
||||
} from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
type SpinnerVariantProps = Omit<SpinnerProps, 'variant'>;
|
||||
|
||||
const Default = ({ className, ...props }: SpinnerVariantProps) => (
|
||||
<LoaderIcon className={cn('animate-spin', className)} {...props} />
|
||||
);
|
||||
|
||||
const Circle = ({ className, ...props }: SpinnerVariantProps) => (
|
||||
<LoaderCircleIcon className={cn('animate-spin', className)} {...props} />
|
||||
);
|
||||
|
||||
const Pinwheel = ({ className, ...props }: SpinnerVariantProps) => (
|
||||
<LoaderPinwheelIcon className={cn('animate-spin', className)} {...props} />
|
||||
);
|
||||
|
||||
const CircleFilled = ({
|
||||
className,
|
||||
size = 24,
|
||||
...props
|
||||
}: SpinnerVariantProps) => (
|
||||
<div className="relative" style={{ width: size, height: size }}>
|
||||
<div className="absolute inset-0 rotate-180">
|
||||
<LoaderCircleIcon
|
||||
className={cn('animate-spin', className, 'text-foreground opacity-20')}
|
||||
size={size}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
<LoaderCircleIcon
|
||||
className={cn('relative animate-spin', className)}
|
||||
size={size}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
const Ellipsis = ({ size = 24, ...props }: SpinnerVariantProps) => {
|
||||
return (
|
||||
<svg
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
width={size}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<title>Loading...</title>
|
||||
<circle cx="4" cy="12" fill="currentColor" r="2">
|
||||
<animate
|
||||
attributeName="cy"
|
||||
begin="0;ellipsis3.end+0.25s"
|
||||
calcMode="spline"
|
||||
dur="0.6s"
|
||||
id="ellipsis1"
|
||||
keySplines=".33,.66,.66,1;.33,0,.66,.33"
|
||||
values="12;6;12"
|
||||
/>
|
||||
</circle>
|
||||
<circle cx="12" cy="12" fill="currentColor" r="2">
|
||||
<animate
|
||||
attributeName="cy"
|
||||
begin="ellipsis1.begin+0.1s"
|
||||
calcMode="spline"
|
||||
dur="0.6s"
|
||||
keySplines=".33,.66,.66,1;.33,0,.66,.33"
|
||||
values="12;6;12"
|
||||
/>
|
||||
</circle>
|
||||
<circle cx="20" cy="12" fill="currentColor" r="2">
|
||||
<animate
|
||||
attributeName="cy"
|
||||
begin="ellipsis1.begin+0.2s"
|
||||
calcMode="spline"
|
||||
dur="0.6s"
|
||||
id="ellipsis3"
|
||||
keySplines=".33,.66,.66,1;.33,0,.66,.33"
|
||||
values="12;6;12"
|
||||
/>
|
||||
</circle>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
const Ring = ({ size = 24, ...props }: SpinnerVariantProps) => (
|
||||
<svg
|
||||
height={size}
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 44 44"
|
||||
width={size}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<title>Loading...</title>
|
||||
<g fill="none" fillRule="evenodd" strokeWidth="2">
|
||||
<circle cx="22" cy="22" r="1">
|
||||
<animate
|
||||
attributeName="r"
|
||||
begin="0s"
|
||||
calcMode="spline"
|
||||
dur="1.8s"
|
||||
keySplines="0.165, 0.84, 0.44, 1"
|
||||
keyTimes="0; 1"
|
||||
repeatCount="indefinite"
|
||||
values="1; 20"
|
||||
/>
|
||||
<animate
|
||||
attributeName="stroke-opacity"
|
||||
begin="0s"
|
||||
calcMode="spline"
|
||||
dur="1.8s"
|
||||
keySplines="0.3, 0.61, 0.355, 1"
|
||||
keyTimes="0; 1"
|
||||
repeatCount="indefinite"
|
||||
values="1; 0"
|
||||
/>
|
||||
</circle>
|
||||
<circle cx="22" cy="22" r="1">
|
||||
<animate
|
||||
attributeName="r"
|
||||
begin="-0.9s"
|
||||
calcMode="spline"
|
||||
dur="1.8s"
|
||||
keySplines="0.165, 0.84, 0.44, 1"
|
||||
keyTimes="0; 1"
|
||||
repeatCount="indefinite"
|
||||
values="1; 20"
|
||||
/>
|
||||
<animate
|
||||
attributeName="stroke-opacity"
|
||||
begin="-0.9s"
|
||||
calcMode="spline"
|
||||
dur="1.8s"
|
||||
keySplines="0.3, 0.61, 0.355, 1"
|
||||
keyTimes="0; 1"
|
||||
repeatCount="indefinite"
|
||||
values="1; 0"
|
||||
/>
|
||||
</circle>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const Bars = ({ size = 24, ...props }: SpinnerVariantProps) => (
|
||||
<svg
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
width={size}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<title>Loading...</title>
|
||||
<style>{`
|
||||
.spinner-bar {
|
||||
animation: spinner-bars-animation .8s linear infinite;
|
||||
animation-delay: -.8s;
|
||||
}
|
||||
.spinner-bars-2 {
|
||||
animation-delay: -.65s;
|
||||
}
|
||||
.spinner-bars-3 {
|
||||
animation-delay: -0.5s;
|
||||
}
|
||||
@keyframes spinner-bars-animation {
|
||||
0% {
|
||||
y: 1px;
|
||||
height: 22px;
|
||||
}
|
||||
93.75% {
|
||||
y: 5px;
|
||||
height: 14px;
|
||||
opacity: 0.2;
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
<rect
|
||||
className="spinner-bar"
|
||||
fill="currentColor"
|
||||
height="22"
|
||||
width="6"
|
||||
x="1"
|
||||
y="1"
|
||||
/>
|
||||
<rect
|
||||
className="spinner-bar spinner-bars-2"
|
||||
fill="currentColor"
|
||||
height="22"
|
||||
width="6"
|
||||
x="9"
|
||||
y="1"
|
||||
/>
|
||||
<rect
|
||||
className="spinner-bar spinner-bars-3"
|
||||
fill="currentColor"
|
||||
height="22"
|
||||
width="6"
|
||||
x="17"
|
||||
y="1"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const Infinite = ({ size = 24, ...props }: SpinnerVariantProps) => (
|
||||
<svg
|
||||
height={size}
|
||||
preserveAspectRatio="xMidYMid"
|
||||
viewBox="0 0 100 100"
|
||||
width={size}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<title>Loading...</title>
|
||||
<path
|
||||
d="M24.3 30C11.4 30 5 43.3 5 50s6.4 20 19.3 20c19.3 0 32.1-40 51.4-40 C88.6 30 95 43.3 95 50s-6.4 20-19.3 20C56.4 70 43.6 30 24.3 30z"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeDasharray="205.271142578125 51.317785644531256"
|
||||
strokeLinecap="round"
|
||||
strokeWidth="10"
|
||||
style={{
|
||||
transform: 'scale(0.8)',
|
||||
transformOrigin: '50px 50px',
|
||||
}}
|
||||
>
|
||||
<animate
|
||||
attributeName="stroke-dashoffset"
|
||||
dur="2s"
|
||||
keyTimes="0;1"
|
||||
repeatCount="indefinite"
|
||||
values="0;256.58892822265625"
|
||||
/>
|
||||
</path>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export type SpinnerProps = LucideProps & {
|
||||
variant?:
|
||||
| 'default'
|
||||
| 'circle'
|
||||
| 'pinwheel'
|
||||
| 'circle-filled'
|
||||
| 'ellipsis'
|
||||
| 'ring'
|
||||
| 'bars'
|
||||
| 'infinite';
|
||||
};
|
||||
|
||||
export const Spinner = ({ variant, ...props }: SpinnerProps) => {
|
||||
switch (variant) {
|
||||
case 'circle':
|
||||
return <Circle {...props} />;
|
||||
case 'pinwheel':
|
||||
return <Pinwheel {...props} />;
|
||||
case 'circle-filled':
|
||||
return <CircleFilled {...props} />;
|
||||
case 'ellipsis':
|
||||
return <Ellipsis {...props} />;
|
||||
case 'ring':
|
||||
return <Ring {...props} />;
|
||||
case 'bars':
|
||||
return <Bars {...props} />;
|
||||
case 'infinite':
|
||||
return <Infinite {...props} />;
|
||||
default:
|
||||
return <Default {...props} />;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,139 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as SheetPrimitive from "@radix-ui/react-dialog"
|
||||
import { XIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
|
||||
return <SheetPrimitive.Root data-slot="sheet" {...props} />
|
||||
}
|
||||
|
||||
function SheetTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
|
||||
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />
|
||||
}
|
||||
|
||||
function SheetClose({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Close>) {
|
||||
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />
|
||||
}
|
||||
|
||||
function SheetPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Portal>) {
|
||||
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />
|
||||
}
|
||||
|
||||
function SheetOverlay({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Overlay>) {
|
||||
return (
|
||||
<SheetPrimitive.Overlay
|
||||
data-slot="sheet-overlay"
|
||||
className={cn(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SheetContent({
|
||||
className,
|
||||
children,
|
||||
side = "right",
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
|
||||
side?: "top" | "right" | "bottom" | "left"
|
||||
}) {
|
||||
return (
|
||||
<SheetPortal>
|
||||
<SheetOverlay />
|
||||
<SheetPrimitive.Content
|
||||
data-slot="sheet-content"
|
||||
className={cn(
|
||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
|
||||
side === "right" &&
|
||||
"data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm",
|
||||
side === "left" &&
|
||||
"data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm",
|
||||
side === "top" &&
|
||||
"data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b",
|
||||
side === "bottom" &&
|
||||
"data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none">
|
||||
<XIcon className="size-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</SheetPrimitive.Close>
|
||||
</SheetPrimitive.Content>
|
||||
</SheetPortal>
|
||||
)
|
||||
}
|
||||
|
||||
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sheet-header"
|
||||
className={cn("flex flex-col gap-1.5 p-4", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sheet-footer"
|
||||
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SheetTitle({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Title>) {
|
||||
return (
|
||||
<SheetPrimitive.Title
|
||||
data-slot="sheet-title"
|
||||
className={cn("text-foreground font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SheetDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Description>) {
|
||||
return (
|
||||
<SheetPrimitive.Description
|
||||
data-slot="sheet-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Sheet,
|
||||
SheetTrigger,
|
||||
SheetClose,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetFooter,
|
||||
SheetTitle,
|
||||
SheetDescription,
|
||||
}
|
||||
@@ -0,0 +1,740 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, VariantProps } from "class-variance-authority"
|
||||
import { PanelLeftIcon } from "lucide-react"
|
||||
|
||||
import { useIsMobile } from "@/hooks/use-mobile"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetDescription,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
} from "@/components/ui/sheet"
|
||||
import { Skeleton } from "@/components/ui/skeleton"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip"
|
||||
|
||||
const SIDEBAR_COOKIE_NAME = "sidebar_state"
|
||||
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
|
||||
const SIDEBAR_WIDTH = "16rem"
|
||||
const SIDEBAR_WIDTH_MOBILE = "18rem"
|
||||
const SIDEBAR_WIDTH_ICON = "3rem"
|
||||
const SIDEBAR_KEYBOARD_SHORTCUT = "b"
|
||||
|
||||
type SidebarContextProps = {
|
||||
state: "expanded" | "collapsed"
|
||||
open: boolean
|
||||
setOpen: (open: boolean) => void
|
||||
openMobile: boolean
|
||||
setOpenMobile: (open: boolean) => void
|
||||
isMobile: boolean
|
||||
toggleSidebar: () => void
|
||||
}
|
||||
|
||||
const SidebarContext = React.createContext<SidebarContextProps | null>(null)
|
||||
|
||||
function useSidebar() {
|
||||
const context = React.useContext(SidebarContext)
|
||||
if (!context) {
|
||||
throw new Error("useSidebar must be used within a SidebarProvider.")
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
function SidebarProvider({
|
||||
defaultOpen = true,
|
||||
open: openProp,
|
||||
onOpenChange: setOpenProp,
|
||||
className,
|
||||
style,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & {
|
||||
defaultOpen?: boolean
|
||||
open?: boolean
|
||||
onOpenChange?: (open: boolean) => void
|
||||
}) {
|
||||
const isMobile = useIsMobile()
|
||||
const [openMobile, setOpenMobile] = React.useState(false)
|
||||
|
||||
// This is the internal state of the sidebar.
|
||||
// We use openProp and setOpenProp for control from outside the component.
|
||||
const [_open, _setOpen] = React.useState(defaultOpen)
|
||||
const open = openProp ?? _open
|
||||
const setOpen = React.useCallback(
|
||||
(value: boolean | ((value: boolean) => boolean)) => {
|
||||
const openState = typeof value === "function" ? value(open) : value
|
||||
if (setOpenProp) {
|
||||
setOpenProp(openState)
|
||||
} else {
|
||||
_setOpen(openState)
|
||||
}
|
||||
|
||||
// This sets the cookie to keep the sidebar state.
|
||||
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
|
||||
},
|
||||
[setOpenProp, open]
|
||||
)
|
||||
|
||||
// Helper to toggle the sidebar.
|
||||
const toggleSidebar = React.useCallback(() => {
|
||||
return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open)
|
||||
}, [isMobile, setOpen, setOpenMobile])
|
||||
|
||||
// Adds a keyboard shortcut to toggle the sidebar.
|
||||
/*
|
||||
React.useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (
|
||||
event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
|
||||
(event.metaKey || event.ctrlKey)
|
||||
) {
|
||||
event.preventDefault()
|
||||
toggleSidebar()
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("keydown", handleKeyDown)
|
||||
return () => window.removeEventListener("keydown", handleKeyDown)
|
||||
}, [toggleSidebar])
|
||||
*/
|
||||
// We add a state so that we can do data-state="expanded" or "collapsed".
|
||||
// This makes it easier to style the sidebar with Tailwind classes.
|
||||
const state = open ? "expanded" : "collapsed"
|
||||
|
||||
const contextValue = React.useMemo<SidebarContextProps>(
|
||||
() => ({
|
||||
state,
|
||||
open,
|
||||
setOpen,
|
||||
isMobile,
|
||||
openMobile,
|
||||
setOpenMobile,
|
||||
toggleSidebar,
|
||||
}),
|
||||
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]
|
||||
)
|
||||
|
||||
return (
|
||||
<SidebarContext.Provider value={contextValue}>
|
||||
|
||||
<div
|
||||
data-slot="sidebar-wrapper"
|
||||
style={
|
||||
{
|
||||
"--sidebar-width": SIDEBAR_WIDTH,
|
||||
"--sidebar-width-icon": SIDEBAR_WIDTH_ICON,
|
||||
...style,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
className={cn(
|
||||
"group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex min-h-svh w-full",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
</SidebarContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
function Sidebar({
|
||||
side = "left",
|
||||
variant = "sidebar",
|
||||
collapsible = "offcanvas",
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & {
|
||||
side?: "left" | "right"
|
||||
variant?: "sidebar" | "floating" | "inset"
|
||||
collapsible?: "offcanvas" | "icon" | "none"
|
||||
}) {
|
||||
const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
|
||||
|
||||
if (collapsible === "none") {
|
||||
return (
|
||||
<div
|
||||
data-slot="sidebar"
|
||||
className={cn(
|
||||
"bg-sidebar text-sidebar-foreground flex h-full w-(--sidebar-width) flex-col",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (isMobile) {
|
||||
return (
|
||||
<Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
|
||||
<SheetContent
|
||||
data-sidebar="sidebar"
|
||||
data-slot="sidebar"
|
||||
data-mobile="true"
|
||||
className="bg-sidebar text-sidebar-foreground w-(--sidebar-width) p-0 [&>button]:hidden"
|
||||
style={
|
||||
{
|
||||
"--sidebar-width": SIDEBAR_WIDTH_MOBILE,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
side={side}
|
||||
>
|
||||
<SheetHeader className="sr-only">
|
||||
<SheetTitle>Sidebar</SheetTitle>
|
||||
<SheetDescription>Displays the mobile sidebar.</SheetDescription>
|
||||
</SheetHeader>
|
||||
<div className="flex h-full w-full flex-col">{children}</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="group peer text-sidebar-foreground hidden md:block"
|
||||
data-state={state}
|
||||
data-collapsible={state === "collapsed" ? collapsible : ""}
|
||||
data-variant={variant}
|
||||
data-side={side}
|
||||
data-slot="sidebar"
|
||||
>
|
||||
{/* This is what handles the sidebar gap on desktop */}
|
||||
<div
|
||||
data-slot="sidebar-gap"
|
||||
className={cn(
|
||||
"relative w-(--sidebar-width) bg-transparent transition-[width] duration-200 ease-linear",
|
||||
"group-data-[collapsible=offcanvas]:w-0",
|
||||
"group-data-[side=right]:rotate-180",
|
||||
variant === "floating" || variant === "inset"
|
||||
? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]"
|
||||
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon)"
|
||||
)}
|
||||
/>
|
||||
<div
|
||||
data-slot="sidebar-container"
|
||||
className={cn(
|
||||
"fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear md:flex",
|
||||
side === "left"
|
||||
? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"
|
||||
: "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
|
||||
// Adjust the padding for floating and inset variants.
|
||||
variant === "floating" || variant === "inset"
|
||||
? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]"
|
||||
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div
|
||||
data-sidebar="sidebar"
|
||||
data-slot="sidebar-inner"
|
||||
className="bg-sidebar group-data-[variant=floating]:border-sidebar-border flex h-full w-full flex-col group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:shadow-sm"
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarTrigger({
|
||||
className,
|
||||
onClick,
|
||||
...props
|
||||
}: React.ComponentProps<typeof Button>) {
|
||||
const { toggleSidebar } = useSidebar()
|
||||
|
||||
return (
|
||||
<Button
|
||||
data-sidebar="trigger"
|
||||
data-slot="sidebar-trigger"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className={cn("size-7", className)}
|
||||
onClick={(event) => {
|
||||
onClick?.(event)
|
||||
toggleSidebar()
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
<PanelLeftIcon />
|
||||
<span className="sr-only">Toggle Sidebar</span>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
|
||||
const { toggleSidebar } = useSidebar()
|
||||
|
||||
return (
|
||||
<button
|
||||
data-sidebar="rail"
|
||||
data-slot="sidebar-rail"
|
||||
aria-label="Toggle Sidebar"
|
||||
tabIndex={-1}
|
||||
onClick={toggleSidebar}
|
||||
title="Toggle Sidebar"
|
||||
className={cn(
|
||||
"hover:after:bg-sidebar-border absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] sm:flex",
|
||||
"in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize",
|
||||
"[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize",
|
||||
"hover:group-data-[collapsible=offcanvas]:bg-sidebar group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full",
|
||||
"[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
|
||||
"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarInset({ className, ...props }: React.ComponentProps<"main">) {
|
||||
return (
|
||||
<main
|
||||
data-slot="sidebar-inset"
|
||||
className={cn(
|
||||
"bg-background relative flex w-full flex-1 flex-col",
|
||||
"md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarInput({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof Input>) {
|
||||
return (
|
||||
<Input
|
||||
data-slot="sidebar-input"
|
||||
data-sidebar="input"
|
||||
className={cn("bg-background h-8 w-full shadow-none", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sidebar-header"
|
||||
data-sidebar="header"
|
||||
className={cn("flex flex-col gap-2 p-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sidebar-footer"
|
||||
data-sidebar="footer"
|
||||
className={cn("flex flex-col gap-2 p-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof Separator>) {
|
||||
return (
|
||||
<Separator
|
||||
data-slot="sidebar-separator"
|
||||
data-sidebar="separator"
|
||||
className={cn("bg-sidebar-border mx-2 w-auto", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sidebar-content"
|
||||
data-sidebar="content"
|
||||
className={cn(
|
||||
"flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sidebar-group"
|
||||
data-sidebar="group"
|
||||
className={cn("relative flex w-full min-w-0 flex-col p-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarGroupLabel({
|
||||
className,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & { asChild?: boolean }) {
|
||||
const Comp = asChild ? Slot : "div"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="sidebar-group-label"
|
||||
data-sidebar="group-label"
|
||||
className={cn(
|
||||
"text-sidebar-foreground/70 ring-sidebar-ring flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium outline-hidden transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
"group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarGroupAction({
|
||||
className,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"button"> & { asChild?: boolean }) {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="sidebar-group-action"
|
||||
data-sidebar="group-action"
|
||||
className={cn(
|
||||
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
// Increases the hit area of the button on mobile.
|
||||
"after:absolute after:-inset-2 md:after:hidden",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarGroupContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sidebar-group-content"
|
||||
data-sidebar="group-content"
|
||||
className={cn("w-full text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarMenu({ className, ...props }: React.ComponentProps<"ul">) {
|
||||
return (
|
||||
<ul
|
||||
data-slot="sidebar-menu"
|
||||
data-sidebar="menu"
|
||||
className={cn("flex w-full min-w-0 flex-col gap-1", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
|
||||
return (
|
||||
<li
|
||||
data-slot="sidebar-menu-item"
|
||||
data-sidebar="menu-item"
|
||||
className={cn("group/menu-item relative", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const sidebarMenuButtonVariants = cva(
|
||||
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
|
||||
outline:
|
||||
"bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
|
||||
},
|
||||
size: {
|
||||
default: "h-8 text-sm",
|
||||
sm: "h-7 text-xs",
|
||||
lg: "h-12 text-sm group-data-[collapsible=icon]:p-0!",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
const SidebarMenuButton = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
React.ComponentProps<"button"> & {
|
||||
asChild?: boolean
|
||||
isActive?: boolean
|
||||
tooltip?: string | React.ComponentProps<typeof TooltipContent>
|
||||
} & VariantProps<typeof sidebarMenuButtonVariants>
|
||||
>(
|
||||
(
|
||||
{
|
||||
asChild = false,
|
||||
isActive = false,
|
||||
variant = "default",
|
||||
size = "default",
|
||||
tooltip,
|
||||
className,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
const { isMobile, state } = useSidebar()
|
||||
|
||||
const button = (
|
||||
<Comp
|
||||
ref={ref}
|
||||
data-slot="sidebar-menu-button"
|
||||
data-sidebar="menu-button"
|
||||
data-size={size}
|
||||
data-active={isActive}
|
||||
className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
||||
if (!tooltip) {
|
||||
return button
|
||||
}
|
||||
|
||||
if (typeof tooltip === "string") {
|
||||
tooltip = { children: tooltip }
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>{button}</TooltipTrigger>
|
||||
<TooltipContent
|
||||
side="right"
|
||||
align="center"
|
||||
hidden={state !== "collapsed" || isMobile}
|
||||
{...tooltip}
|
||||
/>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
SidebarMenuButton.displayName = "SidebarMenuButton"
|
||||
|
||||
function SidebarMenuAction({
|
||||
className,
|
||||
asChild = false,
|
||||
showOnHover = false,
|
||||
...props
|
||||
}: React.ComponentProps<"button"> & {
|
||||
asChild?: boolean
|
||||
showOnHover?: boolean
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="sidebar-menu-action"
|
||||
data-sidebar="menu-action"
|
||||
className={cn(
|
||||
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground peer-hover/menu-button:text-sidebar-accent-foreground absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
// Increases the hit area of the button on mobile.
|
||||
"after:absolute after:-inset-2 md:after:hidden",
|
||||
"peer-data-[size=sm]/menu-button:top-1",
|
||||
"peer-data-[size=default]/menu-button:top-1.5",
|
||||
"peer-data-[size=lg]/menu-button:top-2.5",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
showOnHover &&
|
||||
"peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarMenuBadge({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sidebar-menu-badge"
|
||||
data-sidebar="menu-badge"
|
||||
className={cn(
|
||||
"text-sidebar-foreground pointer-events-none absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums select-none",
|
||||
"peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground",
|
||||
"peer-data-[size=sm]/menu-button:top-1",
|
||||
"peer-data-[size=default]/menu-button:top-1.5",
|
||||
"peer-data-[size=lg]/menu-button:top-2.5",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarMenuSkeleton({
|
||||
className,
|
||||
showIcon = false,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & {
|
||||
showIcon?: boolean
|
||||
}) {
|
||||
// Random width between 50 to 90%.
|
||||
const width = React.useMemo(() => {
|
||||
return `${Math.floor(Math.random() * 40) + 50}%`
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div
|
||||
data-slot="sidebar-menu-skeleton"
|
||||
data-sidebar="menu-skeleton"
|
||||
className={cn("flex h-8 items-center gap-2 rounded-md px-2", className)}
|
||||
{...props}
|
||||
>
|
||||
{showIcon && (
|
||||
<Skeleton
|
||||
className="size-4 rounded-md"
|
||||
data-sidebar="menu-skeleton-icon"
|
||||
/>
|
||||
)}
|
||||
<Skeleton
|
||||
className="h-4 max-w-(--skeleton-width) flex-1"
|
||||
data-sidebar="menu-skeleton-text"
|
||||
style={
|
||||
{
|
||||
"--skeleton-width": width,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) {
|
||||
return (
|
||||
<ul
|
||||
data-slot="sidebar-menu-sub"
|
||||
data-sidebar="menu-sub"
|
||||
className={cn(
|
||||
"border-sidebar-border mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l px-2.5 py-0.5",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarMenuSubItem({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"li">) {
|
||||
return (
|
||||
<li
|
||||
data-slot="sidebar-menu-sub-item"
|
||||
data-sidebar="menu-sub-item"
|
||||
className={cn("group/menu-sub-item relative", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const SidebarMenuSubButton = React.forwardRef<
|
||||
HTMLAnchorElement,
|
||||
React.ComponentProps<"a"> & {
|
||||
asChild?: boolean
|
||||
size?: "sm" | "md"
|
||||
isActive?: boolean
|
||||
}
|
||||
>(({ asChild = false, size = "md", isActive = false, className, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "a"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
ref={ref}
|
||||
data-slot="sidebar-menu-sub-button"
|
||||
data-sidebar="menu-sub-button"
|
||||
data-size={size}
|
||||
data-active={isActive}
|
||||
className={cn(
|
||||
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground [&>svg]:text-sidebar-accent-foreground flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline-hidden focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
"data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
|
||||
size === "sm" && "text-xs",
|
||||
size === "md" && "text-sm",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
SidebarMenuSubButton.displayName = "SidebarMenuSubButton"
|
||||
|
||||
|
||||
|
||||
|
||||
export {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarFooter,
|
||||
SidebarGroup,
|
||||
SidebarGroupAction,
|
||||
SidebarGroupContent,
|
||||
SidebarGroupLabel,
|
||||
SidebarHeader,
|
||||
SidebarInput,
|
||||
SidebarInset,
|
||||
SidebarMenu,
|
||||
SidebarMenuAction,
|
||||
SidebarMenuBadge,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
SidebarMenuSkeleton,
|
||||
SidebarMenuSub,
|
||||
SidebarMenuSubButton,
|
||||
SidebarMenuSubItem,
|
||||
SidebarProvider,
|
||||
SidebarRail,
|
||||
SidebarSeparator,
|
||||
SidebarTrigger,
|
||||
useSidebar,
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="skeleton"
|
||||
className={cn("bg-accent animate-pulse rounded-md", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Skeleton }
|
||||
@@ -0,0 +1,25 @@
|
||||
|
||||
|
||||
import { useTheme } from "@/providers/theme-provider/theme-provider"
|
||||
import { Toaster as Sonner, ToasterProps } from "sonner"
|
||||
|
||||
const Toaster = ({ ...props }: ToasterProps) => {
|
||||
const { theme = "system" } = useTheme()
|
||||
|
||||
return (
|
||||
<Sonner
|
||||
theme={theme as ToasterProps["theme"]}
|
||||
className="toaster group"
|
||||
style={
|
||||
{
|
||||
"--normal-bg": "var(--popover)",
|
||||
"--normal-text": "var(--popover-foreground)",
|
||||
"--normal-border": "var(--border)",
|
||||
} as React.CSSProperties
|
||||
}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Toaster }
|
||||
@@ -0,0 +1,29 @@
|
||||
import * as React from "react"
|
||||
import * as SwitchPrimitive from "@radix-ui/react-switch"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Switch({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SwitchPrimitive.Root>) {
|
||||
return (
|
||||
<SwitchPrimitive.Root
|
||||
data-slot="switch"
|
||||
className={cn(
|
||||
"peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<SwitchPrimitive.Thumb
|
||||
data-slot="switch-thumb"
|
||||
className={cn(
|
||||
"bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0"
|
||||
)}
|
||||
/>
|
||||
</SwitchPrimitive.Root>
|
||||
)
|
||||
}
|
||||
|
||||
export { Switch }
|
||||
@@ -0,0 +1,114 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Table({ className, ...props }: React.ComponentProps<"table">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="table-container"
|
||||
className="relative w-full overflow-x-auto"
|
||||
>
|
||||
<table
|
||||
data-slot="table"
|
||||
className={cn("w-full caption-bottom text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
|
||||
return (
|
||||
<thead
|
||||
data-slot="table-header"
|
||||
className={cn("[&_tr]:border-b", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
|
||||
return (
|
||||
<tbody
|
||||
data-slot="table-body"
|
||||
className={cn("[&_tr:last-child]:border-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
|
||||
return (
|
||||
<tfoot
|
||||
data-slot="table-footer"
|
||||
className={cn(
|
||||
"bg-muted/50 border-t font-medium [&>tr]:last:border-b-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
|
||||
return (
|
||||
<tr
|
||||
data-slot="table-row"
|
||||
className={cn(
|
||||
"hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
|
||||
return (
|
||||
<th
|
||||
data-slot="table-head"
|
||||
className={cn(
|
||||
"text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
|
||||
return (
|
||||
<td
|
||||
data-slot="table-cell"
|
||||
className={cn(
|
||||
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableCaption({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"caption">) {
|
||||
return (
|
||||
<caption
|
||||
data-slot="table-caption"
|
||||
className={cn("text-muted-foreground mt-4 text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableBody,
|
||||
TableFooter,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableCaption,
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import * as React from "react"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Textarea = React.forwardRef<
|
||||
HTMLTextAreaElement,
|
||||
React.ComponentProps<"textarea">
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<textarea
|
||||
ref={ref}
|
||||
data-slot="textarea"
|
||||
className={cn(
|
||||
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
Textarea.displayName = "Textarea"
|
||||
|
||||
export { Textarea }
|
||||
@@ -0,0 +1,73 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group"
|
||||
import { type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { toggleVariants } from "@/components/ui/toggle"
|
||||
|
||||
const ToggleGroupContext = React.createContext<
|
||||
VariantProps<typeof toggleVariants>
|
||||
>({
|
||||
size: "default",
|
||||
variant: "default",
|
||||
})
|
||||
|
||||
function ToggleGroup({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ToggleGroupPrimitive.Root> &
|
||||
VariantProps<typeof toggleVariants>) {
|
||||
return (
|
||||
<ToggleGroupPrimitive.Root
|
||||
data-slot="toggle-group"
|
||||
data-variant={variant}
|
||||
data-size={size}
|
||||
className={cn(
|
||||
"group/toggle-group flex w-fit items-center rounded-md data-[variant=outline]:shadow-xs",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ToggleGroupContext.Provider value={{ variant, size }}>
|
||||
{children}
|
||||
</ToggleGroupContext.Provider>
|
||||
</ToggleGroupPrimitive.Root>
|
||||
)
|
||||
}
|
||||
|
||||
function ToggleGroupItem({
|
||||
className,
|
||||
children,
|
||||
variant,
|
||||
size,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ToggleGroupPrimitive.Item> &
|
||||
VariantProps<typeof toggleVariants>) {
|
||||
const context = React.useContext(ToggleGroupContext)
|
||||
|
||||
return (
|
||||
<ToggleGroupPrimitive.Item
|
||||
data-slot="toggle-group-item"
|
||||
data-variant={context.variant || variant}
|
||||
data-size={context.size || size}
|
||||
className={cn(
|
||||
toggleVariants({
|
||||
variant: context.variant || variant,
|
||||
size: context.size || size,
|
||||
}),
|
||||
"min-w-0 flex-1 shrink-0 rounded-none shadow-none first:rounded-l-md last:rounded-r-md focus:z-10 focus-visible:z-10 data-[variant=outline]:border-l-0 data-[variant=outline]:first:border-l",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</ToggleGroupPrimitive.Item>
|
||||
)
|
||||
}
|
||||
|
||||
export { ToggleGroup, ToggleGroupItem }
|
||||
@@ -0,0 +1,45 @@
|
||||
import * as React from "react"
|
||||
import * as TogglePrimitive from "@radix-ui/react-toggle"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const toggleVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] outline-none transition-[color,box-shadow] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-transparent",
|
||||
outline:
|
||||
"border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-2 min-w-9",
|
||||
sm: "h-8 px-1.5 min-w-8",
|
||||
lg: "h-10 px-2.5 min-w-10",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Toggle({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TogglePrimitive.Root> &
|
||||
VariantProps<typeof toggleVariants>) {
|
||||
return (
|
||||
<TogglePrimitive.Root
|
||||
data-slot="toggle"
|
||||
className={cn(toggleVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Toggle, toggleVariants }
|
||||
@@ -0,0 +1,59 @@
|
||||
import * as React from "react"
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function TooltipProvider({
|
||||
delayDuration = 0,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
|
||||
return (
|
||||
<TooltipPrimitive.Provider
|
||||
data-slot="tooltip-provider"
|
||||
delayDuration={delayDuration}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function Tooltip({
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
|
||||
</TooltipProvider>
|
||||
)
|
||||
}
|
||||
|
||||
function TooltipTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
|
||||
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
|
||||
}
|
||||
|
||||
function TooltipContent({
|
||||
className,
|
||||
sideOffset = 0,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
|
||||
return (
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipPrimitive.Content
|
||||
data-slot="tooltip-content"
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-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 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<TooltipPrimitive.Arrow className={cn( "bg-primary fill-primary z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" , className) } />
|
||||
</TooltipPrimitive.Content>
|
||||
</TooltipPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* List of available font names (visit the url `/settings/appearance`).
|
||||
* This array is used to generate dynamic font classes (e.g., `font-inter`, `font-manrope`).
|
||||
*
|
||||
* 📝 How to Add a New Font (Tailwind v4+):
|
||||
* 1. Add the font name here.
|
||||
* 2. Update the `<link>` tag in 'index.html' to include the new font from Google Fonts (or any other source).
|
||||
* 3. Add the new font family to 'index.css' using the `@theme inline` and `font-family` CSS variable.
|
||||
*
|
||||
* Example:
|
||||
* fonts.ts → Add 'roboto' to this array.
|
||||
* index.html → Add Google Fonts link for Roboto.
|
||||
* index.css → Add the new font in the CSS, e.g.:
|
||||
* @theme inline {
|
||||
* // ... other font families
|
||||
* --font-roboto: 'Roboto', var(--font-sans);
|
||||
* }
|
||||
*/
|
||||
export const fonts = ['inter', 'manrope', 'system'] as const
|
||||
@@ -0,0 +1,42 @@
|
||||
|
||||
import { Outlet } from "react-router-dom";
|
||||
import { SidebarProvider } from "@/components/ui/sidebar";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { AppSidebar } from "@/components/layout/app-sidebar/app-sidebar";
|
||||
import { Header } from "@/components/header";
|
||||
|
||||
|
||||
|
||||
interface Props {
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
const Dashboard: React.FC<Props> = ({ children }) => {
|
||||
return (
|
||||
<SidebarProvider defaultOpen={false}>
|
||||
<AppSidebar />
|
||||
<div
|
||||
id='content'
|
||||
className={cn(
|
||||
'ml-auto w-full max-w-full',
|
||||
'peer-data-[state=collapsed]:w-[calc(100%-var(--sidebar-width-icon)-1rem)]',
|
||||
'peer-data-[state=expanded]:w-[calc(100%-var(--sidebar-width))]',
|
||||
'sm:transition-[width] sm:duration-200 sm:ease-linear',
|
||||
'flex h-svh flex-col',
|
||||
'group-data-[scroll-locked=1]/body:h-full',
|
||||
'has-[main.fixed-main]:group-data-[scroll-locked=1]/body:h-svh'
|
||||
)}
|
||||
>
|
||||
<Header className="bg-card border-b pl-3"/>
|
||||
{
|
||||
children ? children : <Outlet />
|
||||
}
|
||||
</div>
|
||||
|
||||
</SidebarProvider>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
export default Dashboard;
|
||||
+8
-11
@@ -1,5 +1,6 @@
|
||||
import { CardinalityStyle } from "@/lib/database";
|
||||
import { cn } from "@heroui/react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import React from "react";
|
||||
|
||||
export interface CardinalityMarkerProps {
|
||||
@@ -16,15 +17,12 @@ const CardinalityMarker: React.FC<CardinalityMarkerProps> = ({ selected = false,
|
||||
|
||||
const id = `${cardinality}_${direction}${selected ? "_selected" : ""}`;
|
||||
const renderMarker = () => {
|
||||
|
||||
if (cardinality == "many") {
|
||||
if (direction == "start")
|
||||
return (<path d="M 0 50 L 100 50 M 100 50 L 0 0 M 100 50 L 0 100 " />)
|
||||
else if (direction == "end")
|
||||
return (<path d="M 100 50 L 0 50 M 0 50 L 100 0 M 0 50 L 100 100" />)
|
||||
|
||||
}
|
||||
|
||||
if (cardinality == "one") {
|
||||
if (direction == "start") {
|
||||
return (<path d="M 0 50 L 100 50 M 50 50 M 50 50 M 75 25 L 75 75" />)
|
||||
@@ -33,8 +31,6 @@ const CardinalityMarker: React.FC<CardinalityMarkerProps> = ({ selected = false,
|
||||
return (<path d="M 100 50 L 0 50 M 50 50 M 50 50 M 25 25 L 25 75" />)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -52,7 +48,7 @@ const CardinalityMarker: React.FC<CardinalityMarkerProps> = ({ selected = false,
|
||||
>
|
||||
<svg
|
||||
fill="transparent"
|
||||
className={selected ? "stroke-primary" : "stroke-default-600 dark:stroke-default-400"}
|
||||
className={selected ? "stroke-ring dark:!stroke-primary-foreground" : "stroke-ring/60 dark:!stroke-muted-foreground/60"}
|
||||
strokeWidth="4"
|
||||
width="24"
|
||||
height="24"
|
||||
@@ -81,8 +77,9 @@ const CardinalityMarker: React.FC<CardinalityMarkerProps> = ({ selected = false,
|
||||
r="8"
|
||||
strokeWidth="1"
|
||||
className={
|
||||
cn("dark:fill-default fill-default",
|
||||
selected ? " stroke-primary fill-background dark:fill-primary-900" : " stroke-default-600 dark:stroke-default-400"
|
||||
cn(" fill-background",
|
||||
selected ? " stroke-ring fill-background dark:!stroke-primary-foreground" :
|
||||
" stroke-ring/60 dark:!stroke-muted-foreground "
|
||||
)
|
||||
}
|
||||
/>
|
||||
@@ -93,8 +90,8 @@ const CardinalityMarker: React.FC<CardinalityMarkerProps> = ({ selected = false,
|
||||
dominantBaseline="middle"
|
||||
fontSize="8"
|
||||
className={
|
||||
cn("fill-font/90 dark:fill-font/90 font-semibold" ,
|
||||
selected ? "fill-primary dark:fill-primary" : "" ,
|
||||
cn("fill-ring/60 font-semibold dark:!fill-muted-foreground" ,
|
||||
selected ? "fill-ring dark:!fill-primary-foreground" : "" ,
|
||||
)
|
||||
}
|
||||
>
|
||||
+50
-64
@@ -1,10 +1,15 @@
|
||||
import { Tooltip, TooltipTrigger, TooltipContent } from "@/components/tooltip/tooltip";
|
||||
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useDatabaseHistory } from "@/providers/database-history/database-history-provider";
|
||||
import { Button, cn, Divider, Navbar } from "@heroui/react";
|
||||
|
||||
import { useOnViewportChange, useReactFlow } from "@xyflow/react";
|
||||
|
||||
import { LayoutGrid, Redo, Scan, Undo, ZoomIn, ZoomOut } from "lucide-react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useCallback, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface DbControlButtons {
|
||||
@@ -52,39 +57,30 @@ const DatabaseControlButtons: React.FC<DbControlButtons> = ({ adjustPositions })
|
||||
})
|
||||
}, []);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<Navbar className="flex rounded-md border-1 border-default-200 bg-background dark:bg-transparent bg-default dark:border-divider " isBlurred
|
||||
classNames={{
|
||||
wrapper: "h-14 p-2 gap-1",
|
||||
}}
|
||||
<div className="flex !rounded-md p-2 border-1 !overflow-hidden !bg-background/20 dark:!bg-card/20 text-muted-foreground shadow-md backdrop-blur-sm"
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span>
|
||||
<Button
|
||||
size="md"
|
||||
isIconOnly
|
||||
variant="light"
|
||||
onPressEnd={undo}
|
||||
radius="sm"
|
||||
size={"icon"}
|
||||
onClick={undo}
|
||||
disabled={!canUndo}
|
||||
|
||||
|
||||
variant={"ghost"}
|
||||
|
||||
>
|
||||
<Undo className={cn(
|
||||
"size-4 dark:text-white",
|
||||
!canUndo ? "text-font/30 dark:text-font/30" : ""
|
||||
"size-4"
|
||||
)} />
|
||||
</Button>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{t("control_buttons.undo")}
|
||||
<span className="ml-2 text-default-400">
|
||||
<span className="ml-2">
|
||||
Cntl + Z
|
||||
</span>
|
||||
</TooltipContent>
|
||||
@@ -94,12 +90,11 @@ const DatabaseControlButtons: React.FC<DbControlButtons> = ({ adjustPositions })
|
||||
<TooltipTrigger asChild>
|
||||
<span>
|
||||
<Button
|
||||
size="md"
|
||||
isIconOnly
|
||||
variant="light"
|
||||
radius="sm"
|
||||
onPressEnd={adjustPositions}>
|
||||
<LayoutGrid className="size-4 dark:text-white" />
|
||||
size={"icon"}
|
||||
onClick={adjustPositions}
|
||||
variant={"ghost"}
|
||||
>
|
||||
<LayoutGrid className="size-4" />
|
||||
</Button>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
@@ -108,18 +103,17 @@ const DatabaseControlButtons: React.FC<DbControlButtons> = ({ adjustPositions })
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Divider orientation="vertical" className="bg-font/10" />
|
||||
<Separator orientation="vertical" />
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span>
|
||||
<Button
|
||||
size="md"
|
||||
isIconOnly
|
||||
variant="light"
|
||||
radius="sm"
|
||||
onPressEnd={onZoomOut}>
|
||||
<ZoomOut className="size-4 dark:text-white" />
|
||||
size="icon"
|
||||
onClick={onZoomOut}
|
||||
variant={"ghost"}
|
||||
>
|
||||
<ZoomOut className="size-4" />
|
||||
</Button>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
@@ -129,11 +123,10 @@ const DatabaseControlButtons: React.FC<DbControlButtons> = ({ adjustPositions })
|
||||
</Tooltip>
|
||||
|
||||
<Button
|
||||
size="md"
|
||||
isIconOnly
|
||||
variant="light"
|
||||
onPressEnd={resetZoom}
|
||||
className="w-[60px] p-2 hover:bg-primary-foreground dark:text-white"
|
||||
size="icon"
|
||||
variant={"ghost"}
|
||||
onClick={resetZoom}
|
||||
className="w-[60px] p-2 "
|
||||
>
|
||||
{zoom}
|
||||
</Button>
|
||||
@@ -141,12 +134,11 @@ const DatabaseControlButtons: React.FC<DbControlButtons> = ({ adjustPositions })
|
||||
<TooltipTrigger asChild>
|
||||
<span>
|
||||
<Button
|
||||
size="md"
|
||||
isIconOnly
|
||||
variant="light"
|
||||
radius="sm"
|
||||
onPressEnd={onZoomIn}>
|
||||
<ZoomIn className="size-4 dark:text-white" />
|
||||
size="icon"
|
||||
onClick={onZoomIn}
|
||||
variant={"ghost"}
|
||||
>
|
||||
<ZoomIn className="size-4" />
|
||||
</Button>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
@@ -155,25 +147,23 @@ const DatabaseControlButtons: React.FC<DbControlButtons> = ({ adjustPositions })
|
||||
{t("control_buttons.zoom_in")}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Divider orientation="vertical" className="bg-font/10" />
|
||||
<Separator orientation="vertical" />
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span>
|
||||
<Button
|
||||
size="md"
|
||||
isIconOnly
|
||||
variant="light"
|
||||
radius="sm"
|
||||
onPressEnd={onFitView}
|
||||
size="icon"
|
||||
onClick={onFitView}
|
||||
variant={"ghost"}
|
||||
>
|
||||
<Scan className="size-4 dark:text-white" />
|
||||
<Scan className="size-4" />
|
||||
</Button>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{t("control_buttons.show_all")}
|
||||
|
||||
<span className="ml-2 text-default-400">
|
||||
<span className="ml-2">
|
||||
Cntl + A
|
||||
</span>
|
||||
</TooltipContent>
|
||||
@@ -182,30 +172,26 @@ const DatabaseControlButtons: React.FC<DbControlButtons> = ({ adjustPositions })
|
||||
<TooltipTrigger asChild>
|
||||
<span>
|
||||
<Button
|
||||
size="md"
|
||||
isIconOnly
|
||||
variant="light"
|
||||
radius="sm"
|
||||
onPressEnd={redo}
|
||||
size="icon"
|
||||
variant={"ghost"}
|
||||
onClick={redo}
|
||||
disabled={!canRedo}
|
||||
>
|
||||
<Redo className={cn(
|
||||
"size-4 dark:text-white",
|
||||
!canRedo ? "text-font/30 dark:text-font/30" : ""
|
||||
)} />
|
||||
<Redo className={cn("size-4")} />
|
||||
</Button>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{t("control_buttons.redo")}
|
||||
<span className="ml-2 text-default-400">
|
||||
<span className="ml-2 ">
|
||||
Ctnl + Shift + Z
|
||||
</span>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</Navbar>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export default DatabaseControlButtons;
|
||||
export default DatabaseControlButtons;
|
||||
|
||||
+90
-61
@@ -1,6 +1,6 @@
|
||||
// Importing necessary types and hooks from React Flow (XYFlow)
|
||||
import {
|
||||
addEdge, Background, ColorMode, Connection, Controls, EdgeChange,
|
||||
Background, ColorMode, Connection, Controls, EdgeChange,
|
||||
EdgeRemoveChange, MiniMap, NodeChange, NodePositionChange,
|
||||
NodeRemoveChange, OnEdgesChange, OnNodesChange,
|
||||
ReactFlow, useEdgesState, useNodesState, useReactFlow
|
||||
@@ -10,51 +10,59 @@ import {
|
||||
import { useCallback, useEffect, useMemo } from "react";
|
||||
|
||||
// Custom components and styles
|
||||
import Table from "./table/table";
|
||||
import Table from "./table";
|
||||
|
||||
import '@xyflow/react/dist/style.css';
|
||||
import Relationship from "./table/relationship";
|
||||
import DBController from "./db-controller/db-controller";
|
||||
import Relationship from "./relationship";
|
||||
import { TableInsertType } from "@/lib/schemas/table-schema";
|
||||
|
||||
// Custom context providers and hooks
|
||||
import { useDatabase, useDatabaseOperations } from "@/providers/database-provider/database-provider";
|
||||
import { useTableToNode } from "@/hooks/use-table-to-node";
|
||||
import { useRelationshipToEdge } from "@/hooks/use-relationship-to-edge";
|
||||
import { Cardinality, RelationshipInsertType } from "@/lib/schemas/relationship-schema";
|
||||
import { RelationshipInsertType } from "@/lib/schemas/relationship-schema";
|
||||
|
||||
// Utils and constants
|
||||
import { v4 } from "uuid";
|
||||
import { TARGET_PREFIX } from "./table/field";
|
||||
import CardinalityMarker from "@/components/cardinality-marker/cardinality-marker";
|
||||
import { TARGET_PREFIX } from "./field";
|
||||
import CardinalityMarker from "@/features/database/components/cardinality-marker";
|
||||
import { useDiagramOps } from "@/providers/diagram-provider/diagram-provider";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/tooltip/tooltip";
|
||||
import { addToast, Button } from "@heroui/react";
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
import { adjustTablesPositions, isTablesOverlapping } from "@/utils/tables";
|
||||
import DatabaseControlButtons from "./database-control-buttons";
|
||||
|
||||
|
||||
import { AlertTriangle, Menu } from "lucide-react";
|
||||
import { adjustTablesPositions } from "@/utils/tables";
|
||||
//import DatabaseControlButtons from "./database-control-buttons";
|
||||
import { FieldType } from "@/lib/schemas/field-schema";
|
||||
import useHighlightedEdges from "@/hooks/use-highlighted-edges";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import useOverlappingTables from "@/hooks/use-overlapping-tables";
|
||||
import { useTheme } from "next-themes";
|
||||
|
||||
|
||||
import { getRelationshipSourceAndTarget } from "@/utils/relationship";
|
||||
import { useModal } from "@/providers/modal-provider/modal-provider";
|
||||
import { Modals } from "@/providers/modal-provider/modal-contxet";
|
||||
import { Loading } from "@/components/modal/loading-modal";
|
||||
import { usePowerSync } from "@powersync/react";
|
||||
import { Loading } from "@/components/layout/loading-modal";
|
||||
|
||||
import { CardinalityStyle } from "@/lib/database";
|
||||
import { useTheme } from "@/providers/theme-provider/theme-provider";
|
||||
import { toast } from "sonner";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import DatabaseControlButtons from "./database-control-buttons";
|
||||
|
||||
import { Modals } from "@/providers/modal-provider/modal-contxet";
|
||||
import { useModal } from "@/providers/modal-provider/modal-provider";
|
||||
import { useIsMobile } from "@/hooks/use-mobile";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
|
||||
|
||||
const DatabasePage: React.FC = () => {
|
||||
const DatabaseDiagram: React.FC = () => {
|
||||
const { open } = useModal();
|
||||
|
||||
|
||||
|
||||
const { t } = useTranslation();
|
||||
const { resolvedTheme } = useTheme();
|
||||
const { theme: resolvedTheme } = useTheme();
|
||||
// Extract database state and operations
|
||||
const { database, getField, databases, isSwitchingDatabase, isLoading, currentDatabaseId } = useDatabase();
|
||||
const { database, getField, isSwitchingDatabase, isLoading, databases } = useDatabase();
|
||||
|
||||
const { updateTablePositions, deleteMultiTables, deleteMultiRelationships, createRelationship } = useDatabaseOperations();
|
||||
|
||||
@@ -63,7 +71,7 @@ const DatabasePage: React.FC = () => {
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
||||
|
||||
// Diagram-related state (e.g. connection in progress)
|
||||
const { setIsConnectionInProgress, cardinalityStyle } = useDiagramOps();
|
||||
const { setIsConnectionInProgress, cardinalityStyle, showController, openController } = useDiagramOps();
|
||||
|
||||
// Destructure tables and relationships from database
|
||||
const { tables, relationships } = database || { tables: [], relationships: [] };
|
||||
@@ -75,17 +83,18 @@ const DatabasePage: React.FC = () => {
|
||||
const nodeTypes = useMemo(() => ({ table: Table }), []);
|
||||
const edgeTypes = useMemo(() => ({ 'relationship-edge': Relationship }), []);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoading)
|
||||
return;
|
||||
// no database selected , open (open database) Modal
|
||||
if (!database && databases?.length > 0) {
|
||||
|
||||
|
||||
open(Modals.OPEN_DATABASE, {
|
||||
closable: false
|
||||
})
|
||||
}
|
||||
else if (databases.length == 0) {
|
||||
else if (databases.length == 0) {
|
||||
// there is no databases in the first place to select from ,
|
||||
// we have to create a new one .
|
||||
open(Modals.CREATE_DATABASE, {
|
||||
@@ -99,9 +108,17 @@ const DatabasePage: React.FC = () => {
|
||||
}, [database?.id, isLoading])
|
||||
|
||||
useEffect(() => {
|
||||
setNodes([]);
|
||||
setEdges([]);
|
||||
}, [currentDatabaseId])
|
||||
if (isSwitchingDatabase) {
|
||||
setNodes([]);
|
||||
setEdges([]);
|
||||
} else {
|
||||
fitView({
|
||||
duration: 500
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
}, [isSwitchingDatabase, database?.id])
|
||||
|
||||
// Called when a connection is made between fields
|
||||
const onConnect = useCallback(async (connection: Connection) => {
|
||||
@@ -122,16 +139,19 @@ const DatabasePage: React.FC = () => {
|
||||
targetTableId,
|
||||
sourceFieldId,
|
||||
targetFieldId,
|
||||
|
||||
} as RelationshipInsertType);
|
||||
|
||||
} else {
|
||||
// Show error toast if invalid relationship
|
||||
addToast({
|
||||
title: t("db_controller.invalid_relationship.title"),
|
||||
|
||||
|
||||
toast(t("db_controller.invalid_relationship.title"), {
|
||||
description: t("db_controller.invalid_relationship.description"),
|
||||
color: "danger",
|
||||
variant: "solid"
|
||||
});
|
||||
classNames: {
|
||||
description: "!text-destructive",
|
||||
title: "!text-destructive"
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
setIsConnectionInProgress(false);
|
||||
@@ -147,28 +167,30 @@ const DatabasePage: React.FC = () => {
|
||||
const nodeRemoveChanges: NodeRemoveChange[] = changes.filter((change: NodeChange) => change.type == "remove");
|
||||
// Save new positions to the database
|
||||
if (nodePositionChanges.length > 0)
|
||||
|
||||
updateTablePositions(nodePositionChanges.map((change: NodePositionChange) => ({
|
||||
id: change.id,
|
||||
posX: change.position?.x,
|
||||
posY: change.position?.y
|
||||
} as TableInsertType)));
|
||||
|
||||
|
||||
// Delete tables if removed
|
||||
if (nodeRemoveChanges.length > 0) {
|
||||
if (nodeRemoveChanges.length > 0 && !isSwitchingDatabase) {
|
||||
deleteMultiTables(nodeRemoveChanges.map((change: NodeRemoveChange) => change.id));
|
||||
}
|
||||
return onNodesChange(changes);
|
||||
}, [onNodesChange, database]);
|
||||
}, [onNodesChange, database, isSwitchingDatabase]);
|
||||
|
||||
// Called when edges (relationships) change
|
||||
const handleEdgeChanges: OnEdgesChange<any> = useCallback((changes: EdgeChange<any>[]) => {
|
||||
const edgeRemoveChanges: EdgeRemoveChange[] = changes.filter((change: EdgeChange) => change.type == "remove") as EdgeRemoveChange[];
|
||||
// Delete relationships from database
|
||||
if (edgeRemoveChanges.length > 0) {
|
||||
if (edgeRemoveChanges.length > 0 && !isSwitchingDatabase) {
|
||||
deleteMultiRelationships(edgeRemoveChanges.map((change: EdgeRemoveChange) => change.id));
|
||||
}
|
||||
return onEdgesChange(changes as EdgeChange<never>[]);
|
||||
}, [onEdgesChange]);
|
||||
}, [onEdgesChange, isSwitchingDatabase]);
|
||||
|
||||
// When user starts connecting fields
|
||||
const onConnectStart = useCallback(() => {
|
||||
@@ -199,18 +221,15 @@ const DatabasePage: React.FC = () => {
|
||||
useHighlightedEdges(nodes, relationships, edges);
|
||||
const { isOverlapping, puls } = useOverlappingTables(tables);
|
||||
|
||||
|
||||
const isMobile = useIsMobile();
|
||||
return (
|
||||
|
||||
<div className="w-full h-screen flex relative overflow-hidden">
|
||||
<div className="w-full h-full flex relative overflow-hidden">
|
||||
{
|
||||
(isSwitchingDatabase || isLoading) && <Loading />
|
||||
}
|
||||
<div className="flex max-w-full">
|
||||
<DBController />
|
||||
</div>
|
||||
{
|
||||
|
||||
{
|
||||
<div className="relative w-full h-full ">
|
||||
<ReactFlow
|
||||
colorMode={resolvedTheme as ColorMode}
|
||||
@@ -236,33 +255,35 @@ const DatabasePage: React.FC = () => {
|
||||
|
||||
>
|
||||
<Controls
|
||||
position="bottom-center"
|
||||
position={isMobile ? "top-center" : "bottom-center"}
|
||||
showFitView={false}
|
||||
showZoom={false}
|
||||
showInteractive={false}
|
||||
className="shadow-none">
|
||||
|
||||
className="!shadow-none !border-none">
|
||||
<DatabaseControlButtons
|
||||
adjustPositions={adjustPositions}
|
||||
/>
|
||||
</Controls >
|
||||
<MiniMap
|
||||
nodeStrokeWidth={4}
|
||||
className="dark:bg-content1 bg-background border-1 border-default-200 dark:border-divider rounded-md "
|
||||
maskStrokeColor={resolvedTheme == "dark" ? "#272c35" : "#e9edf1"}
|
||||
maskColor={resolvedTheme == "dark" ? "#20252c99" : "#f2f4f733"}
|
||||
className="!bg-background border-1 rounded-lg overflow-hidden "
|
||||
maskStrokeColor={resolvedTheme == "dark" ? "#FFFFFF1A" : "#e2e8f0"}
|
||||
maskColor={resolvedTheme == "dark" ? "#21262d77" : "#62748e05"}
|
||||
maskStrokeWidth={1}
|
||||
nodeClassName={"fill-default-300 dark:fill-font/30"}
|
||||
nodeClassName={"!fill-muted-foreground/20 "}
|
||||
style={{
|
||||
width: 164,
|
||||
height: 128
|
||||
}}
|
||||
|
||||
/>
|
||||
<Background className="dark:bg-background-100 " />
|
||||
<Background color="#62748e" className="dark:!bg-background " />
|
||||
|
||||
</ReactFlow>
|
||||
<div
|
||||
className="absolute left-[24px] bottom-[24px] "
|
||||
className={cn("absolute left-[24px] ", {
|
||||
"bottom-[24px]": !isMobile,
|
||||
"top-[24px]": isMobile
|
||||
})}
|
||||
>
|
||||
{
|
||||
isOverlapping &&
|
||||
@@ -270,22 +291,30 @@ const DatabasePage: React.FC = () => {
|
||||
<TooltipTrigger asChild>
|
||||
<span>
|
||||
<Button
|
||||
variant="shadow"
|
||||
size="sm"
|
||||
isIconOnly
|
||||
color="danger"
|
||||
className="size-8 p-1 "
|
||||
onPressEnd={puls}>
|
||||
variant="destructive"
|
||||
size="icon"
|
||||
className="shadow-lg size-8"
|
||||
onClick={puls}>
|
||||
<AlertTriangle className="size-4 text-white" />
|
||||
</Button>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<TooltipContent className="bg-destructive fill-destructive ">
|
||||
{t("table.overlapping_tables")}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
}
|
||||
</div>
|
||||
{
|
||||
isMobile &&
|
||||
<Button size={"icon"} className="absolute bottom-[24px] left-[24px]"
|
||||
onClick={() => {
|
||||
openController(!showController)
|
||||
}}
|
||||
>
|
||||
<Menu className="size-5" />
|
||||
</Button>
|
||||
}
|
||||
</div>
|
||||
|
||||
}
|
||||
@@ -314,7 +343,7 @@ const DatabasePage: React.FC = () => {
|
||||
}
|
||||
|
||||
|
||||
export default DatabasePage;
|
||||
export default DatabaseDiagram;
|
||||
|
||||
|
||||
|
||||
+23
-25
@@ -1,11 +1,12 @@
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/tooltip/tooltip";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { RelationshipType } from "@/lib/schemas/relationship-schema";
|
||||
import { useDatabase, useDatabaseOperations } from "@/providers/database-provider/database-provider";
|
||||
import { useDiagramOps } from "@/providers/diagram-provider/diagram-provider";
|
||||
import { CircularDependencyError } from "@/utils/render/render-uttils";
|
||||
import { addToast, Alert, Button, Listbox, ListboxItem, toast } from "@heroui/react";
|
||||
import { CircularDependencyError } from "@/utils/render/render-uttils";
|
||||
import { AlertTriangle, Trash } from "lucide-react";
|
||||
import React, { useCallback, useEffect, useMemo } from "react";
|
||||
import React, { useCallback, useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface CircularDependencyAlertProps {
|
||||
@@ -53,41 +54,38 @@ const CircularDependencyAlert: React.FC<CircularDependencyAlertProps> = ({ error
|
||||
return (
|
||||
<div className="flex flex-col gap-2 w-full h-full items-center pt-12 ">
|
||||
<AlertTriangle
|
||||
className="size-12 text-danger"
|
||||
className="size-12 text-destructive"
|
||||
/>
|
||||
<h3 className="text-danger font-semibold">
|
||||
<h3 className="text-destructive font-semibold">
|
||||
{t("db_controller.circular_dependency.title")}
|
||||
</h3>
|
||||
<p className="text-sm text-font/70 text-center w-[80%] max-w-[360px]">
|
||||
{t("db_controller.circular_dependency.description")} , <span className="font-semibold text-font/90"> {t("db_controller.circular_dependency.suggestion")} </span>
|
||||
<p className="text-sm text-muted-foreground text-center w-[80%] max-w-[360px]">
|
||||
{t("db_controller.circular_dependency.description")} , <span className="font-medium text-foreground"> {t("db_controller.circular_dependency.suggestion")} </span>
|
||||
</p>
|
||||
|
||||
<Listbox aria-label="Relationships" className="w-[80%] max-w-[360px]" onAction={focus}
|
||||
|
||||
|
||||
|
||||
<ul aria-label="Relationships" className="w-[80%] max-w-[360px]"
|
||||
>
|
||||
{
|
||||
circularRelationships.map((relationship: RelationshipType) => (
|
||||
<ListboxItem
|
||||
variant={"faded"}
|
||||
className="data-[hover=true]:bg-default-500"
|
||||
<li
|
||||
onClick={() => focus(relationship.id)}
|
||||
key={relationship.id}>
|
||||
<div className="flex items-center justify-between tex-font/90">
|
||||
<span>
|
||||
<div className="hover:bg-secondary flex items-center justify-between h-10 mb-2 px-3 rounded-md cursor-pointer">
|
||||
<span className="text-sm">
|
||||
{relationship.sourceTable.name} -> {relationship.targetTable.name}
|
||||
</span>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span>
|
||||
<Button
|
||||
isIconOnly
|
||||
variant="light"
|
||||
color={"danger"}
|
||||
size="sm"
|
||||
onPress={() => removeRelationship(relationship.id)}
|
||||
|
||||
|
||||
variant={"outline"}
|
||||
size="icon"
|
||||
onClick={() => removeRelationship(relationship.id)}
|
||||
className="shadow-sm size-7"
|
||||
>
|
||||
<Trash className="text-danger size-4" />
|
||||
<Trash className="text-destructive size-4" />
|
||||
</Button>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
@@ -96,10 +94,10 @@ const CircularDependencyAlert: React.FC<CircularDependencyAlertProps> = ({ error
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</ListboxItem>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</Listbox>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
)
|
||||
@@ -0,0 +1,148 @@
|
||||
import { Ref, useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { RelationshipType } from "@/lib/schemas/relationship-schema";
|
||||
import { useDatabase } from "@/providers/database-provider/database-provider";
|
||||
import { useDiagram } from "@/providers/diagram-provider/diagram-provider";
|
||||
import { useModal } from "@/providers/modal-provider/modal-provider";
|
||||
import { Modals } from "@/providers/modal-provider/modal-contxet";
|
||||
import { getDefaultRelationshipName } from "@/utils/relationship";
|
||||
import EmptyList from "@/components/empty-list";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { IconFilter2Up, IconPlus } from "@tabler/icons-react";
|
||||
import { Accordion } from "@/components/ui/accordion";
|
||||
import RelationshipAccordionItem from "./relationship-accordion-item/relationship-accordion-item";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
|
||||
const RelationshipController: React.FC = ({ }) => {
|
||||
|
||||
|
||||
const { open } = useModal();
|
||||
const { database } = useDatabase();
|
||||
const { relationships: allRelationships } = database || { relationships: [] };
|
||||
const [relationships, setRelationships] = useState<RelationshipType[]>(allRelationships);
|
||||
const nameRef: Ref<HTMLInputElement> = useRef<HTMLInputElement>(null);
|
||||
const { t } = useTranslation();
|
||||
const { focusedRelationshipId , setFocusedRelationshipId } = useDiagram();
|
||||
|
||||
useEffect(() => searchRelationships(), [allRelationships]);
|
||||
|
||||
useEffect(() => {
|
||||
if (focusedRelationshipId) {
|
||||
const accordionItem = document.getElementById(focusedRelationshipId)
|
||||
if (accordionItem)
|
||||
accordionItem?.scrollIntoView({
|
||||
behavior: 'smooth', block: "nearest"
|
||||
})
|
||||
}
|
||||
}, [focusedRelationshipId]);
|
||||
|
||||
const onOpen = useCallback(() => {
|
||||
open(Modals.CREATE_RELATIONSHIP, {
|
||||
onRlationshipCreated: (id: string) => setFocusedRelationshipId(id)
|
||||
})
|
||||
}, []);
|
||||
|
||||
const searchRelationships = useCallback(() => {
|
||||
const keyword: string | undefined = nameRef.current?.value;
|
||||
if (keyword !== undefined) {
|
||||
|
||||
setRelationships(() => {
|
||||
return allRelationships.filter((relationship: RelationshipType) => {
|
||||
if (relationship.name)
|
||||
return relationship.name.toLowerCase().trim().includes(keyword.toLocaleLowerCase().trim());
|
||||
else
|
||||
return getDefaultRelationshipName(relationship).trim().toLocaleLowerCase().includes(
|
||||
keyword.trim().toLocaleLowerCase()
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
}, [nameRef, allRelationships]);
|
||||
|
||||
const collapseAll = useCallback(() => {
|
||||
setFocusedRelationshipId(undefined);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="w-full h-full flex flex-col min-h-0">
|
||||
<div className="flex items-center gap-2 p-3">
|
||||
<Tooltip >
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant={"ghost"}
|
||||
size={"icon"}
|
||||
className="rounded-md h-8 w-8"
|
||||
onClick={collapseAll}
|
||||
>
|
||||
<IconFilter2Up className="size-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{t("db_controller.collapse")}
|
||||
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Input
|
||||
className="h-8 bg-secondary dark:bg-background"
|
||||
placeholder={t("db_controller.filter")}
|
||||
ref={nameRef}
|
||||
type="text"
|
||||
onKeyUp={searchRelationships}
|
||||
/>
|
||||
<Tooltip >
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant={"default"}
|
||||
size={"icon"}
|
||||
className="rounded-md h-8 w-8"
|
||||
onClick={onOpen}
|
||||
>
|
||||
<IconPlus className="size-4 " />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{t("db_controller.add_relationship")}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
</div>
|
||||
|
||||
{
|
||||
allRelationships.length > 0 &&
|
||||
<ScrollArea className="flex-1 px-3 overflow-hidden">
|
||||
<Accordion
|
||||
type="single"
|
||||
collapsible
|
||||
className="w-full "
|
||||
value={focusedRelationshipId}
|
||||
onValueChange={setFocusedRelationshipId}
|
||||
>
|
||||
{relationships.map((relationship: RelationshipType) => (
|
||||
<>
|
||||
<RelationshipAccordionItem relationship={relationship} key={relationship.id} />
|
||||
<Separator className="my-1" />
|
||||
</>
|
||||
))}
|
||||
</Accordion>
|
||||
</ScrollArea>
|
||||
}
|
||||
|
||||
{
|
||||
(allRelationships.length == 0) &&
|
||||
<div className="px-3 h-full">
|
||||
<EmptyList
|
||||
title={t("db_controller.empty_list.no_relationships")}
|
||||
description={t("db_controller.empty_list.no_relationships_description")}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default RelationshipController
|
||||
|
||||
|
||||
+211
@@ -0,0 +1,211 @@
|
||||
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { ForeignKeyActions } from "@/lib/field";
|
||||
import { Cardinality, RelationshipInsertType, RelationshipType } from "@/lib/schemas/relationship-schema";
|
||||
import { useDatabaseOperations } from "@/providers/database-provider/database-provider";
|
||||
|
||||
import { Trash2 } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
|
||||
interface RelationshipAccordionContentProps {
|
||||
relationship: RelationshipType
|
||||
}
|
||||
|
||||
const RelationshipAccordionContent: React.FC<RelationshipAccordionContentProps> = ({ relationship }) => {
|
||||
|
||||
const [cardinality, setCardinality] = useState<string>(relationship.cardinality);
|
||||
const [onDeleteAction, setOnDeleteAction] = useState<string>(relationship.onDelete || ForeignKeyActions.NO_ACTION);
|
||||
const [onUpdateAction, setOnUpdateAction] = useState<string>(relationship.onUpdate || ForeignKeyActions.NO_ACTION);
|
||||
|
||||
const { editRelationship, deleteRelationship } = useDatabaseOperations();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const changeCardinality = (selection: string) => {
|
||||
setCardinality(selection);
|
||||
editRelationship({
|
||||
id: relationship.id,
|
||||
cardinality: selection as Cardinality,
|
||||
} as RelationshipInsertType);
|
||||
}
|
||||
|
||||
const changeOnDelete = (selection: string) => {
|
||||
|
||||
editRelationship({
|
||||
id: relationship.id,
|
||||
onDelete: selection as ForeignKeyActions,
|
||||
} as RelationshipInsertType);
|
||||
setOnDeleteAction(selection);
|
||||
}
|
||||
|
||||
const changeOnUpdate = (selection: string) => {
|
||||
|
||||
editRelationship({
|
||||
id: relationship.id,
|
||||
onUpdate: selection as ForeignKeyActions,
|
||||
} as RelationshipInsertType);
|
||||
setOnUpdateAction(selection);
|
||||
}
|
||||
|
||||
const removeRelationship = () => {
|
||||
deleteRelationship(relationship.id);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setCardinality(relationship.cardinality);
|
||||
}, [relationship.cardinality])
|
||||
|
||||
useEffect(() => {
|
||||
if (!relationship.onDelete)
|
||||
setOnDeleteAction(ForeignKeyActions.NO_ACTION)
|
||||
else
|
||||
setOnDeleteAction(relationship.onDelete as ForeignKeyActions)
|
||||
}, [relationship.onDelete]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!relationship.onUpdate)
|
||||
setOnUpdateAction(ForeignKeyActions.NO_ACTION)
|
||||
else
|
||||
setOnUpdateAction(relationship.onUpdate as ForeignKeyActions)
|
||||
}, [relationship.onUpdate]);
|
||||
|
||||
if (!relationship.sourceTable || !relationship.targetTable)
|
||||
return;
|
||||
|
||||
return (
|
||||
<div className="w-full p-2 space-y-4">
|
||||
<div className="flex">
|
||||
<div className="w-full space-y-1">
|
||||
<Label >
|
||||
{t("db_controller.source_table")}
|
||||
</Label>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="truncate text-sm text-muted-foreground">
|
||||
{relationship.sourceTable?.name}({relationship.sourceField?.name})
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{relationship.sourceTable?.name}({relationship.sourceField?.name})
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div className="w-full space-y-1">
|
||||
<Label >
|
||||
{t("db_controller.referenced_table")}
|
||||
</Label>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="truncate text-sm text-muted-foreground">
|
||||
{relationship.targetTable?.name}({relationship.targetField?.name})
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{relationship.targetTable?.name}({relationship.targetField?.name})
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="cardinality">
|
||||
{t('db_controller.cardinality.name')}
|
||||
</Label>
|
||||
<Select
|
||||
aria-label="cardinality"
|
||||
value={cardinality as any}
|
||||
onValueChange={changeCardinality as any}
|
||||
>
|
||||
<SelectTrigger id="cardinality" className="w-full flex ">
|
||||
<SelectValue placeholder={t('db_controller.cardinality.name')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={Cardinality.one_to_one}>{t("db_controller.cardinality.one_to_one")}</SelectItem>
|
||||
<SelectItem value={Cardinality.one_to_many}>{t("db_controller.cardinality.one_to_many")}</SelectItem>
|
||||
<SelectItem value={Cardinality.many_to_one}>{t("db_controller.cardinality.many_to_one")}</SelectItem>
|
||||
<SelectItem value={Cardinality.many_to_many}>{t("db_controller.cardinality.many_to_many")}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
{
|
||||
cardinality != Cardinality.many_to_many &&
|
||||
<div className="space-y-4">
|
||||
<Label>
|
||||
{t('db_controller.foreign_key_actions.title')}
|
||||
</Label>
|
||||
<div className="flex gap-2">
|
||||
<div className="w-full space-y-2">
|
||||
<Label >
|
||||
{t('db_controller.foreign_key_actions.on_delete')}
|
||||
</Label>
|
||||
<Select
|
||||
aria-label="On delete actions"
|
||||
value={onDeleteAction as any}
|
||||
onValueChange={changeOnDelete as any}
|
||||
>
|
||||
<SelectTrigger className="w-full flex ">
|
||||
<SelectValue placeholder={t('db_controller.foreign_key_actions.on_delete')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{
|
||||
Object.values(ForeignKeyActions).map((value: string) => (
|
||||
<SelectItem
|
||||
key={value}
|
||||
value={value}>{t(`db_controller.foreign_key_actions.actions.${value}`)}</SelectItem>
|
||||
))
|
||||
}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="w-full space-y-2">
|
||||
<div className="w-full space-y-2">
|
||||
<Label >
|
||||
{t('db_controller.foreign_key_actions.on_update')}
|
||||
</Label>
|
||||
<Select
|
||||
aria-label="On update actions"
|
||||
value={onUpdateAction}
|
||||
onValueChange={changeOnUpdate}
|
||||
>
|
||||
<SelectTrigger className="w-full flex ">
|
||||
<SelectValue placeholder={t('db_controller.foreign_key_actions.on_update')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{
|
||||
Object.values(ForeignKeyActions).map((value: string) => (
|
||||
<SelectItem
|
||||
key={value}
|
||||
value={value}>{t(`db_controller.foreign_key_actions.actions.${value}`)}</SelectItem>
|
||||
))
|
||||
}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div className="flex justify-center">
|
||||
<Button
|
||||
variant="destructive"
|
||||
className="w-full"
|
||||
onClick={removeRelationship}
|
||||
>
|
||||
<Trash2 className="mr-1 size-3.5 text-danger" />
|
||||
{t("db_controller.delete")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export default RelationshipAccordionContent;
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
|
||||
import {
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
} from "@/components/ui/accordion"
|
||||
import { RelationshipType } from "@/lib/schemas/relationship-schema";
|
||||
import RelationshipAccordionTrigger from "./relationship-accordion-trigger";
|
||||
import RelationshipAccordionContent from "./relationship-accordion-content";
|
||||
|
||||
interface RelationshipAccordionItemProps {
|
||||
relationship: RelationshipType
|
||||
}
|
||||
|
||||
|
||||
const RelationshipAccordionItem: React.FC<RelationshipAccordionItemProps> = ({ relationship }) => {
|
||||
|
||||
return (
|
||||
<AccordionItem value={relationship.id} className="border-none" id={relationship.id}>
|
||||
<RelationshipAccordionTrigger relationship={relationship} />
|
||||
<AccordionContent className="flex flex-col gap-4 text-balance">
|
||||
<RelationshipAccordionContent
|
||||
relationship={relationship}
|
||||
/>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export default RelationshipAccordionItem;
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
|
||||
|
||||
import { RelationshipInsertType, RelationshipType } from "@/lib/schemas/relationship-schema";
|
||||
import { useDatabaseOperations } from "@/providers/database-provider/database-provider";
|
||||
import { useDiagramOps } from "@/providers/diagram-provider/diagram-provider";
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import hash from "object-hash";
|
||||
import { getDefaultRelationshipName } from "@/utils/relationship";
|
||||
import { AccordionTrigger } from "@/components/ui/accordion";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { IconCheck, IconDotsVertical, IconFocus2, IconPencil, IconTrash } from "@tabler/icons-react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
|
||||
|
||||
|
||||
interface RelationshipAccordionTriggerProps {
|
||||
isOpen?: boolean;
|
||||
relationship: RelationshipType
|
||||
}
|
||||
|
||||
const RelationshipAccordionTrigger: React.FC<RelationshipAccordionTriggerProps> = ({ isOpen, relationship }) => {
|
||||
const defaultName: string = useMemo(() => {
|
||||
return getDefaultRelationshipName(relationship);
|
||||
}, [relationship])
|
||||
|
||||
|
||||
const [editMode, setEditMode] = useState<boolean>(false);
|
||||
|
||||
const { editRelationship, deleteRelationship } = useDatabaseOperations();
|
||||
const { t } = useTranslation();
|
||||
|
||||
|
||||
const [name, setName] = useState<string>(relationship.name ? relationship.name : defaultName);
|
||||
const { focusOnRelationship } = useDiagramOps();
|
||||
|
||||
|
||||
const editRelationshipName = () => {
|
||||
if (relationship.name || name.trim().toLocaleLowerCase() != defaultName)
|
||||
editRelationship({
|
||||
id: relationship.id,
|
||||
name
|
||||
} as RelationshipInsertType);
|
||||
setEditMode(false);
|
||||
}
|
||||
|
||||
const onDeleteRelationship = () => {
|
||||
deleteRelationship(relationship.id);
|
||||
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setName(relationship.name ? relationship.name : defaultName);
|
||||
}, [relationship.name]);
|
||||
|
||||
return (
|
||||
<AccordionTrigger className="group h-12 py-3">
|
||||
{
|
||||
!editMode &&
|
||||
<>
|
||||
<div className="w-full flex">
|
||||
<label
|
||||
className="py-2 !truncate text-sm cursor-pointer max-w-80"
|
||||
>
|
||||
{relationship.name ? relationship.name : defaultName}
|
||||
</label>
|
||||
</div>
|
||||
<div className="hidden shrink-0 flex-row group-hover:flex gap-2">
|
||||
<Button variant="outline" size="icon" className="size-8 shrink-0 " onClick={(event: any) => {
|
||||
event.stopPropagation();
|
||||
setEditMode(true)
|
||||
}}>
|
||||
<IconPencil className="size-4 text-muted-foreground " />
|
||||
</Button>
|
||||
<Button variant="outline" size="icon" className="size-8 shrink-0 " onClick={(event: any) => {
|
||||
event.stopPropagation();
|
||||
focusOnRelationship(relationship.id, true)
|
||||
}}>
|
||||
<IconFocus2 className="size-4 text-muted-foreground " />
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
{
|
||||
editMode && <>
|
||||
<Input
|
||||
placeholder={"Relationship name"}
|
||||
value={name}
|
||||
onChange={(event: any) => setName(event.target.value)}
|
||||
onBlur={editRelationshipName}
|
||||
autoFocus
|
||||
type="text"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
className="h-8"
|
||||
onKeyDown={(e: any) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
editRelationshipName();
|
||||
e.target.blur();
|
||||
}
|
||||
}}
|
||||
|
||||
/>
|
||||
<Button variant="ghost" size="icon" className="size-8 shrink-0 rounded-sm" onClick={editRelationshipName}>
|
||||
<IconCheck className="size-4 text-muted-foreground " />
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="size-8 shrink-0 rounded-sm" >
|
||||
<IconDotsVertical className="size-4 text-muted-foreground " />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="min-w-[164px]">
|
||||
<DropdownMenuLabel>
|
||||
{t("db_controller.actions")}
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<DropdownMenuItem variant="destructive" onClick={onDeleteRelationship}>
|
||||
{t("db_controller.delete")}
|
||||
<DropdownMenuShortcut>
|
||||
<IconTrash className="size-4 text-destructive" />
|
||||
</DropdownMenuShortcut>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
</AccordionTrigger>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export default React.memo(RelationshipAccordionTrigger, (previousState, newState) => {
|
||||
return hash(previousState) == hash(newState);
|
||||
});
|
||||
+19
-18
@@ -1,21 +1,21 @@
|
||||
import { useRenderSql } from "@/hooks/user-render-sql";
|
||||
import { useDatabase, useDatabaseOperations } from "@/providers/database-provider/database-provider";
|
||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useDatabase } from "@/providers/database-provider/database-provider";
|
||||
import React, { useEffect, useMemo } from "react";
|
||||
|
||||
import CodeMirror, { EditorView } from '@uiw/react-codemirror';
|
||||
import CodeMirror from '@uiw/react-codemirror';
|
||||
import { sql } from '@codemirror/lang-sql';
|
||||
import { oneDark } from '@codemirror/theme-one-dark';
|
||||
import { useTheme } from "next-themes";
|
||||
|
||||
import { overrideDarkTheme, overrideLightTheme } from "@/lib/colors";
|
||||
import { DatabaseType } from "@/lib/schemas/database-schema";
|
||||
import CircularDependencyAlert from "./circular-dependecy-alert";
|
||||
import { addToast } from "@heroui/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Clipboard from "@/components/clipboard/clipboard";
|
||||
import Clipboard from "@/components/clipboard";
|
||||
import { TableType } from "@/lib/schemas/table-schema";
|
||||
import { RelationshipType } from "@/lib/schemas/relationship-schema";
|
||||
import { Parser } from "node-sql-parser";
|
||||
|
||||
import { useTheme } from "@/providers/theme-provider/theme-provider";
|
||||
import { toast } from "sonner";
|
||||
|
||||
|
||||
interface SqlPreviewProps {
|
||||
@@ -23,14 +23,14 @@ interface SqlPreviewProps {
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const SqlPreview: React.FC<SqlPreviewProps> = ({ tableFilterIds }) => {
|
||||
|
||||
const { database: currentDatabase } = useDatabase();
|
||||
const database = useMemo(() => {
|
||||
if ( !tableFilterIds )
|
||||
return currentDatabase ;
|
||||
if (!tableFilterIds)
|
||||
return currentDatabase;
|
||||
return {
|
||||
...currentDatabase,
|
||||
tables: currentDatabase?.tables.filter((table: TableType) => tableFilterIds?.includes(table.id)),
|
||||
@@ -41,16 +41,17 @@ const SqlPreview: React.FC<SqlPreviewProps> = ({ tableFilterIds }) => {
|
||||
}, [currentDatabase, tableFilterIds])
|
||||
|
||||
const { sql: sqlCode, circularDependency } = useRenderSql(database as DatabaseType);
|
||||
const { resolvedTheme } = useTheme();
|
||||
const { theme } = useTheme();
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
if (circularDependency)
|
||||
addToast({
|
||||
title: t("db_controller.circular_dependency.title"),
|
||||
toast(t("db_controller.circular_dependency.title"), {
|
||||
description: t("db_controller.circular_dependency.description"),
|
||||
color: "danger",
|
||||
variant: "solid"
|
||||
classNames: {
|
||||
description: "!text-destructive",
|
||||
title: "!text-destructive"
|
||||
},
|
||||
});
|
||||
}, [circularDependency]);
|
||||
|
||||
@@ -60,19 +61,19 @@ const SqlPreview: React.FC<SqlPreviewProps> = ({ tableFilterIds }) => {
|
||||
else
|
||||
return (
|
||||
<div className="flex w-full h-full relative ">
|
||||
<div className="absolute right-[12px] top-[4px] z-[1] bg-background ">
|
||||
<div className="absolute right-[12px] top-[4px] z-[1] ">
|
||||
<Clipboard
|
||||
text={sqlCode}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
<CodeMirror
|
||||
defaultValue={sqlCode}
|
||||
value={sqlCode}
|
||||
className="flex flex-1 w-full "
|
||||
extensions={[sql()]}
|
||||
readOnly
|
||||
theme={resolvedTheme == "light" ? overrideLightTheme : [oneDark, overrideDarkTheme]}
|
||||
theme={theme != "dark" ? overrideLightTheme : [oneDark, overrideDarkTheme]}
|
||||
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
|
||||
|
||||
import { Code } from "lucide-react"
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Ref, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useDatabase, useDatabaseOperations } from "@/providers/database-provider/database-provider";
|
||||
import { TableInsertType, TableType } from "@/lib/schemas/table-schema";
|
||||
import { v4 } from "uuid";
|
||||
import { useDiagram } from "@/providers/diagram-provider/diagram-provider";
|
||||
import { useReactFlow } from "@xyflow/react";
|
||||
import SqlPreview from "../sql-preview";
|
||||
import EmptyList from "@/components/empty-list";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { IconListDetails, IconPlus } from "@tabler/icons-react";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
|
||||
|
||||
const PADDING_X = 40;
|
||||
const PADDING_Y = 80;
|
||||
|
||||
import {
|
||||
Accordion,
|
||||
} from "@/components/ui/accordion"
|
||||
import TableAccordionItem from "./table-accordion-item/table-accordion-item";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { closestCenter, DndContext, PointerSensor, useSensor, useSensors } from "@dnd-kit/core";
|
||||
import { arrayMove, SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
|
||||
import { getTableNextSequence } from "@/utils/tables";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
|
||||
|
||||
const TablesController: React.FC = ({ }) => {
|
||||
|
||||
const { database } = useDatabase();
|
||||
const { createTable, getInteger, orderTables } = useDatabaseOperations();
|
||||
const { getViewport } = useReactFlow();
|
||||
const { tables: allTables } = database || { tables: [] };
|
||||
const [tables, setTables] = useState<TableType[]>(allTables);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [showSqlPreview, setShowSqlPreview] = useState<boolean>(false);
|
||||
const { focusedTableId , setFocusedTableId} = useDiagram();
|
||||
const nameRef: Ref<HTMLInputElement> = useRef<HTMLInputElement>(null);
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor)
|
||||
);
|
||||
|
||||
|
||||
function handleDragEnd(event: any) {
|
||||
const { active, over } = event;
|
||||
|
||||
if (active.id !== over?.id) {
|
||||
|
||||
setTables((items) => {
|
||||
|
||||
const oldIndex = items.findIndex((item: TableType) => item.id == active.id);
|
||||
const newIndex = items.findIndex((item: TableType) => item.id == over.id);
|
||||
|
||||
const tables = arrayMove(items, oldIndex, newIndex);
|
||||
|
||||
orderTables(tables)
|
||||
|
||||
return tables;
|
||||
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
useEffect(() => searchTables(), [allTables]);
|
||||
|
||||
const addNewTable = useCallback(async () => {
|
||||
|
||||
const newTableId: string = v4();
|
||||
const viewport = getViewport();
|
||||
const { x, y, zoom } = viewport;
|
||||
|
||||
const posX = -x / zoom + (PADDING_X / zoom);
|
||||
const posY = -y / zoom + (PADDING_Y / zoom);
|
||||
|
||||
await createTable({
|
||||
id: newTableId,
|
||||
name: `table_${tables.length + 1}`,
|
||||
posX,
|
||||
posY,
|
||||
sequence: getTableNextSequence(tables),
|
||||
fields: [{
|
||||
id: v4(),
|
||||
name: "id",
|
||||
isPrimary: true,
|
||||
typeId: getInteger()?.id,
|
||||
autoIncrement: true,
|
||||
|
||||
}]
|
||||
} as TableInsertType);
|
||||
|
||||
setFocusedTableId(newTableId);
|
||||
}, [database, tables, getViewport, getInteger]);
|
||||
|
||||
useEffect(() => {
|
||||
if (focusedTableId) {
|
||||
const accordionItem = document.getElementById(focusedTableId)
|
||||
if (accordionItem)
|
||||
accordionItem?.scrollIntoView({
|
||||
behavior: 'smooth', block: "nearest"
|
||||
})
|
||||
}
|
||||
}, [focusedTableId]);
|
||||
|
||||
const searchTables = useCallback(() => {
|
||||
const keyword = nameRef.current?.value;
|
||||
if (keyword !== undefined)
|
||||
setTables(() => allTables.filter((table: TableType) => table.name.toLowerCase().trim().includes(keyword?.toLowerCase().trim())))
|
||||
else
|
||||
setTables(allTables)
|
||||
}, [nameRef, allTables]);
|
||||
|
||||
const toggleSqlPreview = useCallback(() => {
|
||||
setShowSqlPreview(preview => !preview);
|
||||
}, []);
|
||||
|
||||
const tableFilterIds = useMemo(() => {
|
||||
return tables.map((table: TableType) => table.id);
|
||||
}, [tables]);
|
||||
|
||||
return (
|
||||
<div className="w-full h-full flex flex-col min-h-0">
|
||||
<div className="flex items-center gap-2 p-3">
|
||||
<Tooltip >
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant={"ghost"}
|
||||
size={"icon"}
|
||||
className="rounded-md h-8 w-8"
|
||||
onClick={toggleSqlPreview}
|
||||
>
|
||||
{showSqlPreview ? (
|
||||
<IconListDetails className="size-4" stroke={1} />
|
||||
) : (
|
||||
<Code className="size-4 text-muted-foreground" />
|
||||
)}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{t("db_controller.show_code")}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Input
|
||||
className="h-8 bg-secondary dark:bg-background"
|
||||
placeholder={t("db_controller.filter")}
|
||||
ref={nameRef}
|
||||
type="text"
|
||||
onKeyUp={searchTables}
|
||||
/>
|
||||
<Tooltip >
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant={"default"}
|
||||
size={"icon"}
|
||||
className="rounded-md h-8 w-8"
|
||||
onClick={addNewTable}
|
||||
>
|
||||
<IconPlus className="size-4 " />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{t("db_controller.add_table")}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
{
|
||||
allTables.length > 0 && !showSqlPreview &&
|
||||
<ScrollArea className="px-3 h-full w-full overflow-hidden">
|
||||
<Accordion
|
||||
type="single"
|
||||
collapsible
|
||||
className="w-full"
|
||||
value={focusedTableId}
|
||||
onValueChange={setFocusedTableId}
|
||||
>
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
|
||||
<SortableContext
|
||||
items={tables}
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
|
||||
{tables.map((table: TableType) => (
|
||||
<>
|
||||
<TableAccordionItem table={table} key={table.id} />
|
||||
<Separator className="my-1" />
|
||||
</>
|
||||
))}
|
||||
</SortableContext>
|
||||
|
||||
|
||||
</DndContext>
|
||||
</Accordion>
|
||||
</ScrollArea>
|
||||
}
|
||||
{
|
||||
allTables.length > 0 && showSqlPreview &&
|
||||
<div className=" flex-1 overflow-auto -ml-3">
|
||||
<SqlPreview
|
||||
tableFilterIds={tableFilterIds}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
{
|
||||
(allTables.length == 0) &&
|
||||
<div className="px-3 h-full">
|
||||
<EmptyList
|
||||
title={t("db_controller.empty_list.no_tables")}
|
||||
description={t("db_controller.empty_list.no_tables_description")}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default TablesController
|
||||
+413
@@ -0,0 +1,413 @@
|
||||
import { FieldInsertType, FieldType } from "@/lib/schemas/field-schema"
|
||||
|
||||
import React, { Ref, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ModifierValidation } from "./field-setting";
|
||||
import { DataTypes, TimeDefaultValues } from "@/lib/field";
|
||||
|
||||
import { useDatabaseOperations } from "@/providers/database-provider/database-provider";
|
||||
import dayjs from 'dayjs';
|
||||
import { now } from "@internationalized/date";
|
||||
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
|
||||
import { MultiSelect } from "@/components/multi-select";
|
||||
import { DatePicker } from "@/components/date-picker";
|
||||
|
||||
interface DefaultValueType {
|
||||
number?: boolean;
|
||||
string?: boolean;
|
||||
boolean?: boolean;
|
||||
time?: boolean;
|
||||
select?: boolean;
|
||||
multiSelect?: boolean
|
||||
}
|
||||
|
||||
interface FieldDefaultValueProps {
|
||||
field: FieldType;
|
||||
}
|
||||
|
||||
|
||||
|
||||
const fieldDefautlValue: React.FC<FieldDefaultValueProps> = ({ field }) => {
|
||||
const { editField } = useDatabaseOperations();
|
||||
|
||||
const { t } = useTranslation();
|
||||
const [defaultValueValidation, setDefaultValueValidation] = useState<ModifierValidation>({
|
||||
isValid: true,
|
||||
});
|
||||
|
||||
const defaultValueRef: Ref<HTMLInputElement> = useRef<HTMLInputElement>(null);
|
||||
const [values, setValues] = useState<string[]>([]);
|
||||
|
||||
|
||||
const [timeSelection, setTimeSelection] = useState<string>(() => {
|
||||
|
||||
if (field.defaultValue == TimeDefaultValues.NOW)
|
||||
return TimeDefaultValues.NOW;
|
||||
if (!field.defaultValue)
|
||||
return TimeDefaultValues.NO_VALUE
|
||||
|
||||
if (field.type.name == "time" && field.defaultValue)
|
||||
return TimeDefaultValues.CUSTOM
|
||||
|
||||
const date = new Date(field.defaultValue);
|
||||
|
||||
if (!isNaN(date.getTime())) {
|
||||
return TimeDefaultValues.CUSTOM;
|
||||
}
|
||||
return TimeDefaultValues.NO_VALUE;
|
||||
});
|
||||
|
||||
const [defaultDateTime, setDefaultDateTime] = useState<any>(() => {
|
||||
try {
|
||||
if (field.type.name == "time" ) {
|
||||
return field.defaultValue ;
|
||||
}
|
||||
|
||||
if (field.defaultValue) {
|
||||
const date = new Date(field.defaultValue);
|
||||
return date;
|
||||
|
||||
}
|
||||
return undefined;
|
||||
} catch (error) {
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
|
||||
const defaultValueType: DefaultValueType = useMemo(() => {
|
||||
return {
|
||||
number: field.type?.type == DataTypes.INTEGER || field.type?.type == DataTypes.NUMERIC,
|
||||
string: field.type?.type == DataTypes.TEXT || field.type?.name == "year",
|
||||
boolean: field.type?.type == DataTypes.BOOLEAN,
|
||||
time: field.type?.type == DataTypes.TIME && field.type?.name != "year",
|
||||
select: field.type?.type == DataTypes.ENUM && field.type?.name != "set",
|
||||
multiSelect: field.type?.type == DataTypes.ENUM && field.type?.name == "set"
|
||||
}
|
||||
}, [field]);
|
||||
|
||||
|
||||
|
||||
const [selectedValues, setSelectedValues] = useState<string[] | string>(() => {
|
||||
|
||||
if (defaultValueType.select) {
|
||||
if (!field.defaultValue)
|
||||
return "none";
|
||||
else
|
||||
return field.defaultValue
|
||||
}
|
||||
else {
|
||||
if (!field.defaultValue)
|
||||
return []
|
||||
if (defaultValueType.multiSelect)
|
||||
return field.defaultValue.split(",")
|
||||
|
||||
return [field.defaultValue as string];
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (field.values && field.values.length > 0) {
|
||||
try {
|
||||
const jsonValues: string[] = JSON.parse(field.values);
|
||||
|
||||
setValues(jsonValues);
|
||||
} catch (error) {
|
||||
setValues([]);
|
||||
}
|
||||
}
|
||||
}, [field.values]);
|
||||
|
||||
|
||||
|
||||
const defaultValueChange = useCallback((event: any) => {
|
||||
const value = event.target.value;
|
||||
if (value && value.trim().length > 0) {
|
||||
if (field.type?.type == DataTypes.INTEGER) {
|
||||
const isValid: boolean = Number.isInteger(Number(value));
|
||||
setDefaultValueValidation({
|
||||
isValid,
|
||||
errorMessage: t("db_controller.field_settings.errors.integer_default_value")
|
||||
})
|
||||
}
|
||||
} else {
|
||||
setDefaultValueValidation({
|
||||
isValid: true,
|
||||
errorMessage: undefined
|
||||
});
|
||||
}
|
||||
}, [field, defaultValueRef]);
|
||||
|
||||
|
||||
const saveDefaultValue = useCallback((booleanValue?: boolean) => {
|
||||
|
||||
|
||||
if (defaultValueValidation.isValid) {
|
||||
let value: string | undefined;
|
||||
|
||||
if (field.type.type != DataTypes.BOOLEAN)
|
||||
value = defaultValueRef.current?.value;
|
||||
else {
|
||||
value = String(booleanValue);
|
||||
}
|
||||
|
||||
editField({
|
||||
id: field.id,
|
||||
defaultValue: value ? String(value) : null,
|
||||
} as FieldInsertType);
|
||||
}
|
||||
}, [defaultValueRef, field, defaultValueValidation]);
|
||||
|
||||
|
||||
|
||||
|
||||
const changeTimeDefaultValue = (selection: any) => {
|
||||
|
||||
|
||||
let value: string | undefined;
|
||||
|
||||
if (!selection)
|
||||
return;
|
||||
|
||||
if (selection == TimeDefaultValues.NOW)
|
||||
value = TimeDefaultValues.NOW;
|
||||
|
||||
if (selection == TimeDefaultValues.CUSTOM) {
|
||||
|
||||
const currentDateTime = now(Intl.DateTimeFormat().resolvedOptions().timeZone)
|
||||
const date: Date = currentDateTime?.toDate();
|
||||
|
||||
setDefaultDateTime(date);
|
||||
if (field.type.name == "date")
|
||||
value = dayjs(date).format("YYYY-MM-DD")
|
||||
else if (field.type.name == "datetime" || field.type.name == "timestamp")
|
||||
value = dayjs(date).format("YYYY-MM-DD HH:mm:ss")
|
||||
else if (field.type.name == "time")
|
||||
value = dayjs(date).format("HH:mm:ss")
|
||||
|
||||
}
|
||||
|
||||
editField(({
|
||||
id: field.id,
|
||||
defaultValue: value ? String(value) : null
|
||||
}) as FieldInsertType)
|
||||
|
||||
setTimeSelection(selection);
|
||||
}
|
||||
|
||||
const saveDefaultDateTime = useCallback((date: Date | undefined) => {
|
||||
|
||||
let value: string | undefined;
|
||||
|
||||
|
||||
|
||||
if (field.type.name != "time" && date) {
|
||||
setDefaultDateTime(date);
|
||||
|
||||
if (field.type.name == "date")
|
||||
value = dayjs(date).format("YYYY-MM-DD")
|
||||
else if (field.type.name == "datetime" || field.type.name == "timestamp" || field.type.name == "timestamptz")
|
||||
value = dayjs(date).format("YYYY-MM-DD HH:mm:ss")
|
||||
}
|
||||
else if ( field.type.name == "time") {
|
||||
value = defaultDateTime ;
|
||||
}
|
||||
|
||||
|
||||
editField(({
|
||||
id: field.id,
|
||||
defaultValue: value ? String(value) : null
|
||||
}) as FieldInsertType);
|
||||
}, [field, defaultDateTime]);
|
||||
|
||||
const enumValueChange = useCallback((selection: any) => {
|
||||
|
||||
if (defaultValueType.select) {
|
||||
setSelectedValues(selection);
|
||||
editField({
|
||||
id: field.id,
|
||||
defaultValue: selection == "none" ? null : selection
|
||||
} as FieldInsertType);
|
||||
|
||||
return;
|
||||
}
|
||||
let values: string | null = null;
|
||||
const arraySelection = Array.from(selection);
|
||||
|
||||
if (arraySelection.length > 0) {
|
||||
values = arraySelection.join(',');
|
||||
} else {
|
||||
values = null;
|
||||
}
|
||||
|
||||
|
||||
editField({
|
||||
id: field.id,
|
||||
defaultValue: values
|
||||
} as FieldInsertType);
|
||||
|
||||
setSelectedValues(selection);
|
||||
}, [field, defaultValueType])
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
{
|
||||
!defaultValueType.boolean &&
|
||||
|
||||
<Label htmlFor="default_value">
|
||||
{t("db_controller.field_settings.default_value")}
|
||||
</Label>
|
||||
}
|
||||
{
|
||||
(defaultValueType.number || defaultValueType.string) &&
|
||||
<>
|
||||
<Input
|
||||
type={defaultValueType.number ? "number" : "text"}
|
||||
id="default_value"
|
||||
ref={defaultValueRef}
|
||||
aria-invalid={!defaultValueValidation.isValid}
|
||||
onChange={defaultValueChange}
|
||||
defaultValue={field.defaultValue as string}
|
||||
onBlur={saveDefaultValue as any}
|
||||
placeholder={t("db_controller.field_settings.value")}
|
||||
/>
|
||||
{
|
||||
!defaultValueValidation.isValid &&
|
||||
<p className="text-destructive text-xs">
|
||||
{defaultValueValidation.errorMessage}
|
||||
</p>
|
||||
}
|
||||
</>
|
||||
|
||||
}
|
||||
|
||||
{
|
||||
defaultValueType.boolean &&
|
||||
<div className="flex w-full justify-between">
|
||||
<Label htmlFor="default_value">
|
||||
{t("db_controller.field_settings.default_value")}
|
||||
</Label>
|
||||
<Switch
|
||||
id="default_value"
|
||||
onCheckedChange={saveDefaultValue as any}
|
||||
defaultChecked={field.defaultValue == "true"} />
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
(defaultValueType.select) &&
|
||||
|
||||
<Select
|
||||
aria-label="value"
|
||||
value={selectedValues as any}
|
||||
onValueChange={enumValueChange as any}
|
||||
|
||||
>
|
||||
<SelectTrigger id="charset" className="w-full flex ">
|
||||
<SelectValue placeholder={t('db_controller.field_settings.pick_value')} />
|
||||
</SelectTrigger>
|
||||
|
||||
<SelectContent>
|
||||
<SelectItem value={"none"} >
|
||||
No Default value
|
||||
</SelectItem>
|
||||
|
||||
{
|
||||
values.map((value: string) => (
|
||||
<SelectItem
|
||||
key={value}
|
||||
value={value}
|
||||
>
|
||||
{value}
|
||||
</SelectItem>
|
||||
))
|
||||
}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
}
|
||||
{
|
||||
(defaultValueType.multiSelect) &&
|
||||
<MultiSelect
|
||||
options={
|
||||
values.map((value: string) => ({ value, label: value })) as any
|
||||
}
|
||||
onValueChange={enumValueChange}
|
||||
defaultValue={selectedValues as any}
|
||||
placeholder="Select fields..."
|
||||
variant={"secondary"}
|
||||
hideSelectAll
|
||||
/>
|
||||
|
||||
}
|
||||
{
|
||||
defaultValueType.time &&
|
||||
<>
|
||||
<Select
|
||||
aria-label="Time"
|
||||
value={timeSelection}
|
||||
onValueChange={changeTimeDefaultValue}
|
||||
>
|
||||
<SelectTrigger id="time" className="w-full flex">
|
||||
<SelectValue placeholder={t('db_controller.field_settings.pick_value')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={TimeDefaultValues.NO_VALUE} >
|
||||
{t("db_controller.field_settings.time_default_value.no_value")}
|
||||
</SelectItem>
|
||||
<SelectItem value={TimeDefaultValues.CUSTOM}>{t("db_controller.field_settings.time_default_value.custom")}</SelectItem>
|
||||
{
|
||||
(field.type.name == "datetime" || field.type.name?.includes("timestamp")) ?
|
||||
<SelectItem value={TimeDefaultValues.NOW}>{t("db_controller.field_settings.time_default_value.now")}</SelectItem> : null
|
||||
}
|
||||
</SelectContent>
|
||||
|
||||
|
||||
|
||||
</Select>
|
||||
{
|
||||
timeSelection == TimeDefaultValues.CUSTOM && field.type.name != "time" &&
|
||||
<DatePicker
|
||||
value={defaultDateTime}
|
||||
onValueChange={saveDefaultDateTime}
|
||||
/>
|
||||
|
||||
}
|
||||
{
|
||||
timeSelection == TimeDefaultValues.CUSTOM && field.type.name == "time" &&
|
||||
<>
|
||||
<Label htmlFor="time">
|
||||
Time
|
||||
</Label>
|
||||
<Input
|
||||
id="time"
|
||||
type="time"
|
||||
step={1}
|
||||
defaultValue={defaultDateTime}
|
||||
onChange={(event) => setDefaultDateTime( event.target.value)}
|
||||
onBlur={saveDefaultDateTime as any}
|
||||
className="bg-background appearance-none [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-calendar-picker-indicator]:appearance-none"
|
||||
|
||||
/>
|
||||
</>
|
||||
}
|
||||
</>
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export default React.memo(fieldDefautlValue);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
+176
@@ -0,0 +1,176 @@
|
||||
|
||||
|
||||
import { useSortable } from "@dnd-kit/sortable";
|
||||
import { Settings2 } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
import { FieldType } from "@/lib/schemas/field-schema";
|
||||
import { Key, useEffect, useState } from "react";
|
||||
import { useDatabaseOperations } from "@/providers/database-provider/database-provider";
|
||||
import FieldSetting from "./field-setting";
|
||||
import { DataType } from "@/lib/schemas/data-type-schema";
|
||||
import { IconGripVertical, IconKey, IconKeyframe } from "@tabler/icons-react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Combobox } from "@/components/combobox";
|
||||
import { Toggle } from "@/components/ui/toggle";
|
||||
|
||||
import { TooltipTrigger, Tooltip, TooltipContent } from "@/components/ui/tooltip";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
interface Props {
|
||||
field: FieldType
|
||||
}
|
||||
|
||||
|
||||
|
||||
const FieldItem: React.FC<Props> = ({ field }) => {
|
||||
|
||||
const [fieldName, setFieldName] = useState<string>(field.name);
|
||||
|
||||
const { data_types } = useDatabaseOperations();
|
||||
const { editField } = useDatabaseOperations();
|
||||
|
||||
const [selectedType, setSelectedType] = useState<string | undefined>(field.typeId as string | undefined);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { attributes, listeners, setNodeRef, transform } = useSortable({ id: field.id });
|
||||
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setFieldName(field.name);
|
||||
}, [field.name]);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedType(field.typeId as string | undefined);
|
||||
}, [field.typeId])
|
||||
|
||||
|
||||
const saveFieldName = () => {
|
||||
editField({
|
||||
id: field.id,
|
||||
name: fieldName
|
||||
} as FieldType);
|
||||
}
|
||||
const updateFieldType = (key: Key | null) => {
|
||||
|
||||
const dataType: DataType | undefined = data_types.find((dataTypes: DataType) => dataTypes.id == key);
|
||||
if (!dataType)
|
||||
return;
|
||||
|
||||
if (dataType.id == field.typeId)
|
||||
return;
|
||||
if (key != null) {
|
||||
editField({
|
||||
id: field.id,
|
||||
typeId: key
|
||||
} as FieldType);
|
||||
setSelectedType(key as string | undefined);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
const toggleNullable = (nullable: boolean) => {
|
||||
editField({
|
||||
id: field.id,
|
||||
nullable: !nullable
|
||||
} as FieldType);
|
||||
}
|
||||
|
||||
const togglePrimaryKey = (primaryKey: boolean) => {
|
||||
editField({
|
||||
id: field.id,
|
||||
isPrimary: primaryKey
|
||||
} as FieldType);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex w-full gap-1 items-center " style={style} ref={setNodeRef} {...attributes}>
|
||||
<div {...listeners}>
|
||||
<IconGripVertical className="size-4 text-muted-foreground hover:text-foreground cursor-move shrink-0" />
|
||||
</div>
|
||||
<div className="flex gap-2 w-full">
|
||||
<Input
|
||||
aria-label={t("db_controller.name")}
|
||||
placeholder={t("db_controller.name")}
|
||||
value={fieldName}
|
||||
onChange={(event: any) => setFieldName(event.target.value)}
|
||||
onBlur={saveFieldName}
|
||||
className=" flex flex-1 !bg-transparent"
|
||||
onKeyDown={(e : any) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
saveFieldName() ;
|
||||
e.target.blur() ;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Combobox
|
||||
items={data_types}
|
||||
label="name"
|
||||
placeholder={t("db_controller.type")}
|
||||
selectedItem={selectedType}
|
||||
onSelectionChange={updateFieldType}
|
||||
className="flex flex-1 !bg-transparent !font-normal min-w-0"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2 ml-2 ">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span>
|
||||
<Toggle size={"sm"}
|
||||
|
||||
className="size-9 text-muted-foreground "
|
||||
pressed={!field.nullable as boolean}
|
||||
onPressedChange={toggleNullable}
|
||||
>
|
||||
<IconKeyframe className="size-4" />
|
||||
</Toggle>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{t("db_controller.required")} {field.nullable ? "?" : ""}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span>
|
||||
<Toggle size={"sm"}
|
||||
className="size-9 text-muted-foreground "
|
||||
pressed={field.isPrimary as boolean}
|
||||
onPressedChange={togglePrimaryKey}
|
||||
>
|
||||
<IconKey className="size-4" />
|
||||
</Toggle>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{t("db_controller.primary_key")} {!field.isPrimary ? "?" : ""}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Popover >
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
size="icon"
|
||||
variant={"ghost"}
|
||||
className="size-9 dark:bg-card dark:border-none text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<Settings2 className="size-4 " />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent side="right">
|
||||
<FieldSetting field={field} />
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
export default FieldItem;
|
||||
+12
-34
@@ -1,16 +1,15 @@
|
||||
import { Button } from "@heroui/react";
|
||||
import { Plus } from "lucide-react";
|
||||
|
||||
import FieldItem from "./field-item";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
|
||||
import { closestCenter, DndContext, PointerSensor, useSensor, useSensors } from "@dnd-kit/core";
|
||||
import { arrayMove, SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
|
||||
import { FieldType } from "@/lib/schemas/field-schema";
|
||||
import { TableType } from "@/lib/schemas/table-schema";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { useDatabase, useDatabaseOperations } from "@/providers/database-provider/database-provider";
|
||||
import { v4 } from "uuid";
|
||||
import { getNextSequence } from "@/utils/field";
|
||||
import { FieldType } from "@/lib/schemas/field-schema";
|
||||
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useDatabaseOperations } from "@/providers/database-provider/database-provider";
|
||||
|
||||
import hash from "object-hash" ;
|
||||
|
||||
interface Props {
|
||||
@@ -20,10 +19,9 @@ interface Props {
|
||||
|
||||
|
||||
const FieldList: React.FC<Props> = ({ tableFields , tableId}) => {
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [fields, setFields] = useState<FieldType[]>(tableFields);
|
||||
const { createField, orderTableFields , getInteger } = useDatabaseOperations();
|
||||
const { orderTableFields } = useDatabaseOperations();
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -52,16 +50,7 @@ const FieldList: React.FC<Props> = ({ tableFields , tableId}) => {
|
||||
}
|
||||
}
|
||||
|
||||
const addField = () => {
|
||||
createField({
|
||||
id: v4(),
|
||||
name: `field_${fields.length + 1}`,
|
||||
tableId: tableId,
|
||||
sequence: getNextSequence(fields) ,
|
||||
nullable: true,
|
||||
typeId: getInteger()?.id
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full space-y-2 no-select">
|
||||
<DndContext
|
||||
@@ -83,18 +72,7 @@ const FieldList: React.FC<Props> = ({ tableFields , tableId}) => {
|
||||
</SortableContext>
|
||||
</div>
|
||||
</DndContext>
|
||||
<Button
|
||||
variant="flat"
|
||||
radius="sm"
|
||||
|
||||
startContent={
|
||||
<Plus className="size-4 text-font/90 dark:text-white" />
|
||||
}
|
||||
className="h-8 p-2 text-xs bg-transparent hover:bg-default text-font/90 font-semibold"
|
||||
onPressEnd={addField}
|
||||
>
|
||||
{t("db_controller.add_field")}
|
||||
</Button>
|
||||
|
||||
</div >
|
||||
)
|
||||
}
|
||||
+211
-230
@@ -1,14 +1,24 @@
|
||||
import TagInput from "@/components/tag-input/tag-input";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/tooltip/tooltip";
|
||||
|
||||
import { DatabaseDialect } from "@/lib/database";
|
||||
import { DataTypes, Modifiers, MySQLCharset, MySQLCollation, PostgreSQLCharset, PostgreSQLCollation, SQLiteCharset, SQLiteCollation } from "@/lib/field";
|
||||
import { FieldInsertType, FieldType } from "@/lib/schemas/field-schema";
|
||||
import { useDatabaseOperations } from "@/providers/database-provider/database-provider";
|
||||
import { Button, Checkbox, Input, Select, SelectItem, SharedSelection, Switch, Textarea, Tooltip as HeroUITooltip } from "@heroui/react";
|
||||
import { CircleHelp, Trash2, TriangleAlert } from "lucide-react";
|
||||
|
||||
|
||||
|
||||
import React, { Ref, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import FieldDefaultValue from "./field-default-value";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { IconInfoCircle, IconTrash } from "@tabler/icons-react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { InputTags } from "@/components/input-tags";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
|
||||
|
||||
|
||||
@@ -20,6 +30,7 @@ interface FieldSettingProps {
|
||||
export interface ModifierValidation {
|
||||
isValid: boolean;
|
||||
errorMessage?: string;
|
||||
|
||||
}
|
||||
|
||||
const FieldSetting: React.FC<FieldSettingProps> = ({ field }) => {
|
||||
@@ -28,10 +39,10 @@ const FieldSetting: React.FC<FieldSettingProps> = ({ field }) => {
|
||||
|
||||
const { deleteField, editField } = useDatabaseOperations();
|
||||
const { t } = useTranslation();
|
||||
const [charset, setCharset] = useState(new Set([field.charset]));
|
||||
const [collation, setCollation] = useState(new Set([field.collate]));
|
||||
const [charset, setCharset] = useState<string | undefined>(field.charset || "none");
|
||||
const [collation, setCollation] = useState<string | undefined>(field.collate || "none");
|
||||
|
||||
const [note, setNote] = useState<string | undefined>(field.note as string | undefined);
|
||||
const [note, setNote] = useState<string >(field.note ? field.note : "");
|
||||
|
||||
const maxLengthRef: Ref<HTMLInputElement> = useRef<HTMLInputElement>(null);
|
||||
const scaleRef: Ref<HTMLInputElement> = useRef<HTMLInputElement>(null);
|
||||
@@ -41,8 +52,6 @@ const FieldSetting: React.FC<FieldSettingProps> = ({ field }) => {
|
||||
isValid: true,
|
||||
});
|
||||
|
||||
|
||||
|
||||
const [precisionValidation, setPrecisionValidation] = useState<ModifierValidation>({
|
||||
isValid: true,
|
||||
});
|
||||
@@ -53,6 +62,9 @@ const FieldSetting: React.FC<FieldSettingProps> = ({ field }) => {
|
||||
});
|
||||
|
||||
|
||||
const [jsonValues, setJsonValues] = useState<any[]>(field.values ? JSON.parse(field.values) : []);
|
||||
const [mountScale, setMountScale] = useState<boolean>(false);
|
||||
|
||||
const collations = useMemo(() => {
|
||||
if (!field.type)
|
||||
return undefined;
|
||||
@@ -83,13 +95,14 @@ const FieldSetting: React.FC<FieldSettingProps> = ({ field }) => {
|
||||
|
||||
|
||||
const updateFieldNote = useCallback(() => {
|
||||
|
||||
editField({
|
||||
id: field.id,
|
||||
note: note,
|
||||
} as FieldInsertType)
|
||||
}, [field , note]) ;
|
||||
}, [field, note]);
|
||||
|
||||
const toggleUnqiue = useCallback((value: boolean) => {
|
||||
const toggleUnique = useCallback((value: boolean) => {
|
||||
editField({
|
||||
id: field.id,
|
||||
unique: value
|
||||
@@ -119,31 +132,24 @@ const FieldSetting: React.FC<FieldSettingProps> = ({ field }) => {
|
||||
}, [field]);
|
||||
|
||||
|
||||
const changeCharset = useCallback((keys: SharedSelection) => {
|
||||
|
||||
if (keys.anchorKey != field.charset) {
|
||||
|
||||
editField({
|
||||
id: field.id,
|
||||
charset: keys.anchorKey,
|
||||
|
||||
} as FieldInsertType);
|
||||
}
|
||||
setCharset(keys as any);
|
||||
const changeCharset = useCallback((charset: string) => {
|
||||
setCharset(charset);
|
||||
editField({
|
||||
id: field.id,
|
||||
charset: charset == "none" ? null : charset,
|
||||
} as FieldInsertType);
|
||||
}, [field]);
|
||||
|
||||
const changeCollation = useCallback((collation: string) => {
|
||||
|
||||
const changeCollation = useCallback((keys: SharedSelection) => {
|
||||
setCollation(collation);
|
||||
editField({
|
||||
id: field.id,
|
||||
collate: collation == "none" ? null : collation,
|
||||
|
||||
if (keys.anchorKey != field.collate) {
|
||||
} as FieldInsertType);
|
||||
|
||||
editField({
|
||||
id: field.id,
|
||||
collate: keys.anchorKey,
|
||||
|
||||
} as FieldInsertType);
|
||||
}
|
||||
setCollation(keys as any);
|
||||
}, [field])
|
||||
|
||||
|
||||
@@ -177,8 +183,9 @@ const FieldSetting: React.FC<FieldSettingProps> = ({ field }) => {
|
||||
|
||||
const updateValues = useCallback((values: string[]) => {
|
||||
|
||||
const jsonValues = JSON.stringify(values);
|
||||
setJsonValues(values)
|
||||
|
||||
const jsonValues = JSON.stringify(values);
|
||||
if (jsonValues != field.values)
|
||||
editField({
|
||||
id: field.id,
|
||||
@@ -188,13 +195,15 @@ const FieldSetting: React.FC<FieldSettingProps> = ({ field }) => {
|
||||
|
||||
}, [field])
|
||||
|
||||
const showNumericModifiers: boolean = modifiers.includes(Modifiers.AUTO_INCREMENT) || modifiers.includes(Modifiers.UNSIGNED) || modifiers.includes(Modifiers.ZEROFILL)
|
||||
const showNumericModifiers: boolean = modifiers.includes(Modifiers.UNSIGNED) || modifiers.includes(Modifiers.ZEROFILL)
|
||||
const showDecimalModifiers: boolean = modifiers.includes(Modifiers.PRECISION) || modifiers.includes(Modifiers.SCALE);
|
||||
const showTextModifiers: boolean = modifiers.includes(Modifiers.COLLATE) || modifiers.includes(Modifiers.CHARSET);
|
||||
|
||||
|
||||
|
||||
const maxLengthChange = useCallback((value: any) => {
|
||||
const maxLengthChange = useCallback((input: any) => {
|
||||
const value = input.target.value;
|
||||
|
||||
if (value && value.trim().length > 0) {
|
||||
const isValid = Number.isInteger(Number(value)) && value > 0;
|
||||
|
||||
@@ -210,14 +219,8 @@ const FieldSetting: React.FC<FieldSettingProps> = ({ field }) => {
|
||||
isValid: true,
|
||||
errorMessage: undefined
|
||||
});
|
||||
|
||||
}, [maxLengthRef, field]);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const validateScaleAndPrecision = useCallback(() => {
|
||||
const scale: number | null = scaleRef.current && Number(scaleRef.current?.value);
|
||||
const precision: number | null = precisionRef.current && Number(precisionRef.current?.value);
|
||||
@@ -261,232 +264,222 @@ const FieldSetting: React.FC<FieldSettingProps> = ({ field }) => {
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (precisionRef.current) {
|
||||
setMountScale(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div className="w-full flex flex-col gap-2 p-2 min-w-[260px] max-w-[260px]">
|
||||
<h3 className="font-semibold text-sm text-font/90">
|
||||
<div className="w-full flex flex-col gap-2 max-w-[260px]">
|
||||
<h3 className="font-medium text-sm">
|
||||
{t("db_controller.field_settings.title")}
|
||||
</h3>
|
||||
<hr className="border-divider" />
|
||||
<Separator />
|
||||
{
|
||||
!modifiers.includes(Modifiers.NO_UNIQUE) &&
|
||||
<div className="flex w-full justify-between">
|
||||
<span className="text-xs text-font/70 font-medium dark:text-font/90">
|
||||
<div className="flex items-center justify-between ">
|
||||
<Label htmlFor="unique">
|
||||
{t("db_controller.field_settings.unique")}
|
||||
</span>
|
||||
<Checkbox
|
||||
defaultSelected={field.unique as boolean}
|
||||
size="md"
|
||||
|
||||
classNames={{
|
||||
wrapper: "before:border-divider group-data-[hover=true]:before:bg-default",
|
||||
}}
|
||||
onValueChange={toggleUnqiue} />
|
||||
</Label>
|
||||
<Switch id="unique" onCheckedChange={toggleUnique} defaultChecked={field.unique as boolean} />
|
||||
</div>
|
||||
}
|
||||
{
|
||||
modifiers.includes(Modifiers.AUTO_INCREMENT) &&
|
||||
<div className="flex w-full justify-between">
|
||||
<Label htmlFor="autoincrement">
|
||||
{t("db_controller.field_settings.autoIncrement")}
|
||||
</Label>
|
||||
<Switch id="autoincrement" onCheckedChange={toggleAutoIncrement} defaultChecked={field.autoIncrement as boolean} />
|
||||
</div>
|
||||
}
|
||||
{
|
||||
showNumericModifiers && <>
|
||||
<h3 className="font-semibold text-sm text-font/90">
|
||||
<h3 className="font-medium text-sm">
|
||||
{t("db_controller.field_settings.numeric_setting")}
|
||||
</h3>
|
||||
<hr className="border-divider" />
|
||||
{
|
||||
modifiers.includes(Modifiers.AUTO_INCREMENT) &&
|
||||
<div className="flex w-full justify-between">
|
||||
<span className="text-xs text-font/70 font-medium dark:text-font/90">
|
||||
{t("db_controller.field_settings.autoIncrement")}
|
||||
</span>
|
||||
<Checkbox defaultSelected={field.autoIncrement as boolean}
|
||||
classNames={{
|
||||
wrapper: "before:border-divider group-data-[hover=true]:before:bg-default",
|
||||
}}
|
||||
size="md" onValueChange={toggleAutoIncrement} />
|
||||
</div>
|
||||
}
|
||||
<Separator />
|
||||
{
|
||||
modifiers.includes(Modifiers.UNSIGNED) &&
|
||||
<div className="flex w-full justify-between">
|
||||
<span className="text-xs text-font/70 font-medium dark:text-font/90">
|
||||
<Label htmlFor="unsigned">
|
||||
{t("db_controller.field_settings.unsigned")}
|
||||
</span>
|
||||
<Checkbox
|
||||
classNames={{
|
||||
wrapper: "before:border-divider group-data-[hover=true]:before:bg-default",
|
||||
}}
|
||||
defaultSelected={field.unsigned as boolean} size="md" onValueChange={toggleUnsigned} />
|
||||
</Label>
|
||||
<Switch id="unsigned" onCheckedChange={toggleUnsigned} defaultChecked={field.unsigned as boolean} />
|
||||
</div>
|
||||
}
|
||||
{
|
||||
modifiers.includes(Modifiers.ZEROFILL) &&
|
||||
<div className="flex w-full justify-between">
|
||||
<span className="text-xs text-font/70 font-medium dark:text-font/90">
|
||||
<Label htmlFor="zerofill">
|
||||
{t("db_controller.field_settings.zeroFill")}
|
||||
</span>
|
||||
<Checkbox
|
||||
classNames={{
|
||||
wrapper: "before:border-divider group-data-[hover=true]:before:bg-default",
|
||||
}}
|
||||
defaultSelected={field.zeroFill as boolean} size="md" onValueChange={toggleZeroFill} />
|
||||
</Label>
|
||||
<Switch id="zerofill" onCheckedChange={toggleZeroFill} defaultChecked={field.zeroFill as boolean} />
|
||||
</div>
|
||||
}
|
||||
<Separator/>
|
||||
|
||||
|
||||
</>
|
||||
}
|
||||
|
||||
{
|
||||
showDecimalModifiers && <>
|
||||
<h3 className="font-semibold text-sm text-font/90">
|
||||
<h3 className="font-medium text-sm ">
|
||||
{t("db_controller.field_settings.decimal_setting")}
|
||||
</h3>
|
||||
<hr className="border-divider" />
|
||||
<Separator />
|
||||
<div className="flex gap-2">
|
||||
{
|
||||
modifiers.includes(Modifiers.PRECISION) &&
|
||||
<div className="flex flex-col gap-2 w-full">
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<label className="flex items-center text-font/70 justify-between text-xs font-medium dark:text-font/90">
|
||||
<TooltipTrigger asChild>
|
||||
<Label className="flex items-center justify-between font-medium" htmlFor="precision">
|
||||
<span>
|
||||
{t("db_controller.field_settings.precision")}
|
||||
</span>
|
||||
<CircleHelp className="size-3.5" />
|
||||
</label>
|
||||
<IconInfoCircle className="size-3.5" />
|
||||
</Label>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{t("db_controller.field_settings.precision_def")}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Input
|
||||
id="precision"
|
||||
type="number"
|
||||
size="sm"
|
||||
ref={precisionRef}
|
||||
defaultValue={String(field.precision)}
|
||||
onBlur={saveScaleAndPrecision}
|
||||
onValueChange={validateScaleAndPrecision}
|
||||
isInvalid={!precisionValidation.isValid}
|
||||
endContent={
|
||||
!precisionValidation.isValid && <>
|
||||
<HeroUITooltip showArrow={true} content={precisionValidation.errorMessage} radius="sm" color="danger">
|
||||
|
||||
<TriangleAlert className="size-4 text-danger cursor-default" />
|
||||
|
||||
</HeroUITooltip>
|
||||
</>
|
||||
}
|
||||
radius="sm"
|
||||
variant="faded"
|
||||
onChange={validateScaleAndPrecision}
|
||||
aria-invalid={!precisionValidation.isValid}
|
||||
placeholder={t("db_controller.field_settings.precision")}
|
||||
className="h-8 w-full focus-visible:ring-0 shadow-none "
|
||||
classNames={{
|
||||
inputWrapper: "dark:bg-default border-divider group-hover:border-primary group-data-[focus=true]:border-primary ",
|
||||
}}
|
||||
|
||||
/>
|
||||
|
||||
|
||||
</div>
|
||||
}
|
||||
{
|
||||
modifiers.includes(Modifiers.SCALE) &&
|
||||
<div className="flex flex-col gap-2 w-full">
|
||||
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<label className="flex items-center justify-between text-xs font-medium text-icon dark:text-font/90">
|
||||
<TooltipTrigger asChild>
|
||||
<Label className="flex items-center justify-between font-medium" htmlFor="scale">
|
||||
<span>
|
||||
{t("db_controller.field_settings.scale")}
|
||||
</span>
|
||||
<CircleHelp className="size-3.5" />
|
||||
</label>
|
||||
<IconInfoCircle className="size-3.5" />
|
||||
</Label>
|
||||
</TooltipTrigger>
|
||||
|
||||
<TooltipContent>
|
||||
{t("db_controller.field_settings.scale_def")}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Input
|
||||
type="number"
|
||||
size="sm"
|
||||
ref={scaleRef}
|
||||
defaultValue={String(field.scale)}
|
||||
onBlur={saveScaleAndPrecision}
|
||||
isDisabled={!precisionRef.current?.value || !precisionValidation.isValid}
|
||||
onValueChange={validateScaleAndPrecision}
|
||||
isInvalid={!scaleValidation.isValid}
|
||||
endContent={
|
||||
!scaleValidation.isValid && <>
|
||||
<HeroUITooltip showArrow={true} content={scaleValidation.errorMessage} radius="sm" color="danger">
|
||||
<TriangleAlert className="size-4 text-danger cursor-default" />
|
||||
</HeroUITooltip>
|
||||
</>
|
||||
}
|
||||
radius="sm"
|
||||
variant="faded"
|
||||
placeholder={t("db_controller.field_settings.scale")}
|
||||
className="h-8 w-full focus-visible:ring-0 shadow-none "
|
||||
classNames={{
|
||||
inputWrapper: "dark:bg-default border-divider group-hover:border-primary group-data-[focus=true]:border-primary ",
|
||||
}}
|
||||
{
|
||||
mountScale &&
|
||||
<Input
|
||||
id="scale"
|
||||
type="number"
|
||||
ref={scaleRef}
|
||||
defaultValue={String(field.scale)}
|
||||
onBlur={saveScaleAndPrecision}
|
||||
disabled={!precisionRef.current?.value || !precisionValidation.isValid}
|
||||
onChange={validateScaleAndPrecision}
|
||||
aria-invalid={!scaleValidation.isValid}
|
||||
placeholder={t("db_controller.field_settings.scale")}
|
||||
/>
|
||||
}
|
||||
|
||||
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
{
|
||||
!precisionValidation.isValid &&
|
||||
<p className="text-destructive text-xs">
|
||||
{precisionValidation.errorMessage}
|
||||
</p>
|
||||
}
|
||||
{
|
||||
!scaleValidation.isValid &&
|
||||
<p className="text-destructive text-xs">
|
||||
{scaleValidation.errorMessage}
|
||||
</p>
|
||||
}
|
||||
|
||||
</>
|
||||
}
|
||||
|
||||
{
|
||||
showTextModifiers &&
|
||||
<>
|
||||
<h3 className="font-semibold text-sm text-font/90">
|
||||
<h3 className="font-medium text-sm ">
|
||||
{t("db_controller.field_settings.text_setting")}
|
||||
</h3>
|
||||
<hr className="border-divider" />
|
||||
<Separator />
|
||||
{
|
||||
modifiers.includes(Modifiers.CHARSET) && charsets && <>
|
||||
<label className="text-xs font-medium text-icon dark:text-font/90">
|
||||
(modifiers.includes(Modifiers.CHARSET) && charsets) && <>
|
||||
<Label htmlFor="charset">
|
||||
{t("db_controller.field_settings.charset")}
|
||||
</label>
|
||||
</Label>
|
||||
<Select
|
||||
size="sm"
|
||||
variant="bordered"
|
||||
aria-label="charset"
|
||||
placeholder={t("db_controller.field_settings.charset")}
|
||||
selectedKeys={charset as any}
|
||||
onSelectionChange={changeCharset}
|
||||
className="h-8 w-full focus-visible:ring-0 shadow-none "
|
||||
|
||||
classNames={{
|
||||
trigger: "border-divider group-hover:border-primary data-[focus=true]:border-primary data-[open=true]:border-primary",
|
||||
selectorIcon: "text-icon",
|
||||
popoverContent: "rounded-md "
|
||||
}}
|
||||
|
||||
|
||||
value={charset as any}
|
||||
onValueChange={changeCharset as any}
|
||||
>
|
||||
{
|
||||
Object.values(charsets).map((charset: string) => (<SelectItem key={charset}>{charset}</SelectItem>))
|
||||
}
|
||||
<SelectTrigger id="charset" className="w-full flex ">
|
||||
<SelectValue placeholder={t("db_controller.field_settings.charset")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={"none"} >
|
||||
No charset
|
||||
</SelectItem>
|
||||
|
||||
{
|
||||
Object.values(charsets).map((charset: string) => (
|
||||
<SelectItem
|
||||
key={charset}
|
||||
value={charset}
|
||||
>
|
||||
{charset}
|
||||
</SelectItem>
|
||||
))
|
||||
}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</>
|
||||
}
|
||||
{
|
||||
modifiers.includes(Modifiers.COLLATE) && collations && <>
|
||||
<label className="text-xs font-medium text-icon dark:text-font/90">
|
||||
(modifiers.includes(Modifiers.COLLATE) && collations) && <>
|
||||
<Label htmlFor="collation">
|
||||
{t("db_controller.field_settings.collation")}
|
||||
</label>
|
||||
</Label>
|
||||
<Select
|
||||
size="sm"
|
||||
variant="bordered"
|
||||
aria-label="collation"
|
||||
placeholder={t("db_controller.field_settings.collation")}
|
||||
selectedKeys={collation as any}
|
||||
onSelectionChange={changeCollation}
|
||||
className="h-8 w-full focus-visible:ring-0 shadow-none "
|
||||
classNames={{
|
||||
trigger: "border-divider group-hover:border-primary data-[focus=true]:border-primary data-[open=true]:border-primary",
|
||||
selectorIcon: "text-icon",
|
||||
popoverContent: "rounded-md "
|
||||
value={collation as any}
|
||||
onValueChange={changeCollation as any}
|
||||
|
||||
}}
|
||||
>
|
||||
{
|
||||
Object.values(collations).map((collation: string) => (<SelectItem key={collation}>{collation}</SelectItem>))
|
||||
}
|
||||
<SelectTrigger id="collation" className="w-full">
|
||||
<SelectValue placeholder={t("db_controller.field_settings.collation")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
||||
<SelectItem value={"none"} >
|
||||
No collation
|
||||
</SelectItem>
|
||||
{
|
||||
Object.values(collations).map((collation: string) => (<SelectItem
|
||||
key={collation}
|
||||
value={collation}>{collation}</SelectItem>))
|
||||
}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</>
|
||||
}
|
||||
@@ -495,99 +488,87 @@ const FieldSetting: React.FC<FieldSettingProps> = ({ field }) => {
|
||||
}
|
||||
{
|
||||
modifiers.includes(Modifiers.LENGTH) && <>
|
||||
<label className="text-xs font-medium text-font/70 dark:text-font/90">
|
||||
<Label htmlFor="length">
|
||||
{
|
||||
field.type?.type == DataTypes.INTEGER ?
|
||||
t("db_controller.field_settings.integer_width")
|
||||
:
|
||||
t("db_controller.field_settings.max_length")
|
||||
}
|
||||
</label>
|
||||
</Label>
|
||||
|
||||
<Input
|
||||
type="number"
|
||||
size="sm"
|
||||
id="length"
|
||||
ref={maxLengthRef}
|
||||
defaultValue={String(field.maxLength)}
|
||||
onBlur={saveMaxLength}
|
||||
radius="sm"
|
||||
isInvalid={!maxLengthValidation.isValid}
|
||||
onValueChange={maxLengthChange}
|
||||
endContent={
|
||||
!maxLengthValidation.isValid && <>
|
||||
<HeroUITooltip showArrow={true} content={maxLengthValidation.errorMessage} radius="sm" color="danger">
|
||||
|
||||
<TriangleAlert className="size-4 text-danger cursor-default" />
|
||||
|
||||
</HeroUITooltip>
|
||||
</>
|
||||
}
|
||||
variant="faded"
|
||||
aria-invalid={!maxLengthValidation.isValid}
|
||||
onChange={maxLengthChange}
|
||||
placeholder={
|
||||
field.type?.type == DataTypes.INTEGER ?
|
||||
t("db_controller.field_settings.width")
|
||||
:
|
||||
t("db_controller.field_settings.max_length")
|
||||
|
||||
}
|
||||
className="h-8 w-full focus-visible:ring-0 shadow-none "
|
||||
classNames={{
|
||||
inputWrapper: "dark:bg-default border-divider group-hover:border-primary group-data-[focus=true]:border-primary ",
|
||||
}}
|
||||
|
||||
/>
|
||||
{
|
||||
!maxLengthValidation.isValid &&
|
||||
<p className="text-destructive text-xs">
|
||||
{maxLengthValidation.errorMessage}
|
||||
</p>
|
||||
}
|
||||
</>
|
||||
}
|
||||
|
||||
{
|
||||
modifiers.includes(Modifiers.VALUES) && <>
|
||||
<h3 className="font-semibold text-sm text-font/90">
|
||||
<Label htmlFor="values">
|
||||
{field.type.name != null && (field.type.name?.[0].toUpperCase() + field.type.name?.slice(1))} {t("db_controller.field_settings.values")}
|
||||
</h3>
|
||||
<hr className="border-divider" />
|
||||
<TagInput
|
||||
onItemsChange={updateValues}
|
||||
defaultItems={field.values ? JSON.parse(field.values) : []}
|
||||
</Label>
|
||||
<Separator />
|
||||
<InputTags
|
||||
value={jsonValues}
|
||||
onChange={updateValues as any}
|
||||
placeholder="Enter values, comma separated..."
|
||||
/>
|
||||
</>
|
||||
}
|
||||
|
||||
{
|
||||
!modifiers.includes(Modifiers.NO_DEFAULT) &&
|
||||
<FieldDefaultValue field={field} />
|
||||
<>
|
||||
<Separator />
|
||||
<FieldDefaultValue field={field} />
|
||||
|
||||
</>
|
||||
}
|
||||
<label className="text-xs font-medium text-font/70 dark:text-font/90">
|
||||
|
||||
<Label htmlFor="collation">
|
||||
{t("db_controller.field_settings.note")}
|
||||
</label>
|
||||
</Label>
|
||||
<Textarea
|
||||
variant="bordered"
|
||||
className="w-full"
|
||||
label={t("db_controller.field_settings.field_note")}
|
||||
placeholder={t("db_controller.field_settings.field_note")}
|
||||
value={note}
|
||||
disableAutosize
|
||||
disableAnimation
|
||||
onValueChange={setNote}
|
||||
onChange={(event: any) => setNote(event.target.value)}
|
||||
onBlur={updateFieldNote}
|
||||
classNames={{
|
||||
base: "max-w-xs",
|
||||
input: "resize-y min-h-[60px] max-h-[180px]",
|
||||
inputWrapper: "bg-default border-divider dark:bg-background-100 group-hover:border-primary group-data-[focus=true]:border-primary",
|
||||
label: "text-font/90 group-data-[focus=true]:text-font/70 group-data-[filled-within=true]:text-font/70 "
|
||||
}} />
|
||||
<hr className="border-divider" />
|
||||
className="resize-none min-h-[86px] focus-visible:ring-0 bg-secondary dark:bg-background"
|
||||
/>
|
||||
<Separator />
|
||||
<Button
|
||||
className="bg-default dark:bg-danger dark:border-none dark:text-white"
|
||||
radius="sm"
|
||||
variant="faded"
|
||||
color="danger"
|
||||
variant={"destructive"}
|
||||
size="sm"
|
||||
onPressEnd={removeField}>
|
||||
<span className="font-medium text-sm ">
|
||||
{t("db_controller.field_settings.delete_field")}
|
||||
</span>
|
||||
<Trash2 className="mr-1 size-3.5 text-danger dark:text-white" />
|
||||
onClick={removeField}>
|
||||
|
||||
{t("db_controller.field_settings.delete_field")}
|
||||
<IconTrash className="size-4" />
|
||||
|
||||
</Button>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export default React.memo(FieldSetting);
|
||||
export default React.memo(FieldSetting);
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user