mirror of
https://github.com/stackrender/stackrender.git
synced 2026-09-10 11:15:42 +00:00
Compare commits
42 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e169b84ade | |||
| 994c797195 | |||
| 42e2bcf5e9 | |||
| 91fe5d6cf2 | |||
| 585f5c7a7f | |||
| d0cd2d8fa4 | |||
| fab51f2d13 | |||
| fe84e0d748 | |||
| ae9e052cf6 | |||
| 557387a4fd | |||
| 4a2f6ae8f5 | |||
| e8838d1f1e | |||
| c1ae2dbf69 | |||
| eb6c80417c | |||
| 5cce8cc03d | |||
| caa277577a | |||
| 6c3b431ee2 | |||
| 3985e53886 | |||
| ef27393d9a | |||
| 9feb0397bc | |||
| 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-*
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
cache: npm
|
||||||
|
- run: npm ci
|
||||||
|
- run: npm test
|
||||||
+1
-1
@@ -60,7 +60,7 @@ representative at an online or offline event.
|
|||||||
|
|
||||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||||
reported to the community leaders responsible for enforcement at
|
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 complaints will be reviewed and investigated promptly and fairly.
|
||||||
|
|
||||||
All community leaders are obligated to respect the privacy and security of the
|
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:
|
If you have an idea for improvement:
|
||||||
- Open a **feature request** in the issues section.
|
- Open a **feature request** in the issues section.
|
||||||
- Describe the feature and explain why it would be useful.
|
- 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
|
## License Agreement
|
||||||
By contributing, you agree that your work will be licensed under the **GNU Affero General Public License v3.0**.
|
By contributing, you agree that your work will be licensed under the **GNU Affero General Public License v3.0**.
|
||||||
|
|
||||||
## Questions or Support
|
## 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,168 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
### Out of Memory During Build
|
||||||
|
|
||||||
|
The production build is memory-intensive (the app compiles thousands of modules). If `docker compose up` or `docker build` fails with an out-of-memory error such as `Reached heap limit` or `cannot allocate memory` during the `npm run build` step, increase the memory available to Docker:
|
||||||
|
|
||||||
|
- **Docker Desktop**: Settings, then Resources, then Memory. Set it to at least **6-8 GB** and apply.
|
||||||
|
|
||||||
|
The Dockerfile raises the Node heap (`NODE_OPTIONS=--max-old-space-size`) for the build. That heap size must fit inside the memory available to Docker, so raising the Docker memory limit is the fix when the build runs out of memory.
|
||||||
|
|
||||||
|
### 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;"]
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
|
|
||||||
|
|
||||||

|

|
||||||
<h4 align="center">
|
<h4 align="center">
|
||||||
<a href="https://www.stackrender.io">
|
<a href="https://www.stackrender.io">
|
||||||
<img src="https://img.shields.io/badge/Start%20Building!-gray.svg?logo=data:image/svg+xml;base64,PHN2ZyBmaWxsPSIjRkZENzAwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMiIgaGVpZ2h0PSIxMiIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMTMgMyBMMiAxMyBoNyBMMTEgMjEgTDIyIDExIGgtNyBMIDEzIDMgeiIvPjwvc3ZnPg==" alt="Start Building!" />
|
<img src="https://img.shields.io/badge/Start%20Building!-gray.svg?logo=data:image/svg+xml;base64,PHN2ZyBmaWxsPSIjRkZENzAwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMiIgaGVpZ2h0PSIxMiIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMTMgMyBMMiAxMyBoNyBMMTEgMjEgTDIyIDExIGgtNyBMIDEzIDMgeiIvPjwvc3ZnPg==" alt="Start Building!" />
|
||||||
</a>
|
</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" />
|
<img src="https://img.shields.io/discord/1352085267535761448?color=5865F2&label=Discord&logo=discord&logoColor=white" alt="Discord community channel" />
|
||||||
</a>
|
</a>
|
||||||
<a href="https://x.com/intent/follow?screen_name=Iam_The_Dev">
|
<a href="https://x.com/intent/follow?screen_name=Iam_The_Dev">
|
||||||
@@ -15,16 +15,16 @@
|
|||||||
|
|
||||||
</h4>
|
</h4>
|
||||||
|
|
||||||
# Welcom to StackRender ⚡
|
# Welcome to StackRender ⚡
|
||||||
StackRender was born from the need to automate backend development, covering everything from the database to the final API endpoint. Our first step toward this long-term vision is to provide a **next-generation, free, open-source database schema diagram generator**.
|
StackRender was born from the need to automate backend development, covering everything from the database to the final API endpoint. Our first step toward this long-term vision is to provide a **next-generation, free, open-source database schema diagram generator**.
|
||||||
|
|
||||||
StackRender helps you go from **specifications** to a **fully functional, production-ready database** that can be exported in your preferred SQL dialect: **MySQL, PostgreSQL, MariaDB, or SQLite**.
|
StackRender helps you go from **specifications** to a **fully functional, production-ready database** that can be exported in your preferred SQL dialect: **MySQL, PostgreSQL, MariaDB, SQLite, Oracle, or SQL Server**.
|
||||||
|
|
||||||
## Main features
|
## Main features
|
||||||
|
|
||||||
- **Interactive Diagram UI** – Visually design and manage your database schemas with an intuitive drag-and-drop interface.
|
- **Interactive Diagram UI** – Visually design and manage your database schemas with an intuitive drag-and-drop interface.
|
||||||
- **In-Depth Tables & Columns Control** – Fully customize tables, columns, types, and constraints.
|
- **In-Depth Tables & Columns Control** – Fully customize tables, columns, types, and constraints.
|
||||||
- **Indices Suggestions** – Receive recommendations to optimize database performance.
|
- **Index Suggestions** – Receive recommendations to optimize database performance.
|
||||||
- **Import / Export SQL DDL** – Easily import existing schemas or export your design as SQL scripts.
|
- **Import / Export SQL DDL** – Easily import existing schemas or export your design as SQL scripts.
|
||||||
- **Foreign Key Cycle Detection** – Identify and resolve circular dependencies in relationships.
|
- **Foreign Key Cycle Detection** – Identify and resolve circular dependencies in relationships.
|
||||||
- **AI-Powered Database Assistant** – (Cloud version) Generate database diagrams from specifications and perform additional operations such as schema enrichment, soft-delete implementation, and automatic documentation generation.
|
- **AI-Powered Database Assistant** – (Cloud version) Generate database diagrams from specifications and perform additional operations such as schema enrichment, soft-delete implementation, and automatic documentation generation.
|
||||||
@@ -38,25 +38,46 @@ StackRender is currently in **Public Beta**. Star and watch this repository to g
|
|||||||
- ✅ PostgreSQL
|
- ✅ PostgreSQL
|
||||||
- ✅ MySQL
|
- ✅ MySQL
|
||||||
- ✅ MariaDB
|
- ✅ MariaDB
|
||||||
- ✅ SQLite
|
- ✅ SQLite
|
||||||
|
- ✅ Oracle
|
||||||
|
- ✅ SQL Server (MSSQL)
|
||||||
|
|
||||||
And more coming very soon!
|
|
||||||
## Get Started
|
## Get Started
|
||||||
|
|
||||||
Use the [cloud version](https://www.stackrender.io) or deploy locally to start designing your database schemas in minutes.
|
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:
|
Install dependencies and start the development server:
|
||||||
```text
|
```bash
|
||||||
npm install
|
npm install
|
||||||
npm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Then visit `http://localhost:3000` in your browser.
|
||||||
|
|
||||||
### How to Build
|
### How to Build
|
||||||
Install dependencies and start the production build:
|
Install dependencies and create a production build:
|
||||||
```text
|
```bash
|
||||||
npm install
|
npm install
|
||||||
npm run start
|
npm run build
|
||||||
```
|
```
|
||||||
|
|
||||||
## Try Using It in the Cloud
|
## Try Using It in the Cloud
|
||||||
|
|
||||||
1. Visit [StackRender.io](https://www.stackrender.io)
|
1. Visit [StackRender.io](https://www.stackrender.io)
|
||||||
@@ -67,8 +88,8 @@ npm run start
|
|||||||
6. Export your Database SQL DDL and run it!
|
6. Export your Database SQL DDL and run it!
|
||||||
|
|
||||||
## 🤝 Community
|
## 🤝 Community
|
||||||
|
- [**Docs**](https://www.stackrender.io/docs) – Explore the StackRender docs to learn more about its features.
|
||||||
- [**Discord**](https://discord.com/invite/DsN8RcPR6Y) – Live discussions about upcoming releases and the future of StackRender.
|
- [**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.
|
- [**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.
|
- [**X (Twitter)**](https://x.com/intent/follow?screen_name=Iam_The_Dev) – Get the latest StackRender news and updates.
|
||||||
|
|
||||||
@@ -76,7 +97,7 @@ npm run start
|
|||||||
|
|
||||||
We welcome all contributions, whether small bug fixes or major feature additions.
|
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.
|
- 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.
|
- 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
|
## Reporting a Vulnerability
|
||||||
If you discover a security vulnerability in StackRender, please report it to us directly.
|
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.
|
- GitHub Security Advisory: Use the "Report a vulnerability" option in the repository's Security tab.
|
||||||
|
|
||||||
When reporting, please include:
|
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>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
<head>
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<title>StackRender Studio</title>
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta key="title" content="StackRender Studio" property="og:title" />
|
<title>StackRender Studio</title>
|
||||||
<meta
|
<meta key="title" content="StackRender Studio" property="og:title" />
|
||||||
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."
|
<meta
|
||||||
property="og:description"
|
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
|
<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."
|
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"
|
name="description" />
|
||||||
/>
|
<meta key="viewport"
|
||||||
<meta
|
content="viewport-fit=cover, width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"
|
||||||
key="viewport"
|
name="viewport" />
|
||||||
content="viewport-fit=cover, width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"
|
<link href="/favicon.png" rel="icon" sizes="32x32" />
|
||||||
name="viewport"
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
/>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
<link href="/favicon.png" rel="icon" sizes="32x32" />
|
<link
|
||||||
</head>
|
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"
|
||||||
<body>
|
rel="stylesheet" />
|
||||||
<div id="root"></div>
|
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
</head>
|
||||||
</body>
|
|
||||||
</html>
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
+35
@@ -0,0 +1,35 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
|
||||||
|
# Putting these at the server level ensures BOTH index.html AND your cached .js/.wasm assets use them!
|
||||||
|
add_header Cross-Origin-Opener-Policy "same-origin" always;
|
||||||
|
add_header Cross-Origin-Embedder-Policy "require-corp" 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";
|
||||||
|
add_header Cross-Origin-Opener-Policy "same-origin" always;
|
||||||
|
add_header Cross-Origin-Embedder-Policy "require-corp" always;
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+4737
-8912
File diff suppressed because it is too large
Load Diff
+47
-17
@@ -1,31 +1,54 @@
|
|||||||
{
|
{
|
||||||
"name": "vite-template",
|
"name": "StackRender",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.0",
|
"version": "1.4.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "tsc && vite build",
|
"build": "tsc && cross-env NODE_OPTIONS=--max-old-space-size=8192 vite build",
|
||||||
"lint": "eslint -c .eslintrc.json ./src/**/**/*.{ts,tsx} --fix",
|
"lint": "eslint -c .eslintrc.json ./src/**/**/*.{ts,tsx} --fix",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview",
|
||||||
|
"test": "vitest run",
|
||||||
|
"test:watch": "vitest"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/lang-sql": "^6.9.0",
|
"@codemirror/lang-sql": "^6.9.0",
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
"@dnd-kit/sortable": "^10.0.0",
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
"@heroui/react": "^2.7.6",
|
"@guanmingchiu/sqlparser-ts": "^0.61.1",
|
||||||
"@internationalized/date": "^3.8.2",
|
"@internationalized/date": "^3.8.2",
|
||||||
"@nextui-org/react": "^2.6.11",
|
"@nextui-org/react": "^2.6.11",
|
||||||
"@powersync/drizzle-driver": "^0.4.0",
|
"@powersync/drizzle-driver": "^0.4.0",
|
||||||
"@powersync/react": "^1.5.3",
|
"@powersync/react": "^1.5.3",
|
||||||
"@powersync/web": "^1.20.0",
|
"@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-aria/visually-hidden": "3.8.21",
|
||||||
"@react-types/shared": "3.28.0",
|
"@react-types/shared": "3.28.0",
|
||||||
"@reecelucas/react-use-hotkeys": "^2.0.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",
|
"@uiw/react-codemirror": "^4.23.12",
|
||||||
"@xyflow/react": "^12.6.3",
|
"@xyflow/react": "^12.6.3",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"cmdk": "^1.1.1",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"drizzle-orm": "^0.43.1",
|
"drizzle-orm": "^0.43.1",
|
||||||
@@ -33,29 +56,32 @@
|
|||||||
"fast-json-patch": "^3.1.1",
|
"fast-json-patch": "^3.1.1",
|
||||||
"framer-motion": "11.15.0",
|
"framer-motion": "11.15.0",
|
||||||
"i18next-browser-languagedetector": "^8.0.5",
|
"i18next-browser-languagedetector": "^8.0.5",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.18.1",
|
||||||
"lucide-react": "^0.501.0",
|
"lucide-react": "^0.501.0",
|
||||||
"next-themes": "^0.4.6",
|
|
||||||
"node-sql-parser": "^5.3.9",
|
"node-sql-parser": "^5.3.9",
|
||||||
"object-hash": "^3.0.0",
|
"object-hash": "^3.0.0",
|
||||||
"pgsql-ast-parser": "^12.0.1",
|
"pgsql-ast-parser": "^12.0.1",
|
||||||
"pluralize": "^8.0.0",
|
"pluralize": "^8.0.0",
|
||||||
"react": "18.3.1",
|
"react": "18.3.1",
|
||||||
|
"react-day-picker": "^9.10.0",
|
||||||
"react-dom": "18.3.1",
|
"react-dom": "18.3.1",
|
||||||
"react-i18next": "^15.5.1",
|
"react-i18next": "^15.5.1",
|
||||||
"react-resizable": "^3.0.5",
|
"react-resizable-panels": "^2.1.9",
|
||||||
"react-router-dom": "6.23.0",
|
"react-router-dom": "6.30.3",
|
||||||
"react-tag-input": "^6.10.6",
|
"react-tag-input": "^6.10.6",
|
||||||
|
"sonner": "^2.0.7",
|
||||||
"sql-formatter": "^15.6.3",
|
"sql-formatter": "^15.6.3",
|
||||||
"tailwind-merge": "^3.2.0",
|
"tailwind-merge": "^3.3.1",
|
||||||
"tailwind-variants": "0.3.0",
|
"tailwind-variants": "0.3.0",
|
||||||
"tailwindcss": "3.4.16",
|
"tailwindcss": "^4.1.13",
|
||||||
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"use-undo": "^1.1.1",
|
"use-undo": "^1.1.1",
|
||||||
"uuid": "^11.1.0"
|
"uuid": "^11.1.0",
|
||||||
|
"vaul": "^1.1.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/better-sqlite3": "^7.6.13",
|
"@types/better-sqlite3": "^7.6.13",
|
||||||
"@types/node": "20.5.7",
|
"@types/node": "20.19.0",
|
||||||
"@types/object-hash": "^3.0.6",
|
"@types/object-hash": "^3.0.6",
|
||||||
"@types/pluralize": "^0.0.33",
|
"@types/pluralize": "^0.0.33",
|
||||||
"@types/react": "18.3.3",
|
"@types/react": "18.3.3",
|
||||||
@@ -64,6 +90,8 @@
|
|||||||
"@typescript-eslint/parser": "8.11.0",
|
"@typescript-eslint/parser": "8.11.0",
|
||||||
"@vitejs/plugin-react": "^4.2.1",
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
"autoprefixer": "10.4.19",
|
"autoprefixer": "10.4.19",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
|
"esbuild": "^0.28.0",
|
||||||
"eslint": "^8.57.0",
|
"eslint": "^8.57.0",
|
||||||
"eslint-config-prettier": "9.1.0",
|
"eslint-config-prettier": "9.1.0",
|
||||||
"eslint-plugin-import": "^2.26.0",
|
"eslint-plugin-import": "^2.26.0",
|
||||||
@@ -73,11 +101,13 @@
|
|||||||
"eslint-plugin-react": "^7.23.2",
|
"eslint-plugin-react": "^7.23.2",
|
||||||
"eslint-plugin-react-hooks": "^4.6.0",
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
"eslint-plugin-unused-imports": "4.1.4",
|
"eslint-plugin-unused-imports": "4.1.4",
|
||||||
"postcss": "8.4.38",
|
|
||||||
"prettier": "3.3.3",
|
"prettier": "3.3.3",
|
||||||
|
"rollup": "^4.61.1",
|
||||||
|
"tw-animate-css": "^1.3.6",
|
||||||
"typescript": "5.6.3",
|
"typescript": "5.6.3",
|
||||||
"vite": "^5.2.0",
|
"vite": "^5.2.0",
|
||||||
"vite-plugin-top-level-await": "^1.5.0",
|
"vite-plugin-top-level-await": "^1.5.0",
|
||||||
"vite-tsconfig-paths": "^4.3.2"
|
"vite-tsconfig-paths": "^4.3.2",
|
||||||
|
"vitest": "^2.1.9"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
After Width: | Height: | Size: 94 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 66 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 157 KiB After Width: | Height: | Size: 75 KiB |
+1
-11
@@ -1,38 +1,28 @@
|
|||||||
|
|
||||||
|
|
||||||
import "@/styles/globals.css"
|
import "@/styles/globals.css"
|
||||||
import { TooltipProvider } from "./components/tooltip/tooltip";
|
|
||||||
import { ReactFlowProvider } from "@xyflow/react";
|
import { ReactFlowProvider } from "@xyflow/react";
|
||||||
import useAppRoutes from "./routes/app-route";
|
import useAppRoutes from "./routes/app-route";
|
||||||
import { SyncProvider } from "./providers/sync-provider/sync-provider";
|
import { SyncProvider } from "./providers/sync-provider/sync-provider";
|
||||||
import DatabaseProvider from "./providers/database-provider/database-provider";
|
import DatabaseProvider from "./providers/database-provider/database-provider";
|
||||||
import DiagramProvider from "./providers/diagram-provider/diagram-provider";
|
import DiagramProvider from "./providers/diagram-provider/diagram-provider";
|
||||||
|
|
||||||
import { ToastProvider } from "@heroui/react";
|
|
||||||
import { ModalProvider } from "./providers/modal-provider/modal-provider";
|
import { ModalProvider } from "./providers/modal-provider/modal-provider";
|
||||||
import DatabaseHotkeysProvider from "./providers/database-hotkeys/database-hotkeys-provider";
|
import DatabaseHotkeysProvider from "./providers/database-hotkeys/database-hotkeys-provider";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
|
||||||
const appRoutes = useAppRoutes();
|
const appRoutes = useAppRoutes();
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ToastProvider placement="bottom-right" />
|
|
||||||
<SyncProvider>
|
<SyncProvider>
|
||||||
<ReactFlowProvider>
|
<ReactFlowProvider>
|
||||||
<DatabaseProvider>
|
<DatabaseProvider>
|
||||||
<DiagramProvider>
|
<DiagramProvider>
|
||||||
|
|
||||||
<TooltipProvider delayDuration={0}>
|
|
||||||
<ModalProvider>
|
<ModalProvider>
|
||||||
<DatabaseHotkeysProvider>
|
<DatabaseHotkeysProvider>
|
||||||
{appRoutes}
|
{appRoutes}
|
||||||
</DatabaseHotkeysProvider>
|
</DatabaseHotkeysProvider>
|
||||||
</ModalProvider>
|
</ModalProvider>
|
||||||
</TooltipProvider>
|
|
||||||
|
|
||||||
</DiagramProvider>
|
</DiagramProvider>
|
||||||
</DatabaseProvider>
|
</DatabaseProvider>
|
||||||
</ReactFlowProvider>
|
</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 React, { useCallback, useState } from "react";
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "../tooltip/tooltip";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Button } from "@heroui/react";
|
|
||||||
|
|
||||||
import { Copy, CopyCheck } from "lucide-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 [isCopied, setIsCopied] = useState<boolean>(false);
|
||||||
|
|
||||||
const copyToClipboard = useCallback(async () => {
|
const copyToClipboard = useCallback(async () => {
|
||||||
|
|
||||||
|
|
||||||
if (text)
|
if (text)
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(text);
|
await navigator.clipboard.writeText(text);
|
||||||
@@ -33,15 +41,14 @@ const Clipboard: React.FC<ClipboardProps> = ({ text }) => {
|
|||||||
}, [text])
|
}, [text])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger>
|
<TooltipTrigger asChild>
|
||||||
<span>
|
<span>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="icon"
|
||||||
isIconOnly
|
variant="outline"
|
||||||
variant="bordered"
|
className="w-7 h-7 text-muted-foreground bg-card/50 backdrop-blur-xs dark:backdrop-blur-md shadow-lg"
|
||||||
className="text-font/90 border-1 border-divider"
|
onClick={copyToClipboard}
|
||||||
onPressEnd={copyToClipboard}
|
|
||||||
>
|
>
|
||||||
{
|
{
|
||||||
!isCopied ? <Copy className="size-4" /> : <CopyCheck className="size-4" />
|
!isCopied ? <Copy className="size-4" /> : <CopyCheck className="size-4" />
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { useTheme } from "@/providers/theme-provider/theme-provider";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { Spinner } from "./ui/shadcn-io/spinner";
|
||||||
|
|
||||||
|
interface CodeEditorProps {
|
||||||
|
defaultValue?: string;
|
||||||
|
value?: string;
|
||||||
|
className?: string | undefined,
|
||||||
|
readOnly?: boolean,
|
||||||
|
onChange?: (sql: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const CodeEditor: React.FC<CodeEditorProps> = ({
|
||||||
|
defaultValue,
|
||||||
|
value,
|
||||||
|
className,
|
||||||
|
readOnly = false,
|
||||||
|
onChange
|
||||||
|
}) => {
|
||||||
|
|
||||||
|
|
||||||
|
const [editor, setEditor] = useState<any>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
|
||||||
|
let mounted = true;
|
||||||
|
|
||||||
|
Promise.all([
|
||||||
|
import("@uiw/react-codemirror"),
|
||||||
|
import("@codemirror/lang-sql"),
|
||||||
|
import("@codemirror/view"),
|
||||||
|
]).then(([cm, sqlLang, view]) => {
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
const overrideDarkTheme = view.EditorView.theme({
|
||||||
|
|
||||||
|
'.cm-content': {
|
||||||
|
backgroundColor: "#1c2025",
|
||||||
|
},
|
||||||
|
".cm-gutter": {
|
||||||
|
backgroundColor: "#1c2025",
|
||||||
|
},
|
||||||
|
|
||||||
|
".cm-gutterElement": {
|
||||||
|
color: "#4b515a"
|
||||||
|
},
|
||||||
|
|
||||||
|
".ͼp": {
|
||||||
|
color: "#A994FF"
|
||||||
|
},
|
||||||
|
|
||||||
|
".cm-line .ͼq": {
|
||||||
|
color: "#ff6363"
|
||||||
|
},
|
||||||
|
".ͼu": {
|
||||||
|
color: "#B6E672"
|
||||||
|
},
|
||||||
|
".ͼv": {
|
||||||
|
color: "#6cdcc4"
|
||||||
|
}
|
||||||
|
}, { dark: true });
|
||||||
|
|
||||||
|
const overrideLightTheme = view.EditorView.theme({
|
||||||
|
|
||||||
|
".ͼb": {
|
||||||
|
color: "#2A1D66"
|
||||||
|
},
|
||||||
|
".cm-gutterElement": {
|
||||||
|
color: "#62748e"
|
||||||
|
},
|
||||||
|
".cm-gutter": {
|
||||||
|
backgroundColor: "white",
|
||||||
|
|
||||||
|
},
|
||||||
|
".cm-gutters": {
|
||||||
|
borderColor: "#f2f4f6"
|
||||||
|
},
|
||||||
|
".cm-line": {
|
||||||
|
color: "#0f172b"
|
||||||
|
},
|
||||||
|
|
||||||
|
})
|
||||||
|
setEditor({
|
||||||
|
CodeMirror: cm.default,
|
||||||
|
oneDark: cm.oneDark,
|
||||||
|
sql: sqlLang.sql,
|
||||||
|
overrideDarkTheme,
|
||||||
|
overrideLightTheme,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
mounted = false;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
|
const { theme } = useTheme();
|
||||||
|
|
||||||
|
if (!editor) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex flex-1 w-full min-h-9 rounded-sm border border-border bg-card items-center justify-center",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Spinner className="text-primary" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { CodeMirror, oneDark, sql, overrideDarkTheme, overrideLightTheme } = editor;
|
||||||
|
return (
|
||||||
|
<CodeMirror
|
||||||
|
defaultValue={defaultValue}
|
||||||
|
value={value}
|
||||||
|
className={cn("flex flex-1 w-full min-h-9 rounded-sm h-full bg-card border-1 border-border !min-w-0 overflow-hidden", className)}
|
||||||
|
extensions={[sql()]}
|
||||||
|
readOnly={readOnly}
|
||||||
|
theme={theme != "dark" ? overrideLightTheme : [oneDark, overrideDarkTheme]}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export default React.memo(CodeEditor);
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
import { colorOptions } from "@/lib/colors";
|
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 { 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 {
|
interface ColorPickerProps {
|
||||||
defaultColor?: string;
|
defaultColor?: string;
|
||||||
@@ -15,11 +16,12 @@ interface ColorPickerProps {
|
|||||||
const ColorPicker: React.FC<ColorPickerProps> = ({ defaultColor, onChange }) => {
|
const ColorPicker: React.FC<ColorPickerProps> = ({ defaultColor, onChange }) => {
|
||||||
const [color, setColor] = useState<string | undefined>(defaultColor as string);
|
const [color, setColor] = useState<string | undefined>(defaultColor as string);
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setColor(defaultColor) ;
|
setColor(defaultColor);
|
||||||
} , [defaultColor])
|
}, [defaultColor])
|
||||||
|
|
||||||
const onSelect = useCallback((color: string | undefined) => {
|
const onSelect = useCallback((color: string | undefined) => {
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
@@ -28,6 +30,44 @@ const ColorPicker: React.FC<ColorPickerProps> = ({ defaultColor, onChange }) =>
|
|||||||
}, [onChange])
|
}, [onChange])
|
||||||
|
|
||||||
return (
|
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">
|
<Popover placement="bottom" radius="sm" isOpen={isOpen} onOpenChange={(open) => setIsOpen(open)} shadow="sm">
|
||||||
<PopoverTrigger>
|
<PopoverTrigger>
|
||||||
<Button size="sm"
|
<Button size="sm"
|
||||||
@@ -41,42 +81,10 @@ const ColorPicker: React.FC<ColorPickerProps> = ({ defaultColor, onChange }) =>
|
|||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent>
|
<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>
|
</PopoverContent>
|
||||||
</Popover>
|
</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 {
|
interface EmptyListProps {
|
||||||
title?: string;
|
title?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const EmptyList: React.FC<EmptyListProps> = ({ title, description }) => {
|
const EmptyList: React.FC<EmptyListProps> = ({ title, description }) => {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center gap-1 h-full pt-[86px] main-linear-background rounded-lg">
|
<div className="flex flex-col items-center gap-1 h-full pt-[86px] main-linear-background rounded-lg">
|
||||||
{
|
{
|
||||||
<Image
|
<img
|
||||||
width={64}
|
width={64}
|
||||||
src={"/stackrender.png"}
|
src={"/stackrender.png"}
|
||||||
|
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
@@ -32,7 +21,6 @@ const EmptyList: React.FC<EmptyListProps> = ({ title, description }) => {
|
|||||||
</h1>
|
</h1>
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
|
|
||||||
description &&
|
description &&
|
||||||
<p className="text-sm text-font/70">
|
<p className="text-sm text-font/70">
|
||||||
{description}
|
{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' {...props} >
|
||||||
|
<SidebarHeader>
|
||||||
|
<div className='flex items-center gap-2'>
|
||||||
|
<img
|
||||||
|
className='w-8 p-[4px] 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 { useTranslation } from "react-i18next"
|
||||||
import { useTheme } from "next-themes";
|
import { MenuItem } from "../../types";
|
||||||
import DropdownMenu, { MenuDropdownProps } from "./menu-dropdown";
|
import { useMemo } from "react";
|
||||||
import React, { useEffect, useMemo } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { useModal } from "@/providers/modal-provider/modal-provider";
|
|
||||||
import { Modals } from "@/providers/modal-provider/modal-contxet";
|
import { Modals } from "@/providers/modal-provider/modal-contxet";
|
||||||
|
|
||||||
|
|
||||||
import { useDatabaseHistory } from "@/providers/database-history/database-history-provider";
|
import { useDatabaseHistory } from "@/providers/database-history/database-history-provider";
|
||||||
import { useDatabase, useDatabaseOperations } from "@/providers/database-provider/database-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 { useDiagramOps } from "@/providers/diagram-provider/diagram-provider";
|
||||||
|
import { TableType } from "@/lib/schemas/table-schema";
|
||||||
import { CardinalityStyle } from "@/lib/database";
|
import { CardinalityStyle } from "@/lib/database";
|
||||||
|
import { useTheme } from "@/providers/theme-provider/theme-provider";
|
||||||
|
|
||||||
|
export const useMenuData = () => {
|
||||||
const Menu: React.FC = ({ }) => {
|
|
||||||
|
|
||||||
const { setTheme, resolvedTheme } = useTheme()
|
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const { setTheme, theme } = useTheme()
|
||||||
|
|
||||||
const { open } = useModal();
|
const { open } = useModal();
|
||||||
const { canRedo, canUndo, undo, redo } = useDatabaseHistory();
|
const { canRedo, canUndo, undo, redo } = useDatabaseHistory();
|
||||||
const { deleteMultiTables } = useDatabaseOperations();
|
const { deleteMultiTables } = useDatabaseOperations();
|
||||||
const { database } = useDatabase();
|
const { database } = useDatabase();
|
||||||
const { openController, showController, cardinalityStyle, changeCardinalityStyle } = useDiagramOps();
|
const { openController, showController, cardinalityStyle, changeCardinalityStyle } = useDiagramOps();
|
||||||
|
|
||||||
const menu: MenuDropdownProps[] = useMemo(() => [
|
const menuData: MenuItem[] = useMemo(() => [
|
||||||
{
|
{
|
||||||
id: "menu.file",
|
id: "menu.file",
|
||||||
title: t("menu.file"),
|
title: t("menu.file"),
|
||||||
@@ -53,13 +53,15 @@ const Menu: React.FC = ({ }) => {
|
|||||||
id: "menu.export_sql",
|
id: "menu.export_sql",
|
||||||
title: t("menu.export_sql"),
|
title: t("menu.export_sql"),
|
||||||
clickHandler: () => {
|
clickHandler: () => {
|
||||||
|
|
||||||
|
|
||||||
open(Modals.EXPORT_SQL)
|
open(Modals.EXPORT_SQL)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
//{ title: t("menu.export_orm_models"), divide: true },
|
//{ title: t("menu.export_orm_models"), divide: true },
|
||||||
{
|
{
|
||||||
id: "menu.delete_project",
|
id: "menu.delete_project",
|
||||||
title: t("menu.delete_project"), theme: "danger", clickHandler: () => {
|
title: t("menu.delete_project"), theme: "destructive", clickHandler: () => {
|
||||||
open(Modals.DELETE_DATABASE)
|
open(Modals.DELETE_DATABASE)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -93,7 +95,7 @@ const Menu: React.FC = ({ }) => {
|
|||||||
{
|
{
|
||||||
id: "menu.symbolic",
|
id: "menu.symbolic",
|
||||||
selected: cardinalityStyle == CardinalityStyle.SYMBOLIC,
|
selected: cardinalityStyle == CardinalityStyle.SYMBOLIC,
|
||||||
title: t("menu.symbolic"), clickHandler: () => {
|
title: t("menu.symbolic"), clickHandler: () => {
|
||||||
changeCardinalityStyle(CardinalityStyle.SYMBOLIC)
|
changeCardinalityStyle(CardinalityStyle.SYMBOLIC)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -108,6 +110,8 @@ const Menu: React.FC = ({ }) => {
|
|||||||
id: "menu.hidden",
|
id: "menu.hidden",
|
||||||
selected: cardinalityStyle == CardinalityStyle.HIDDEN,
|
selected: cardinalityStyle == CardinalityStyle.HIDDEN,
|
||||||
title: t("menu.hidden"), clickHandler: () => {
|
title: t("menu.hidden"), clickHandler: () => {
|
||||||
|
|
||||||
|
|
||||||
changeCardinalityStyle(CardinalityStyle.HIDDEN)
|
changeCardinalityStyle(CardinalityStyle.HIDDEN)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -120,23 +124,21 @@ const Menu: React.FC = ({ }) => {
|
|||||||
openController(!showController)
|
openController(!showController)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
id: "menu.theme",
|
id: "menu.theme",
|
||||||
title: t("menu.theme"),
|
title: t("menu.theme"),
|
||||||
|
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
id: "light",
|
id: "light",
|
||||||
title: t("menu.light"),
|
title: t("menu.light"),
|
||||||
clickHandler: () => setTheme("light"),
|
clickHandler: () => setTheme("light"),
|
||||||
selected: resolvedTheme == "light",
|
selected: theme != "dark",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "dark",
|
id: "dark",
|
||||||
title: t("menu.dark"),
|
title: t("menu.dark"),
|
||||||
clickHandler: () => setTheme("dark"),
|
clickHandler: () => setTheme("dark"),
|
||||||
selected: resolvedTheme == "dark"
|
selected: theme == "dark"
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -146,23 +148,25 @@ const Menu: React.FC = ({ }) => {
|
|||||||
id: "menu.help",
|
id: "menu.help",
|
||||||
title: t("menu.help"),
|
title: t("menu.help"),
|
||||||
children: [
|
children: [
|
||||||
{ id: "menu.join_discord", title: t("menu.join_discord") ,
|
{
|
||||||
clickHandler : () => {
|
id: "menu.documentation", title: t("sidebar.documentation"),
|
||||||
window.open("https://discord.gg/DsN8RcPR6Y", "_blank")
|
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]);
|
|
||||||
|
], [t, canRedo, canUndo, undo, redo , deleteMultiTables, database, showController, openController, changeCardinalityStyle, cardinalityStyle, theme]);
|
||||||
return <div className="gap-1 flex">
|
|
||||||
{
|
|
||||||
menu.map((menuItem, index) => (
|
|
||||||
<DropdownMenu {...menuItem} key={menuItem.id} />
|
|
||||||
))
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
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,37 @@
|
|||||||
|
|
||||||
|
import { Outlet } from "react-router-dom";
|
||||||
|
import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar";
|
||||||
|
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 variant="inset" />
|
||||||
|
{/* dark:p-0 dark:border-l-1 */}
|
||||||
|
<SidebarInset className=" overflow-hidden max-h-screen flex flex-col flex-1 !m-0 !rounded-none bg-sidebar p-1 ">
|
||||||
|
{/*dark:border-none dark:rounded-none */}
|
||||||
|
<div className="border-4 border-sidebar-border ring-1 ring-border/30 dark:!border-background dark:ring-sidebar-border flex flex-col min-h-0 !overflow-hidden rounded-2xl h-screen">
|
||||||
|
<div className="overflow-hidden h-full flex flex-col flex-1 bg-white dark:bg-background dark:!border-1 dark:border-sidebar-border dark:rounded-[14px] !overflow-hidden" >
|
||||||
|
<Header className="bg-card border-b pl-3" />
|
||||||
|
{
|
||||||
|
children ? children : <Outlet />
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</SidebarInset>
|
||||||
|
|
||||||
|
</SidebarProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export default Dashboard;
|
||||||
+12
-15
@@ -1,5 +1,6 @@
|
|||||||
import { CardinalityStyle } from "@/lib/database";
|
import { CardinalityStyle } from "@/lib/database";
|
||||||
import { cn } from "@heroui/react";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
export interface CardinalityMarkerProps {
|
export interface CardinalityMarkerProps {
|
||||||
@@ -16,15 +17,12 @@ const CardinalityMarker: React.FC<CardinalityMarkerProps> = ({ selected = false,
|
|||||||
|
|
||||||
const id = `${cardinality}_${direction}${selected ? "_selected" : ""}`;
|
const id = `${cardinality}_${direction}${selected ? "_selected" : ""}`;
|
||||||
const renderMarker = () => {
|
const renderMarker = () => {
|
||||||
|
|
||||||
if (cardinality == "many") {
|
if (cardinality == "many") {
|
||||||
if (direction == "start")
|
if (direction == "start")
|
||||||
return (<path d="M 0 50 L 100 50 M 100 50 L 0 0 M 100 50 L 0 100 " />)
|
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")
|
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" />)
|
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 (cardinality == "one") {
|
||||||
if (direction == "start") {
|
if (direction == "start") {
|
||||||
return (<path d="M 0 50 L 100 50 M 50 50 M 50 50 M 75 25 L 75 75" />)
|
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" />)
|
return (<path d="M 100 50 L 0 50 M 50 50 M 50 50 M 25 25 L 25 75" />)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -52,8 +48,8 @@ const CardinalityMarker: React.FC<CardinalityMarkerProps> = ({ selected = false,
|
|||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
fill="transparent"
|
fill="transparent"
|
||||||
className={selected ? "stroke-primary" : "stroke-default-600 dark:stroke-default-400"}
|
className={selected ? ' !stroke-[oklch(0.60_0.028_263.3984)] dark:!stroke-primary-foreground' : 'stroke-[oklch(0.72_0.022_263.3984)] dark:!stroke-muted-foreground'}
|
||||||
strokeWidth="4"
|
strokeWidth="6"
|
||||||
width="24"
|
width="24"
|
||||||
height="24"
|
height="24"
|
||||||
viewBox="0 0 100 100">
|
viewBox="0 0 100 100">
|
||||||
@@ -78,23 +74,24 @@ const CardinalityMarker: React.FC<CardinalityMarkerProps> = ({ selected = false,
|
|||||||
<circle
|
<circle
|
||||||
cx="12"
|
cx="12"
|
||||||
cy="12"
|
cy="12"
|
||||||
r="8"
|
r="6"
|
||||||
strokeWidth="1"
|
strokeWidth="1"
|
||||||
className={
|
className={
|
||||||
cn("dark:fill-default fill-default",
|
cn(" fill-background ",
|
||||||
selected ? " stroke-primary fill-background dark:fill-primary-900" : " stroke-default-600 dark:stroke-default-400"
|
selected ? " stroke-[oklch(0.60_0.028_263.3984)] fill-background dark:!stroke-primary-foreground" :
|
||||||
|
" stroke-[oklch(0.72_0.022_263.3984)] dark:!stroke-muted-foreground "
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<text
|
<text
|
||||||
x="12"
|
x="12"
|
||||||
y="13"
|
y="12.5"
|
||||||
textAnchor="middle"
|
textAnchor="middle"
|
||||||
dominantBaseline="middle"
|
dominantBaseline="middle"
|
||||||
fontSize="8"
|
fontSize="6"
|
||||||
className={
|
className={
|
||||||
cn("fill-font/90 dark:fill-font/90 font-semibold" ,
|
cn("fill-[oklch(0.72_0.022_263.3984)] font-semibold dark:!fill-muted-foreground" ,
|
||||||
selected ? "fill-primary dark:fill-primary" : "" ,
|
selected ? "fill-[oklch(0.60_0.028_263.3984)] dark:!fill-primary-foreground" : "" ,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
+51
-65
@@ -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 { useDatabaseHistory } from "@/providers/database-history/database-history-provider";
|
||||||
import { Button, cn, Divider, Navbar } from "@heroui/react";
|
|
||||||
import { useOnViewportChange, useReactFlow } from "@xyflow/react";
|
import { useOnViewportChange, useReactFlow } from "@xyflow/react";
|
||||||
|
|
||||||
import { LayoutGrid, Redo, Scan, Undo, ZoomIn, ZoomOut } from "lucide-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";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
interface DbControlButtons {
|
interface DbControlButtons {
|
||||||
@@ -52,39 +57,30 @@ const DatabaseControlButtons: React.FC<DbControlButtons> = ({ adjustPositions })
|
|||||||
})
|
})
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Navbar className="flex rounded-md border-1 border-default-200 bg-background dark:bg-transparent bg-default dark:border-divider " isBlurred
|
<div className="flex !rounded-md p-2 border-1 !overflow-hidden !bg-input/50 dark:!bg-card/50 text-muted-foreground shadow-md backdrop-blur-xs dark:backdrop-blur-md">
|
||||||
classNames={{
|
|
||||||
wrapper: "h-14 p-2 gap-1",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<span>
|
<span>
|
||||||
<Button
|
<Button
|
||||||
size="md"
|
size={"icon"}
|
||||||
isIconOnly
|
onClick={undo}
|
||||||
variant="light"
|
|
||||||
onPressEnd={undo}
|
|
||||||
radius="sm"
|
|
||||||
disabled={!canUndo}
|
disabled={!canUndo}
|
||||||
|
|
||||||
|
|
||||||
|
variant={"ghost"}
|
||||||
|
|
||||||
>
|
>
|
||||||
<Undo className={cn(
|
<Undo className={cn(
|
||||||
"size-4 dark:text-white",
|
"size-4"
|
||||||
!canUndo ? "text-font/30 dark:text-font/30" : ""
|
|
||||||
)} />
|
)} />
|
||||||
</Button>
|
</Button>
|
||||||
</span>
|
</span>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
{t("control_buttons.undo")}
|
{t("control_buttons.undo")}
|
||||||
<span className="ml-2 text-default-400">
|
<span className="ml-2">
|
||||||
Cntl + Z
|
Cntl + Z
|
||||||
</span>
|
</span>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
@@ -94,12 +90,11 @@ const DatabaseControlButtons: React.FC<DbControlButtons> = ({ adjustPositions })
|
|||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<span>
|
<span>
|
||||||
<Button
|
<Button
|
||||||
size="md"
|
size={"icon"}
|
||||||
isIconOnly
|
onClick={adjustPositions}
|
||||||
variant="light"
|
variant={"ghost"}
|
||||||
radius="sm"
|
>
|
||||||
onPressEnd={adjustPositions}>
|
<LayoutGrid className="size-4" />
|
||||||
<LayoutGrid className="size-4 dark:text-white" />
|
|
||||||
</Button>
|
</Button>
|
||||||
</span>
|
</span>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
@@ -108,18 +103,17 @@ const DatabaseControlButtons: React.FC<DbControlButtons> = ({ adjustPositions })
|
|||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Divider orientation="vertical" className="bg-font/10" />
|
<Separator orientation="vertical" />
|
||||||
|
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<span>
|
<span>
|
||||||
<Button
|
<Button
|
||||||
size="md"
|
size="icon"
|
||||||
isIconOnly
|
onClick={onZoomOut}
|
||||||
variant="light"
|
variant={"ghost"}
|
||||||
radius="sm"
|
>
|
||||||
onPressEnd={onZoomOut}>
|
<ZoomOut className="size-4" />
|
||||||
<ZoomOut className="size-4 dark:text-white" />
|
|
||||||
</Button>
|
</Button>
|
||||||
</span>
|
</span>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
@@ -129,11 +123,10 @@ const DatabaseControlButtons: React.FC<DbControlButtons> = ({ adjustPositions })
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
size="md"
|
size="icon"
|
||||||
isIconOnly
|
variant={"ghost"}
|
||||||
variant="light"
|
onClick={resetZoom}
|
||||||
onPressEnd={resetZoom}
|
className="w-[60px] p-2 "
|
||||||
className="w-[60px] p-2 hover:bg-primary-foreground dark:text-white"
|
|
||||||
>
|
>
|
||||||
{zoom}
|
{zoom}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -141,12 +134,11 @@ const DatabaseControlButtons: React.FC<DbControlButtons> = ({ adjustPositions })
|
|||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<span>
|
<span>
|
||||||
<Button
|
<Button
|
||||||
size="md"
|
size="icon"
|
||||||
isIconOnly
|
onClick={onZoomIn}
|
||||||
variant="light"
|
variant={"ghost"}
|
||||||
radius="sm"
|
>
|
||||||
onPressEnd={onZoomIn}>
|
<ZoomIn className="size-4" />
|
||||||
<ZoomIn className="size-4 dark:text-white" />
|
|
||||||
</Button>
|
</Button>
|
||||||
</span>
|
</span>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
@@ -155,25 +147,23 @@ const DatabaseControlButtons: React.FC<DbControlButtons> = ({ adjustPositions })
|
|||||||
{t("control_buttons.zoom_in")}
|
{t("control_buttons.zoom_in")}
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Divider orientation="vertical" className="bg-font/10" />
|
<Separator orientation="vertical" />
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<span>
|
<span>
|
||||||
<Button
|
<Button
|
||||||
size="md"
|
size="icon"
|
||||||
isIconOnly
|
onClick={onFitView}
|
||||||
variant="light"
|
variant={"ghost"}
|
||||||
radius="sm"
|
|
||||||
onPressEnd={onFitView}
|
|
||||||
>
|
>
|
||||||
<Scan className="size-4 dark:text-white" />
|
<Scan className="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</span>
|
</span>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
{t("control_buttons.show_all")}
|
{t("control_buttons.show_all")}
|
||||||
|
|
||||||
<span className="ml-2 text-default-400">
|
<span className="ml-2">
|
||||||
Cntl + A
|
Cntl + A
|
||||||
</span>
|
</span>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
@@ -182,30 +172,26 @@ const DatabaseControlButtons: React.FC<DbControlButtons> = ({ adjustPositions })
|
|||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<span>
|
<span>
|
||||||
<Button
|
<Button
|
||||||
size="md"
|
size="icon"
|
||||||
isIconOnly
|
variant={"ghost"}
|
||||||
variant="light"
|
onClick={redo}
|
||||||
radius="sm"
|
|
||||||
onPressEnd={redo}
|
|
||||||
disabled={!canRedo}
|
disabled={!canRedo}
|
||||||
>
|
>
|
||||||
<Redo className={cn(
|
<Redo className={cn("size-4")} />
|
||||||
"size-4 dark:text-white",
|
|
||||||
!canRedo ? "text-font/30 dark:text-font/30" : ""
|
|
||||||
)} />
|
|
||||||
</Button>
|
</Button>
|
||||||
</span>
|
</span>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
{t("control_buttons.redo")}
|
{t("control_buttons.redo")}
|
||||||
<span className="ml-2 text-default-400">
|
<span className="ml-2 ">
|
||||||
Ctnl + Shift + Z
|
Ctnl + Shift + Z
|
||||||
</span>
|
</span>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Navbar>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export default DatabaseControlButtons;
|
export default DatabaseControlButtons;
|
||||||
|
|
||||||
+93
-64
@@ -1,6 +1,6 @@
|
|||||||
// Importing necessary types and hooks from React Flow (XYFlow)
|
// Importing necessary types and hooks from React Flow (XYFlow)
|
||||||
import {
|
import {
|
||||||
addEdge, Background, ColorMode, Connection, Controls, EdgeChange,
|
Background, ColorMode, Connection, Controls, EdgeChange,
|
||||||
EdgeRemoveChange, MiniMap, NodeChange, NodePositionChange,
|
EdgeRemoveChange, MiniMap, NodeChange, NodePositionChange,
|
||||||
NodeRemoveChange, OnEdgesChange, OnNodesChange,
|
NodeRemoveChange, OnEdgesChange, OnNodesChange,
|
||||||
ReactFlow, useEdgesState, useNodesState, useReactFlow
|
ReactFlow, useEdgesState, useNodesState, useReactFlow
|
||||||
@@ -10,51 +10,60 @@ import {
|
|||||||
import { useCallback, useEffect, useMemo } from "react";
|
import { useCallback, useEffect, useMemo } from "react";
|
||||||
|
|
||||||
// Custom components and styles
|
// Custom components and styles
|
||||||
import Table from "./table/table";
|
import Table from "./table";
|
||||||
|
|
||||||
import '@xyflow/react/dist/style.css';
|
import '@xyflow/react/dist/style.css';
|
||||||
import Relationship from "./table/relationship";
|
import Relationship from "./relationship";
|
||||||
import DBController from "./db-controller/db-controller";
|
|
||||||
import { TableInsertType } from "@/lib/schemas/table-schema";
|
import { TableInsertType } from "@/lib/schemas/table-schema";
|
||||||
|
|
||||||
// Custom context providers and hooks
|
// Custom context providers and hooks
|
||||||
import { useDatabase, useDatabaseOperations } from "@/providers/database-provider/database-provider";
|
import { useDatabase, useDatabaseOperations } from "@/providers/database-provider/database-provider";
|
||||||
import { useTableToNode } from "@/hooks/use-table-to-node";
|
import { useTableToNode } from "@/hooks/use-table-to-node";
|
||||||
import { useRelationshipToEdge } from "@/hooks/use-relationship-to-edge";
|
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
|
// Utils and constants
|
||||||
import { v4 } from "uuid";
|
import { v4 } from "uuid";
|
||||||
import { TARGET_PREFIX } from "./table/field";
|
import { TARGET_PREFIX } from "./field";
|
||||||
import CardinalityMarker from "@/components/cardinality-marker/cardinality-marker";
|
import CardinalityMarker from "@/features/database/components/cardinality-marker";
|
||||||
import { useDiagramOps } from "@/providers/diagram-provider/diagram-provider";
|
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 { AlertTriangle, Menu } from "lucide-react";
|
||||||
import { adjustTablesPositions, isTablesOverlapping } from "@/utils/tables";
|
import { adjustTablesPositions } from "@/utils/tables";
|
||||||
import DatabaseControlButtons from "./database-control-buttons";
|
//import DatabaseControlButtons from "./database-control-buttons";
|
||||||
import { FieldType } from "@/lib/schemas/field-schema";
|
import { FieldType } from "@/lib/schemas/field-schema";
|
||||||
import useHighlightedEdges from "@/hooks/use-highlighted-edges";
|
import useHighlightedEdges from "@/hooks/use-highlighted-edges";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import useOverlappingTables from "@/hooks/use-overlapping-tables";
|
import useOverlappingTables from "@/hooks/use-overlapping-tables";
|
||||||
import { useTheme } from "next-themes";
|
|
||||||
|
|
||||||
import { getRelationshipSourceAndTarget } from "@/utils/relationship";
|
import { getRelationshipSourceAndTarget } from "@/utils/relationship";
|
||||||
import { useModal } from "@/providers/modal-provider/modal-provider";
|
import { Loading } from "@/components/layout/loading-modal";
|
||||||
import { Modals } from "@/providers/modal-provider/modal-contxet";
|
|
||||||
import { Loading } from "@/components/modal/loading-modal";
|
|
||||||
import { usePowerSync } from "@powersync/react";
|
|
||||||
import { CardinalityStyle } from "@/lib/database";
|
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";
|
||||||
|
import useToast from "@/hooks/use-toast";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const DatabasePage: React.FC = () => {
|
const DatabaseDiagram: React.FC = () => {
|
||||||
const { open } = useModal();
|
const { open } = useModal();
|
||||||
|
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { resolvedTheme } = useTheme();
|
const { theme: resolvedTheme } = useTheme();
|
||||||
// Extract database state and operations
|
// 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();
|
const { updateTablePositions, deleteMultiTables, deleteMultiRelationships, createRelationship } = useDatabaseOperations();
|
||||||
|
|
||||||
@@ -63,8 +72,8 @@ const DatabasePage: React.FC = () => {
|
|||||||
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
||||||
|
|
||||||
// Diagram-related state (e.g. connection in progress)
|
// Diagram-related state (e.g. connection in progress)
|
||||||
const { setIsConnectionInProgress, cardinalityStyle } = useDiagramOps();
|
const { setIsConnectionInProgress, cardinalityStyle, showController, openController } = useDiagramOps();
|
||||||
|
const raise = useToast() ;
|
||||||
// Destructure tables and relationships from database
|
// Destructure tables and relationships from database
|
||||||
const { tables, relationships } = database || { tables: [], relationships: [] };
|
const { tables, relationships } = database || { tables: [], relationships: [] };
|
||||||
|
|
||||||
@@ -75,17 +84,18 @@ const DatabasePage: React.FC = () => {
|
|||||||
const nodeTypes = useMemo(() => ({ table: Table }), []);
|
const nodeTypes = useMemo(() => ({ table: Table }), []);
|
||||||
const edgeTypes = useMemo(() => ({ 'relationship-edge': Relationship }), []);
|
const edgeTypes = useMemo(() => ({ 'relationship-edge': Relationship }), []);
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isLoading)
|
if (isLoading)
|
||||||
return;
|
return;
|
||||||
// no database selected , open (open database) Modal
|
// no database selected , open (open database) Modal
|
||||||
if (!database && databases?.length > 0) {
|
if (!database && databases?.length > 0) {
|
||||||
|
|
||||||
open(Modals.OPEN_DATABASE, {
|
open(Modals.OPEN_DATABASE, {
|
||||||
closable: false
|
closable: false
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
else if (databases.length == 0) {
|
else if (databases.length == 0) {
|
||||||
// there is no databases in the first place to select from ,
|
// there is no databases in the first place to select from ,
|
||||||
// we have to create a new one .
|
// we have to create a new one .
|
||||||
open(Modals.CREATE_DATABASE, {
|
open(Modals.CREATE_DATABASE, {
|
||||||
@@ -99,9 +109,17 @@ const DatabasePage: React.FC = () => {
|
|||||||
}, [database?.id, isLoading])
|
}, [database?.id, isLoading])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setNodes([]);
|
if (isSwitchingDatabase) {
|
||||||
setEdges([]);
|
setNodes([]);
|
||||||
}, [currentDatabaseId])
|
setEdges([]);
|
||||||
|
} else {
|
||||||
|
fitView({
|
||||||
|
duration: 500
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}, [isSwitchingDatabase, database?.id])
|
||||||
|
|
||||||
// Called when a connection is made between fields
|
// Called when a connection is made between fields
|
||||||
const onConnect = useCallback(async (connection: Connection) => {
|
const onConnect = useCallback(async (connection: Connection) => {
|
||||||
@@ -122,16 +140,17 @@ const DatabasePage: React.FC = () => {
|
|||||||
targetTableId,
|
targetTableId,
|
||||||
sourceFieldId,
|
sourceFieldId,
|
||||||
targetFieldId,
|
targetFieldId,
|
||||||
|
|
||||||
} as RelationshipInsertType);
|
} as RelationshipInsertType);
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// Show error toast if invalid relationship
|
|
||||||
addToast({
|
|
||||||
title: t("db_controller.invalid_relationship.title"),
|
raise(
|
||||||
description: t("db_controller.invalid_relationship.description"),
|
t("db_controller.invalid_relationship.title"),
|
||||||
color: "danger",
|
t("db_controller.invalid_relationship.description"),
|
||||||
variant: "solid"
|
"ERROR"
|
||||||
});
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsConnectionInProgress(false);
|
setIsConnectionInProgress(false);
|
||||||
@@ -147,28 +166,30 @@ const DatabasePage: React.FC = () => {
|
|||||||
const nodeRemoveChanges: NodeRemoveChange[] = changes.filter((change: NodeChange) => change.type == "remove");
|
const nodeRemoveChanges: NodeRemoveChange[] = changes.filter((change: NodeChange) => change.type == "remove");
|
||||||
// Save new positions to the database
|
// Save new positions to the database
|
||||||
if (nodePositionChanges.length > 0)
|
if (nodePositionChanges.length > 0)
|
||||||
|
|
||||||
updateTablePositions(nodePositionChanges.map((change: NodePositionChange) => ({
|
updateTablePositions(nodePositionChanges.map((change: NodePositionChange) => ({
|
||||||
id: change.id,
|
id: change.id,
|
||||||
posX: change.position?.x,
|
posX: change.position?.x,
|
||||||
posY: change.position?.y
|
posY: change.position?.y
|
||||||
} as TableInsertType)));
|
} as TableInsertType)));
|
||||||
|
|
||||||
|
|
||||||
// Delete tables if removed
|
// Delete tables if removed
|
||||||
if (nodeRemoveChanges.length > 0) {
|
if (nodeRemoveChanges.length > 0 && !isSwitchingDatabase) {
|
||||||
deleteMultiTables(nodeRemoveChanges.map((change: NodeRemoveChange) => change.id));
|
deleteMultiTables(nodeRemoveChanges.map((change: NodeRemoveChange) => change.id));
|
||||||
}
|
}
|
||||||
return onNodesChange(changes);
|
return onNodesChange(changes);
|
||||||
}, [onNodesChange, database]);
|
}, [onNodesChange, database, isSwitchingDatabase]);
|
||||||
|
|
||||||
// Called when edges (relationships) change
|
// Called when edges (relationships) change
|
||||||
const handleEdgeChanges: OnEdgesChange<any> = useCallback((changes: EdgeChange<any>[]) => {
|
const handleEdgeChanges: OnEdgesChange<any> = useCallback((changes: EdgeChange<any>[]) => {
|
||||||
const edgeRemoveChanges: EdgeRemoveChange[] = changes.filter((change: EdgeChange) => change.type == "remove") as EdgeRemoveChange[];
|
const edgeRemoveChanges: EdgeRemoveChange[] = changes.filter((change: EdgeChange) => change.type == "remove") as EdgeRemoveChange[];
|
||||||
// Delete relationships from database
|
// Delete relationships from database
|
||||||
if (edgeRemoveChanges.length > 0) {
|
if (edgeRemoveChanges.length > 0 && !isSwitchingDatabase) {
|
||||||
deleteMultiRelationships(edgeRemoveChanges.map((change: EdgeRemoveChange) => change.id));
|
deleteMultiRelationships(edgeRemoveChanges.map((change: EdgeRemoveChange) => change.id));
|
||||||
}
|
}
|
||||||
return onEdgesChange(changes as EdgeChange<never>[]);
|
return onEdgesChange(changes as EdgeChange<never>[]);
|
||||||
}, [onEdgesChange]);
|
}, [onEdgesChange, isSwitchingDatabase]);
|
||||||
|
|
||||||
// When user starts connecting fields
|
// When user starts connecting fields
|
||||||
const onConnectStart = useCallback(() => {
|
const onConnectStart = useCallback(() => {
|
||||||
@@ -199,18 +220,15 @@ const DatabasePage: React.FC = () => {
|
|||||||
useHighlightedEdges(nodes, relationships, edges);
|
useHighlightedEdges(nodes, relationships, edges);
|
||||||
const { isOverlapping, puls } = useOverlappingTables(tables);
|
const { isOverlapping, puls } = useOverlappingTables(tables);
|
||||||
|
|
||||||
|
const isMobile = useIsMobile();
|
||||||
return (
|
return (
|
||||||
|
|
||||||
<div className="w-full h-screen flex relative overflow-hidden">
|
<div className="w-full h-full flex relative overflow-hidden">
|
||||||
{
|
{
|
||||||
(isSwitchingDatabase || isLoading) && <Loading />
|
(isSwitchingDatabase || isLoading) && <Loading />
|
||||||
}
|
}
|
||||||
<div className="flex max-w-full">
|
|
||||||
<DBController />
|
|
||||||
</div>
|
|
||||||
{
|
|
||||||
|
|
||||||
|
{
|
||||||
<div className="relative w-full h-full ">
|
<div className="relative w-full h-full ">
|
||||||
<ReactFlow
|
<ReactFlow
|
||||||
colorMode={resolvedTheme as ColorMode}
|
colorMode={resolvedTheme as ColorMode}
|
||||||
@@ -236,33 +254,36 @@ const DatabasePage: React.FC = () => {
|
|||||||
|
|
||||||
>
|
>
|
||||||
<Controls
|
<Controls
|
||||||
position="bottom-center"
|
position={isMobile ? "top-center" : "bottom-center"}
|
||||||
showFitView={false}
|
showFitView={false}
|
||||||
showZoom={false}
|
showZoom={false}
|
||||||
showInteractive={false}
|
showInteractive={false}
|
||||||
className="shadow-none">
|
className="!shadow-none !border-none">
|
||||||
|
|
||||||
<DatabaseControlButtons
|
<DatabaseControlButtons
|
||||||
adjustPositions={adjustPositions}
|
adjustPositions={adjustPositions}
|
||||||
/>
|
/>
|
||||||
</Controls >
|
</Controls >
|
||||||
<MiniMap
|
<MiniMap
|
||||||
nodeStrokeWidth={4}
|
nodeStrokeWidth={4}
|
||||||
className="dark:bg-content1 bg-background border-1 border-default-200 dark:border-divider rounded-md "
|
className="!bg-input/50 ring-1 ring-border shadow-sm rounded-md overflow-hidden shadow-md backdrop-blur-xs dark:!bg-background/50 "
|
||||||
maskStrokeColor={resolvedTheme == "dark" ? "#272c35" : "#e9edf1"}
|
maskStrokeColor={resolvedTheme == "dark" ? "#FFFFFF1A" : "#cad5e2"}
|
||||||
maskColor={resolvedTheme == "dark" ? "#20252c99" : "#f2f4f733"}
|
maskColor={resolvedTheme == "dark" ? "#21262d88" : "#f1f5f9cc"}
|
||||||
maskStrokeWidth={1}
|
maskStrokeWidth={1}
|
||||||
nodeClassName={"fill-default-300 dark:fill-font/30"}
|
|
||||||
|
nodeClassName={"!fill-foreground/20 "}
|
||||||
style={{
|
style={{
|
||||||
width: 164,
|
width: 148,
|
||||||
height: 128
|
height: 128
|
||||||
}}
|
}}
|
||||||
|
|
||||||
/>
|
/>
|
||||||
<Background className="dark:bg-background-100 " />
|
<Background color="#62748e" className="!bg-background dark:!bg-background" />
|
||||||
|
|
||||||
</ReactFlow>
|
</ReactFlow>
|
||||||
<div
|
<div
|
||||||
className="absolute left-[24px] bottom-[24px] "
|
className={cn("absolute left-[24px] ", {
|
||||||
|
"bottom-[24px]": !isMobile,
|
||||||
|
"top-[24px]": isMobile
|
||||||
|
})}
|
||||||
>
|
>
|
||||||
{
|
{
|
||||||
isOverlapping &&
|
isOverlapping &&
|
||||||
@@ -270,22 +291,30 @@ const DatabasePage: React.FC = () => {
|
|||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<span>
|
<span>
|
||||||
<Button
|
<Button
|
||||||
variant="shadow"
|
variant="destructive"
|
||||||
size="sm"
|
size="icon"
|
||||||
isIconOnly
|
className="shadow-lg size-8"
|
||||||
color="danger"
|
onClick={puls}>
|
||||||
className="size-8 p-1 "
|
|
||||||
onPressEnd={puls}>
|
|
||||||
<AlertTriangle className="size-4 text-white" />
|
<AlertTriangle className="size-4 text-white" />
|
||||||
</Button>
|
</Button>
|
||||||
</span>
|
</span>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent className="bg-destructive fill-destructive ">
|
||||||
{t("table.overlapping_tables")}
|
{t("table.overlapping_tables")}
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
{
|
||||||
|
isMobile &&
|
||||||
|
<Button size={"icon"} className="absolute bottom-[24px] left-[24px]"
|
||||||
|
onClick={() => {
|
||||||
|
openController(!showController)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Menu className="size-5" />
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -314,7 +343,7 @@ const DatabasePage: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export default DatabasePage;
|
export default DatabaseDiagram;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
+24
-26
@@ -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 { RelationshipType } from "@/lib/schemas/relationship-schema";
|
||||||
import { useDatabase, useDatabaseOperations } from "@/providers/database-provider/database-provider";
|
import { useDatabase, useDatabaseOperations } from "@/providers/database-provider/database-provider";
|
||||||
import { useDiagramOps } from "@/providers/diagram-provider/diagram-provider";
|
import { useDiagramOps } from "@/providers/diagram-provider/diagram-provider";
|
||||||
import { CircularDependencyError } from "@/utils/render/render-uttils";
|
import { CircularDependencyError } from "@/utils/render/render-uttils";
|
||||||
import { addToast, Alert, Button, Listbox, ListboxItem, toast } from "@heroui/react";
|
|
||||||
import { AlertTriangle, Trash } from "lucide-react";
|
import { AlertTriangle, Trash } from "lucide-react";
|
||||||
import React, { useCallback, useEffect, useMemo } from "react";
|
import React, { useCallback, useMemo } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
interface CircularDependencyAlertProps {
|
interface CircularDependencyAlertProps {
|
||||||
@@ -51,43 +52,40 @@ const CircularDependencyAlert: React.FC<CircularDependencyAlertProps> = ({ error
|
|||||||
deleteRelationship(id)
|
deleteRelationship(id)
|
||||||
}, [])
|
}, [])
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-2 w-full h-full items-center pt-12 ">
|
<div className="flex flex-col gap-2 w-full h-full items-center pt-12 ">
|
||||||
<AlertTriangle
|
<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")}
|
{t("db_controller.circular_dependency.title")}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-sm text-font/70 text-center w-[80%] max-w-[360px]">
|
<p className="text-sm text-muted-foreground 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>
|
{t("db_controller.circular_dependency.description")} , <span className="font-medium text-foreground"> {t("db_controller.circular_dependency.suggestion")} </span>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<Listbox aria-label="Relationships" className="w-[80%] max-w-[360px]" onAction={focus}
|
<ul aria-label="Relationships" className="w-[80%] max-w-[360px] mt-4"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
>
|
>
|
||||||
{
|
{
|
||||||
circularRelationships.map((relationship: RelationshipType) => (
|
circularRelationships.map((relationship: RelationshipType) => (
|
||||||
<ListboxItem
|
<li
|
||||||
variant={"faded"}
|
onClick={() => focus(relationship.id)}
|
||||||
className="data-[hover=true]:bg-default-500"
|
|
||||||
key={relationship.id}>
|
key={relationship.id}>
|
||||||
<div className="flex items-center justify-between tex-font/90">
|
<div className="hover:bg-secondary flex items-center justify-between h-10 mb-2 px-3 rounded-md cursor-pointer bg-card border-1">
|
||||||
<span>
|
<span className="text-sm">
|
||||||
{relationship.sourceTable.name} -> {relationship.targetTable.name}
|
{relationship.sourceTable.name} -> {relationship.targetTable.name}
|
||||||
</span>
|
</span>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<span>
|
<span>
|
||||||
<Button
|
<Button
|
||||||
isIconOnly
|
|
||||||
variant="light"
|
|
||||||
color={"danger"}
|
variant={"outline"}
|
||||||
size="sm"
|
size="icon"
|
||||||
onPress={() => removeRelationship(relationship.id)}
|
onClick={() => removeRelationship(relationship.id)}
|
||||||
|
className="shadow-sm size-7"
|
||||||
>
|
>
|
||||||
<Trash className="text-danger size-4" />
|
<Trash className="text-destructive size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</span>
|
</span>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
@@ -96,10 +94,10 @@ const CircularDependencyAlert: React.FC<CircularDependencyAlertProps> = ({ error
|
|||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</ListboxItem>
|
</li>
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
</Listbox>
|
</ul>
|
||||||
|
|
||||||
</div>
|
</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);
|
||||||
|
});
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
|
||||||
|
import { useDatabase, useDatabaseOperations } from "@/providers/database-provider/database-provider";
|
||||||
|
import React, { useEffect, useMemo, useState } from "react";
|
||||||
|
|
||||||
|
import { DatabaseType } from "@/lib/schemas/database-schema";
|
||||||
|
import CircularDependencyAlert from "./circular-dependecy-alert";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import Clipboard from "@/components/clipboard";
|
||||||
|
import { TableType } from "@/lib/schemas/table-schema";
|
||||||
|
import { RelationshipType } from "@/lib/schemas/relationship-schema";
|
||||||
|
|
||||||
|
import BaseDatabaseRenderer from "@/utils/render/database/base-database-renderer";
|
||||||
|
import { CircularDependencyError, getRenderer } from "@/utils/render/render-uttils";
|
||||||
|
import { areArraysEqual } from "@/utils/utils";
|
||||||
|
import CodeEditor from "@/components/code-editor";
|
||||||
|
import useToast from "@/hooks/use-toast";
|
||||||
|
|
||||||
|
|
||||||
|
interface SqlPreviewProps {
|
||||||
|
tableFilterIds?: string[] ,
|
||||||
|
className? : string ;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SqlPreview: React.FC<SqlPreviewProps> = ({ tableFilterIds , className}) => {
|
||||||
|
|
||||||
|
|
||||||
|
const { database: currentDatabase } = useDatabase();
|
||||||
|
const { data_types } = useDatabaseOperations();
|
||||||
|
const [sqlCode, setSqlCode] = useState<string>("");
|
||||||
|
const raise = useToast();
|
||||||
|
const [circularDependency, setCircularDependency] = useState<CircularDependencyError | undefined>(undefined);
|
||||||
|
|
||||||
|
const database = useMemo(() => {
|
||||||
|
if (!tableFilterIds)
|
||||||
|
return currentDatabase;
|
||||||
|
return {
|
||||||
|
...currentDatabase,
|
||||||
|
tables: currentDatabase?.tables.filter((table: TableType) => tableFilterIds?.includes(table.id)),
|
||||||
|
relationships: currentDatabase?.relationships.filter((relationship: RelationshipType) =>
|
||||||
|
tableFilterIds?.includes(relationship.sourceTableId) || tableFilterIds?.includes(relationship.targetFieldId)
|
||||||
|
),
|
||||||
|
} as DatabaseType;
|
||||||
|
}, [currentDatabase, tableFilterIds])
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
|
||||||
|
if (database?.dialect && data_types.length > 0) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
const renderer: BaseDatabaseRenderer = getRenderer(database.dialect, data_types);
|
||||||
|
const sql: string = await renderer.renderDDL(database)
|
||||||
|
|
||||||
|
setSqlCode(sql);
|
||||||
|
setCircularDependency(undefined);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
|
||||||
|
if ((error as CircularDependencyError)?.cycle)
|
||||||
|
setCircularDependency((previousError) => {
|
||||||
|
if (!previousError)
|
||||||
|
return error as CircularDependencyError;
|
||||||
|
else if (Array.isArray(previousError.cycle) && Array.isArray((error as CircularDependencyError).cycle) && !(areArraysEqual(previousError.cycle, (error as CircularDependencyError).cycle)))
|
||||||
|
|
||||||
|
return error as CircularDependencyError;
|
||||||
|
return previousError;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
})()
|
||||||
|
}, [database, data_types]);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (circularDependency)
|
||||||
|
raise(
|
||||||
|
t("db_controller.circular_dependency.title"),
|
||||||
|
t("db_controller.circular_dependency.description"),
|
||||||
|
"ERROR"
|
||||||
|
);
|
||||||
|
|
||||||
|
}, [circularDependency]);
|
||||||
|
|
||||||
|
|
||||||
|
if (circularDependency)
|
||||||
|
return <CircularDependencyAlert error={circularDependency} />
|
||||||
|
|
||||||
|
else
|
||||||
|
return (
|
||||||
|
<div className="flex w-full h-full relative min-w-0 !min-h-0 ">
|
||||||
|
<div className="absolute right-2 top-2 z-1">
|
||||||
|
<Clipboard
|
||||||
|
text={sqlCode}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<CodeEditor
|
||||||
|
defaultValue={sqlCode}
|
||||||
|
value={sqlCode}
|
||||||
|
readOnly
|
||||||
|
className={ className}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export default React.memo(SqlPreview);
|
||||||
@@ -0,0 +1,228 @@
|
|||||||
|
|
||||||
|
|
||||||
|
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 ">
|
||||||
|
<SqlPreview
|
||||||
|
tableFilterIds={tableFilterIds}
|
||||||
|
className="rounded-none border-0 border-t-1"
|
||||||
|
/>
|
||||||
|
</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
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user