mirror of
https://github.com/neosubhamoy/neodlp.git
synced 2026-02-05 06:22:23 +05:30
Compare commits
33 Commits
16
.github/banner.svg
vendored
Normal file
16
.github/banner.svg
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<svg width="600" height="130" viewBox="0 0 600 130" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g clip-path="url(#clip0_3_2)">
|
||||||
|
<path d="M86.9062 11H21.0938C9.44399 11 0 20.444 0 32.0938V97.9062C0 109.556 9.44399 119 21.0938 119H86.9062C98.556 119 108 109.556 108 97.9062V32.0938C108 20.444 98.556 11 86.9062 11Z" fill="url(#paint0_linear_3_2)"/>
|
||||||
|
<path d="M55.8196 96.5455C54.7881 97.5856 53.1065 97.5856 52.075 96.5455L27.028 71.2863C25.3778 69.6221 26.5566 66.793 28.9002 66.793H78.9943C81.3379 66.793 82.5168 69.6221 80.8666 71.2863L55.8196 96.5455Z" fill="#FAFAFA"/>
|
||||||
|
<path d="M67.8164 34.4141H40.0781C38.6219 34.4141 37.4414 35.5946 37.4414 37.0508V68.2695C37.4414 69.7257 38.6219 70.9062 40.0781 70.9062H67.8164C69.2726 70.9062 70.4531 69.7257 70.4531 68.2695V37.0508C70.4531 35.5946 69.2726 34.4141 67.8164 34.4141Z" fill="#FAFAFA"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="paint0_linear_3_2" x1="13.6582" y1="26.6621" x2="97.1367" y2="102.02" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#4444FF"/>
|
||||||
|
<stop offset="1" stop-color="#FF43D0"/>
|
||||||
|
</linearGradient>
|
||||||
|
<clipPath id="clip0_3_2">
|
||||||
|
<rect width="108" height="108" fill="white" transform="translate(0 11)"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
138
.github/mockup.svg
vendored
Normal file
138
.github/mockup.svg
vendored
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 541 KiB |
111
.github/workflows/publish-to-aur.yml
vendored
Normal file
111
.github/workflows/publish-to-aur.yml
vendored
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [published]
|
||||||
|
|
||||||
|
name: 🚀 Publish to AUR
|
||||||
|
jobs:
|
||||||
|
update-aur:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: archlinux:base-devel
|
||||||
|
options: --privileged
|
||||||
|
steps:
|
||||||
|
- name: 🚚 Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: 📦 Install dependencies
|
||||||
|
run: |
|
||||||
|
# Install base packages needed
|
||||||
|
pacman -Syu --noconfirm --needed git openssh jq curl
|
||||||
|
|
||||||
|
- name: 🔍 Fetch release information
|
||||||
|
id: release_info
|
||||||
|
run: |
|
||||||
|
# Get latest release version and tag
|
||||||
|
RELEASE_TAG="${{ github.event.release.tag_name }}"
|
||||||
|
if [ -z "$RELEASE_TAG" ]; then
|
||||||
|
# If manually triggered, fetch latest release
|
||||||
|
RELEASE_TAG=$(curl -s "https://api.github.com/repos/${{ github.repository }}/releases/latest" | jq -r '.tag_name')
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Extract version number from tag
|
||||||
|
VERSION=$(echo "$RELEASE_TAG" | sed -E 's/^v([0-9]+\.[0-9]+\.[0-9]+)(-.*)?$/\1/')
|
||||||
|
SUFFIX=$(echo "$RELEASE_TAG" | sed -E 's/^v[0-9]+\.[0-9]+\.[0-9]+(-.*)?$/\1/')
|
||||||
|
|
||||||
|
echo "release_tag=$RELEASE_TAG" >> $GITHUB_OUTPUT
|
||||||
|
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||||
|
echo "suffix=$SUFFIX" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: 🔑 Setup SSH for AUR
|
||||||
|
run: |
|
||||||
|
mkdir -p ~/.ssh
|
||||||
|
|
||||||
|
# Write key with proper newline handling
|
||||||
|
echo "${{ secrets.AUR_SSH_PRIVATE_KEY }}" | sed 's/\\n/\n/g' > ~/.ssh/id_rsa
|
||||||
|
|
||||||
|
# Set proper permissions
|
||||||
|
chmod 600 ~/.ssh/id_rsa
|
||||||
|
ssh-keyscan aur.archlinux.org >> ~/.ssh/known_hosts
|
||||||
|
chmod 600 ~/.ssh/known_hosts
|
||||||
|
|
||||||
|
# Create SSH config file
|
||||||
|
cat > ~/.ssh/config << EOF
|
||||||
|
Host aur.archlinux.org
|
||||||
|
IdentityFile ~/.ssh/id_rsa
|
||||||
|
User aur
|
||||||
|
StrictHostKeyChecking accept-new
|
||||||
|
EOF
|
||||||
|
chmod 600 ~/.ssh/config
|
||||||
|
|
||||||
|
- name: 🔄 Update AUR Package
|
||||||
|
env:
|
||||||
|
VERSION: ${{ steps.release_info.outputs.version }}
|
||||||
|
SUFFIX: ${{ steps.release_info.outputs.suffix }}
|
||||||
|
run: |
|
||||||
|
# Configure Git
|
||||||
|
git config --global user.name "${{ secrets.AUR_USER }}"
|
||||||
|
git config --global user.email "${{ secrets.AUR_EMAIL }}"
|
||||||
|
git config --global --add safe.directory '*'
|
||||||
|
|
||||||
|
# Clone AUR repository
|
||||||
|
GIT_SSH_COMMAND="ssh -v -i ~/.ssh/id_rsa -o StrictHostKeyChecking=accept-new" \
|
||||||
|
git clone "ssh://aur@aur.archlinux.org/neodlp.git" aur-repo
|
||||||
|
cd aur-repo
|
||||||
|
|
||||||
|
# Mark this specific repository as safe too
|
||||||
|
git config --global --add safe.directory "$(pwd)"
|
||||||
|
|
||||||
|
# Update PKGBUILD version
|
||||||
|
sed -i "s/pkgver=.*/pkgver=${VERSION}/" PKGBUILD
|
||||||
|
|
||||||
|
# Create non-root user for makepkg (which refuses to run as root)
|
||||||
|
useradd -m builder
|
||||||
|
chown -R builder:builder .
|
||||||
|
|
||||||
|
# Generate .SRCINFO using makepkg
|
||||||
|
su builder -c "makepkg --printsrcinfo" > .SRCINFO
|
||||||
|
|
||||||
|
# Debug output
|
||||||
|
echo "PKGBUILD:"
|
||||||
|
cat PKGBUILD
|
||||||
|
|
||||||
|
echo ".SRCINFO:"
|
||||||
|
cat .SRCINFO
|
||||||
|
|
||||||
|
# Check if there are any changes to commit
|
||||||
|
if [ -n "$(git status --porcelain)" ]; then
|
||||||
|
echo "Changes detected, committing and pushing..."
|
||||||
|
# Commit and push changes
|
||||||
|
git add PKGBUILD .SRCINFO
|
||||||
|
git commit -m "Update to version v${VERSION}${SUFFIX}"
|
||||||
|
git push
|
||||||
|
echo "Successfully pushed updates to AUR"
|
||||||
|
else
|
||||||
|
echo "No changes detected in PKGBUILD or .SRCINFO, skipping commit"
|
||||||
|
echo "Package is already up to date at version v${VERSION}${SUFFIX}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: 🔍 Verify update
|
||||||
|
run: |
|
||||||
|
echo "Successfully updated AUR package to version ${{ steps.release_info.outputs.version }}${{ steps.release_info.outputs.suffix }}"
|
||||||
|
echo "View the updated package at: https://aur.archlinux.org/packages/neodlp"
|
||||||
23
.github/workflows/publish-to-winget.yml
vendored
Normal file
23
.github/workflows/publish-to-winget.yml
vendored
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [released]
|
||||||
|
|
||||||
|
name: 🚀 Publish to WinGet
|
||||||
|
jobs:
|
||||||
|
publish:
|
||||||
|
runs-on: windows-latest
|
||||||
|
steps:
|
||||||
|
- name: 🛠️ Get release version
|
||||||
|
id: get-version
|
||||||
|
run: |
|
||||||
|
$VERSION="${{ github.event.release.tag_name }}" -replace '^v|[^0-9.]'
|
||||||
|
"version=$VERSION" >> $env:GITHUB_OUTPUT
|
||||||
|
shell: pwsh
|
||||||
|
|
||||||
|
- name: 🚀 Send PR to winget-pkgs repo
|
||||||
|
uses: vedantmgoyal9/winget-releaser@main
|
||||||
|
with:
|
||||||
|
identifier: neosubhamoy.neodlp
|
||||||
|
version: ${{ steps.get-version.outputs.version }}
|
||||||
|
installers-regex: '\.exe$'
|
||||||
|
token: ${{ secrets.WINGET_TOKEN }}
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -11,6 +11,7 @@ node_modules
|
|||||||
dist
|
dist
|
||||||
dist-ssr
|
dist-ssr
|
||||||
*.local
|
*.local
|
||||||
|
.github/workflows/.secrets
|
||||||
|
|
||||||
# Editor directories and files
|
# Editor directories and files
|
||||||
.vscode/*
|
.vscode/*
|
||||||
|
|||||||
12
CHANGELOG.md
12
CHANGELOG.md
@@ -1,10 +1,16 @@
|
|||||||
### ✨ Changelog
|
### ✨ Changelog
|
||||||
|
|
||||||
- Initial MVP release (v0.1.0)
|
- DOWNLOADER: Added 'Cancel Search' button
|
||||||
|
- FIXED: Downloaded files are not moving from temp to download folder in some linux distros
|
||||||
|
- FIXED: 'Stop' all ongoing downloads button not working on linux
|
||||||
|
- Improved search and download error handling
|
||||||
|
- Removed subtitle (CC) embeding restriction from M4A files
|
||||||
|
- Migrated to Zod v4 and improved form validations
|
||||||
|
- Other minor fixes and improvements
|
||||||
|
|
||||||
### 📝 Notes
|
### 📝 Notes
|
||||||
|
|
||||||
> ⚠️ Linux Users: Make sure yt-dlp is not installed in your distro (otherwise you will get package installation conflict)
|
> ⚠️ Linux Users: Make sure yt-dlp is not installed in your distro (otherwise you will get package installation conflict). Don't worry, You can still use yt-dlp cli as before (the only difference is that now it will be installed and auto-updated by neo-dlp, which You can also disable from neo-dlp Settings if you don't want to auto-update yt-dlp)
|
||||||
|
|
||||||
> This is an Un-Signed Build (Windows doesn't trust this Certificate so, it may flag this as malicious software, in that case, disable Windows SmartScreen and Defender, install it, and then re-enable them)
|
> This is an Un-Signed Build (Windows doesn't trust this Certificate so, it may flag this as malicious software, in that case, disable Windows SmartScreen and Defender, install it, and then re-enable them)
|
||||||
|
|
||||||
@@ -19,4 +25,4 @@
|
|||||||
|
|
||||||
> ⬆️ icon indicates this packaging format supports in-built app-updater
|
> ⬆️ icon indicates this packaging format supports in-built app-updater
|
||||||
|
|
||||||
> ⚠️ ARM64 binaries are experimental and may not work properly on Apple Silicon Macs (You might see 'Damaged File' warning) it's because the binaries are not signed and Apple Silicon Macs don't allow unsigned apps (downloaded from internet) to be installed on the system (also I'm not planning to sign it soon as it costs 99$/year, which I can't afford RN!). If you want to use NeoDLP in your Apple Silicon Macs then you have to [compile it from source](https://github.com/neosubhamoy/neodlp?tab=readme-ov-file#%EF%B8%8F-contributing--building-from-source) in your Mac
|
> ⚠️ MacOS ARM64 binary downloads are experimental and may not open on Apple Silicon Macs if downloaded from browser (You will get 'Damaged File' error) it's because the binaries are not signed (signing MacOS binaries requires 99$/year Apple Developer Account subscription, which I can't afford RN!) and Apple Silicon Macs don't allow unsigned apps (downloaded from browser) to be installed on the system. If you want to use NeoDLP on your Apple Silicon Macs, you can simply use the command line [Curl-Bash Installer](https://neodlp.neosubhamoy.com/download) (Recommended) -OR- [compile it from source](https://github.com/neosubhamoy/neodlp?tab=readme-ov-file#%EF%B8%8F-contributing--building-from-source) in your Mac
|
||||||
73
README.md
73
README.md
@@ -1,30 +1,58 @@
|
|||||||
|

|
||||||
|
|
||||||
# NeoDLP - (Neo Downloader Plus)
|
# NeoDLP - (Neo Downloader Plus)
|
||||||
|
|
||||||
Crossplatform Video/Audio Downloader Desktop App with Modern UI and Browser Integration
|
Cross-platform Video/Audio Downloader Desktop App with Modern UI and Browser Integration
|
||||||
|
|
||||||
[](https://github.com/neosubhamoy/neodlp)
|
[](https://github.com/neosubhamoy/neodlp)
|
||||||
[](https://github.com/neosubhamoy/neodlp)
|
[](https://github.com/neosubhamoy/neodlp)
|
||||||
|
[](https://github.com/neosubhamoy/neodlp/releases)
|
||||||
[](https://github.com/neosubhamoy/neodlp)
|
[](https://github.com/neosubhamoy/neodlp)
|
||||||
|
|
||||||
> **🥰 Liked this project? Please consider giving it a Star (🌟) on github to show us your appreciation and help the algorythm recommend this project to even more awesome people like you!**
|
> **🥰 Liked this project? Please consider giving it a Star (🌟) on github to show us your appreciation and help the algorythm recommend this project to even more awesome people like you!**
|
||||||
|
|
||||||
|
[](https://repology.org/project/neodlp/versions)
|
||||||
|
|
||||||
|
### ✨ Highlighted Features
|
||||||
|
|
||||||
|
- Download Video/Audio from popular sites (YT, FB, IG, X and other 2.5k+ [supported sites](https://github.com/yt-dlp/yt-dlp/blob/master/supportedsites.md))
|
||||||
|
- Download Video/Audio in your preffered format (MP4, WEBM, MKV, MP3 etc.)
|
||||||
|
- Supports both Video and Playlist download
|
||||||
|
- Supports Combining Video, Audio streams of your choice
|
||||||
|
- Supports Multi-Language Subtitle/Caption (CC) embeding
|
||||||
|
- Different Video/Audio metadata embeding options (info, chapters, thumbnail etc.)
|
||||||
|
- Highly customizable and many more...😉
|
||||||
|
|
||||||
|
### 🧩 Browser Integration
|
||||||
|
|
||||||
|
You can integrate NeoDLP with your favourite browser (any Chrome/Chromium/Firefox based browser) Just, install [NeoDLP Extension](https://github.com/neosubhamoy/neodlp-extension) to get started!
|
||||||
|
|
||||||
|
After installing the extension you can do the following directly from the browser:
|
||||||
|
|
||||||
|
- Quick Search (search current browser address with NeoDLP) (via pressing keyboard shortcut `ALT`+`SHIFT`+`Q`, You can also change this shortcut key combo from browser settings)
|
||||||
|
|
||||||
|
- Right Click Context Menu Action (Download with Neo Downloader Plus - Link, Selection, Media Source)
|
||||||
|
|
||||||
|
### 👀 Sneak Peek
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
### 💻 Supported Platforms
|
### 💻 Supported Platforms
|
||||||
|
|
||||||
- Windows (10 / 11)
|
- Windows (10 / 11)
|
||||||
- Linux (Debian / Fedora / Arch Linux base)
|
- Linux (Debian / Fedora / Arch Linux base)
|
||||||
- MacOS (>10.3)
|
- MacOS (>10.3)
|
||||||
|
|
||||||
### 🌐 Supported Sites
|
> ⚠️ **NOTE:** Though most linux (debian/fedora/arch base) distros are supported but not all packages are tested on all these platforms, to save time (and some brain cells) and ship the software as fast as possible! (Currently only the debian package is tested on Ubuntu 24.04 - So, other linux packages may have issues, test it yourself and feel free to report issues if you found one)
|
||||||
|
|
||||||
- All [Supported Sites](https://github.com/yt-dlp/yt-dlp/blob/master/supportedsites.md) by [yt-dlp](https://github.com/yt-dlp/yt-dlp) **(2.5K+)**
|
### 🤝 External Dependencies
|
||||||
|
|
||||||
### 🧩 External Dependencies
|
- [YT-DLP](https://github.com/yt-dlp/yt-dlp) - The core CLI tool used to download Video/Audio from the Web (Hero of the show 😎)
|
||||||
|
- [FFmpeg](https://www.ffmpeg.org) - Used for Video/Audio post-processing
|
||||||
- [yt-dlp](https://github.com/yt-dlp/yt-dlp) - The core CLI Tool used to download Video/Audio from the Web
|
|
||||||
- [FFmpeg](https://www.ffmpeg.org) - Used for Video/Audio Post-processing
|
|
||||||
|
|
||||||
### ⬇️ Download and Installation
|
### ⬇️ Download and Installation
|
||||||
|
|
||||||
1. Download the latest [NeoDLP](https://github.com/neosubhamoy/neodlp/releases/latest) release based on your OS and CPU Architecture then install it or install it directly from an available distribution channel
|
1. Download the latest [NeoDLP](https://github.com/neosubhamoy/neodlp/releases/latest) release based on your OS and CPU Architecture, then install it! -OR- Install it directly from an available distribution channel (listed below)
|
||||||
|
|
||||||
| Arch\OS | Windows | Linux | MacOS |
|
| Arch\OS | Windows | Linux | MacOS |
|
||||||
| :---- | :---- | :---- | :---- |
|
| :---- | :---- | :---- | :---- |
|
||||||
@@ -34,8 +62,21 @@ Crossplatform Video/Audio Downloader Desktop App with Modern UI and Browser Inte
|
|||||||
| Platform (OS) | Distribution Channel | Installation Command / Instruction |
|
| Platform (OS) | Distribution Channel | Installation Command / Instruction |
|
||||||
| :---- | :---- | :---- |
|
| :---- | :---- | :---- |
|
||||||
| Windows x86_64 | WinGet | `winget install neodlp` |
|
| Windows x86_64 | WinGet | `winget install neodlp` |
|
||||||
|
| MacOS Universal | Curl-Bash Installer | `curl -sSL https://neodlp.neosubhamoy.com/neodlp_macos_installer.sh \| bash` |
|
||||||
| Linux x86_64 (Arch Linux) | AUR | `yay -S neodlp` |
|
| Linux x86_64 (Arch Linux) | AUR | `yay -S neodlp` |
|
||||||
|
|
||||||
|
### 💝 Support the Development
|
||||||
|
|
||||||
|
NeoDLP is and will be always FREE to Use for Everyone and Open-Sourced. On the other hand the developent process of NeoDLP takes lots of time, effort and even sometimes money! So, if you appriciate my work and have the ability to donate, then please consider supporting the development by donating (even a very small donation matters and helps NeoDLP to be a better product!) Your support is the key to my motivation...🤗
|
||||||
|
|
||||||
|
<a href="https://buymeacoffee.com/neosubhamoy" target="_blank" title="buymeacoffee">
|
||||||
|
<img src="https://iili.io/JoQ0zN9.md.png" alt="buymeacoffee-orange-badge" style="width: 150px;">
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<br></br>
|
||||||
|
|
||||||
|
> 📌 **NOTE:** You can also donate via UPI by sending donations to this UPI ID directly: **[subhamoybiswas636-2@oksbi](upi://pay?pa=subhamoybiswas636-2@oksbi&pn=Subhamoy%20Biswas)**
|
||||||
|
|
||||||
### ⚡ Technologies Used
|
### ⚡ Technologies Used
|
||||||
|
|
||||||

|

|
||||||
@@ -48,20 +89,24 @@ Crossplatform Video/Audio Downloader Desktop App with Modern UI and Browser Inte
|
|||||||
|
|
||||||
Want to be part of this? Feel free to contribute...!! Pull Requests are always welcome...!! (^_^) Follow these simple steps to start building:
|
Want to be part of this? Feel free to contribute...!! Pull Requests are always welcome...!! (^_^) Follow these simple steps to start building:
|
||||||
|
|
||||||
* Make sure to install Rust, Node.js and Git before proceeding.
|
* Make sure to install [Rust](https://www.rust-lang.org/tools/install), [Node.js](https://nodejs.org/en), [Git](https://git-scm.com/downloads) and [Git-LFS](https://git-lfs.com/) before proceeding.
|
||||||
* Install Tauri [Prerequisites](https://v2.tauri.app/start/prerequisites/) for your OS / platform
|
* Install [Tauri Prerequisites](https://v2.tauri.app/start/prerequisites/) for your OS / platform
|
||||||
1. Fork this repo in your github account.
|
1. Fork this repo in your github account.
|
||||||
2. Git clone the forked repo in your local machine.
|
2. Git clone the forked repo in your local machine.
|
||||||
3. Install Node.js dependencies: `npm install`
|
3. Install Node.js dependencies: `npm install`
|
||||||
4. Run development / build process
|
4. Run development / build process
|
||||||
> ⚠️ Make sure to run the build command once before running the dev command for the first time to avoid build time errors
|
> ⚠️ **IMPORTANT:** Make sure to run the build command once before running the dev command for the first time to avoid compile time errors
|
||||||
```code
|
```code
|
||||||
|
# for windows and linux users
|
||||||
npm run tauri dev # for development
|
npm run tauri dev # for development
|
||||||
npm run tauri build # for production build
|
npm run tauri build # for production build
|
||||||
|
|
||||||
# must use --config flag with the commands if you are on macOS
|
# for macOS users (based on cpu architecture)
|
||||||
--config "./src-tauri/tauri.macos-aarch64.conf.json" # for apple silicon macs
|
npm run tauri dev -- --config "./src-tauri/tauri.macos-aarch64.conf.json" # for apple silicon macs, development
|
||||||
--config "./src-tauri/tauri.macos-x86_64.conf.json" # for intel x86 macs
|
npm run tauri build -- --config "./src-tauri/tauri.macos-aarch64.conf.json" # for apple silicon macs, production build
|
||||||
|
|
||||||
|
npm run tauri dev -- --config "./src-tauri/tauri.macos-x86_64.conf.json" # for intel x86 macs, development
|
||||||
|
npm run tauri build -- --config "./src-tauri/tauri.macos-x86_64.conf.json" # for intel x86 macs, production build
|
||||||
```
|
```
|
||||||
5. Do the changes, Send a Pull Request with proper Description (NOTE: Pull Requests Without Proper Description will be Rejected)
|
5. Do the changes, Send a Pull Request with proper Description (NOTE: Pull Requests Without Proper Description will be Rejected)
|
||||||
|
|
||||||
|
|||||||
3683
package-lock.json
generated
3683
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
131
package.json
131
package.json
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "neodlp",
|
"name": "neodlp",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.1.0",
|
"version": "0.2.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
@@ -10,77 +10,78 @@
|
|||||||
"tauri": "tauri"
|
"tauri": "tauri"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hookform/resolvers": "^4.0.0",
|
"@hookform/resolvers": "^5.1.1",
|
||||||
"@radix-ui/react-accordion": "^1.2.3",
|
"@radix-ui/react-accordion": "^1.2.11",
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.6",
|
"@radix-ui/react-alert-dialog": "^1.1.14",
|
||||||
"@radix-ui/react-aspect-ratio": "^1.1.2",
|
"@radix-ui/react-aspect-ratio": "^1.1.7",
|
||||||
"@radix-ui/react-avatar": "^1.1.3",
|
"@radix-ui/react-avatar": "^1.1.10",
|
||||||
"@radix-ui/react-checkbox": "^1.1.4",
|
"@radix-ui/react-checkbox": "^1.3.2",
|
||||||
"@radix-ui/react-collapsible": "^1.1.3",
|
"@radix-ui/react-collapsible": "^1.1.11",
|
||||||
"@radix-ui/react-context-menu": "^2.2.6",
|
"@radix-ui/react-context-menu": "^2.2.15",
|
||||||
"@radix-ui/react-dialog": "^1.1.6",
|
"@radix-ui/react-dialog": "^1.1.14",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.6",
|
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
||||||
"@radix-ui/react-hover-card": "^1.1.6",
|
"@radix-ui/react-hover-card": "^1.1.14",
|
||||||
"@radix-ui/react-label": "^2.1.2",
|
"@radix-ui/react-label": "^2.1.7",
|
||||||
"@radix-ui/react-menubar": "^1.1.6",
|
"@radix-ui/react-menubar": "^1.1.15",
|
||||||
"@radix-ui/react-navigation-menu": "^1.2.5",
|
"@radix-ui/react-navigation-menu": "^1.2.13",
|
||||||
"@radix-ui/react-popover": "^1.1.6",
|
"@radix-ui/react-popover": "^1.1.14",
|
||||||
"@radix-ui/react-progress": "^1.1.2",
|
"@radix-ui/react-progress": "^1.1.7",
|
||||||
"@radix-ui/react-radio-group": "^1.2.3",
|
"@radix-ui/react-radio-group": "^1.3.7",
|
||||||
"@radix-ui/react-scroll-area": "^1.2.3",
|
"@radix-ui/react-scroll-area": "^1.2.9",
|
||||||
"@radix-ui/react-select": "^2.1.6",
|
"@radix-ui/react-select": "^2.2.5",
|
||||||
"@radix-ui/react-separator": "^1.1.2",
|
"@radix-ui/react-separator": "^1.1.7",
|
||||||
"@radix-ui/react-slider": "^1.2.3",
|
"@radix-ui/react-slider": "^1.3.5",
|
||||||
"@radix-ui/react-slot": "^1.1.2",
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
"@radix-ui/react-switch": "^1.1.3",
|
"@radix-ui/react-switch": "^1.2.5",
|
||||||
"@radix-ui/react-tabs": "^1.1.3",
|
"@radix-ui/react-tabs": "^1.1.12",
|
||||||
"@radix-ui/react-toast": "^1.2.6",
|
"@radix-ui/react-toast": "^1.2.14",
|
||||||
"@radix-ui/react-toggle": "^1.1.2",
|
"@radix-ui/react-toggle": "^1.1.9",
|
||||||
"@radix-ui/react-toggle-group": "^1.1.2",
|
"@radix-ui/react-toggle-group": "^1.1.10",
|
||||||
"@radix-ui/react-tooltip": "^1.1.8",
|
"@radix-ui/react-tooltip": "^1.2.7",
|
||||||
"@tanstack/react-query": "^5.67.2",
|
"@tanstack/react-query": "^5.83.0",
|
||||||
"@tanstack/react-query-devtools": "^5.67.2",
|
"@tanstack/react-query-devtools": "^5.83.0",
|
||||||
"@tauri-apps/api": "^2",
|
"@tauri-apps/api": "^2",
|
||||||
"@tauri-apps/plugin-dialog": "^2.2.1",
|
"@tauri-apps/plugin-dialog": "^2.3.1",
|
||||||
"@tauri-apps/plugin-fs": "^2.2.0",
|
"@tauri-apps/plugin-fs": "^2.4.1",
|
||||||
"@tauri-apps/plugin-opener": "^2.2.6",
|
"@tauri-apps/plugin-opener": "^2.4.0",
|
||||||
"@tauri-apps/plugin-os": "^2.2.0",
|
"@tauri-apps/plugin-os": "^2.3.0",
|
||||||
"@tauri-apps/plugin-process": "^2.2.1",
|
"@tauri-apps/plugin-process": "^2.3.0",
|
||||||
"@tauri-apps/plugin-shell": "^2.2.0",
|
"@tauri-apps/plugin-shell": "^2.3.0",
|
||||||
"@tauri-apps/plugin-sql": "^2.2.0",
|
"@tauri-apps/plugin-sql": "^2.3.0",
|
||||||
"@tauri-apps/plugin-updater": "^2.7.1",
|
"@tauri-apps/plugin-updater": "^2.9.0",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.0.0",
|
"cmdk": "^1.1.1",
|
||||||
"date-fns": "^3.6.0",
|
"date-fns": "^4.1.0",
|
||||||
"embla-carousel-react": "^8.5.2",
|
"embla-carousel-react": "^8.6.0",
|
||||||
"input-otp": "^1.4.2",
|
"input-otp": "^1.4.2",
|
||||||
"lucide-react": "^0.475.0",
|
"lucide-react": "^0.525.0",
|
||||||
"next-themes": "^0.4.4",
|
"next-themes": "^0.4.6",
|
||||||
"react": "^18.3.1",
|
"react": "^19.1.0",
|
||||||
"react-day-picker": "^8.10.1",
|
"react-day-picker": "^9.8.0",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^19.1.0",
|
||||||
"react-hook-form": "^7.54.2",
|
"react-hook-form": "^7.60.0",
|
||||||
"react-resizable-panels": "^2.1.7",
|
"react-resizable-panels": "^3.0.3",
|
||||||
"react-router-dom": "^7.1.5",
|
"react-router-dom": "^7.7.0",
|
||||||
"recharts": "^2.15.1",
|
"recharts": "^2.15.4",
|
||||||
"sonner": "^1.7.4",
|
"sonner": "^2.0.6",
|
||||||
"tailwind-merge": "^3.0.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
|
||||||
"vaul": "^1.1.2",
|
"vaul": "^1.1.2",
|
||||||
"zod": "^3.24.2",
|
"zod": "^4.0.5",
|
||||||
"zustand": "^5.0.3"
|
"zustand": "^5.0.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@tailwindcss/postcss": "^4.1.11",
|
||||||
|
"@tailwindcss/vite": "^4.1.11",
|
||||||
"@tauri-apps/cli": "^2",
|
"@tauri-apps/cli": "^2",
|
||||||
"@types/node": "^22.13.4",
|
"@types/node": "^24.0.15",
|
||||||
"@types/react": "^18.3.1",
|
"@types/react": "^19.1.8",
|
||||||
"@types/react-dom": "^18.3.1",
|
"@types/react-dom": "^19.1.6",
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react": "^4.7.0",
|
||||||
"autoprefixer": "^10.4.20",
|
"postcss": "^8.5.6",
|
||||||
"postcss": "^8.5.2",
|
"tailwindcss": "^4.1.11",
|
||||||
"tailwindcss": "^3.4.17",
|
"tw-animate-css": "^1.3.5",
|
||||||
"typescript": "~5.6.2",
|
"typescript": "~5.8.3",
|
||||||
"vite": "^6.0.3"
|
"vite": "^7.0.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
export default {
|
export default {
|
||||||
plugins: {
|
plugins: {
|
||||||
tailwindcss: {},
|
"@tailwindcss/postcss": {},
|
||||||
autoprefixer: {},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
1612
src-tauri/Cargo.lock
generated
1612
src-tauri/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "neodlp"
|
name = "neodlp"
|
||||||
version = "0.1.0"
|
version = "0.2.1"
|
||||||
description = "NeoDLP"
|
description = "NeoDLP"
|
||||||
authors = ["neosubhamoy <hey@neosubhamoy.com>"]
|
authors = ["neosubhamoy <hey@neosubhamoy.com>"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:f26019446a303dfa16f5a4b60e03e853a5d1e0d44a682b31e5cfd2622c0ce2fd
|
oid sha256:5fa4862eb50050941370fa78a8c56faa31516d5abaf1f7cf31bc56de166347d9
|
||||||
size 18152568
|
size 18123562
|
||||||
|
|||||||
Binary file not shown.
@@ -6,7 +6,7 @@
|
|||||||
<string>com.neosubhamoy.neodlp</string>
|
<string>com.neosubhamoy.neodlp</string>
|
||||||
<key>ProgramArguments</key>
|
<key>ProgramArguments</key>
|
||||||
<array>
|
<array>
|
||||||
<string>/Applications/neodlp.app/Contents/MacOS/neodlp</string>
|
<string>/Applications/NeoDLP.app/Contents/MacOS/neodlp</string>
|
||||||
<string>--hidden</string>
|
<string>--hidden</string>
|
||||||
</array>
|
</array>
|
||||||
<key>RunAtLoad</key>
|
<key>RunAtLoad</key>
|
||||||
|
|||||||
@@ -3,5 +3,5 @@
|
|||||||
"description": "NeoDLP MsgHost",
|
"description": "NeoDLP MsgHost",
|
||||||
"path": "/usr/bin/neodlp-msghost",
|
"path": "/usr/bin/neodlp-msghost",
|
||||||
"type": "stdio",
|
"type": "stdio",
|
||||||
"allowed_origins": ["chrome-extension://mehopeailfjmiloiiohgicphlcgpompf/", "chrome-extension://agkddibgemhefmdhlnooiakfnhihhbdb/"]
|
"allowed_origins": ["chrome-extension://mehopeailfjmiloiiohgicphlcgpompf/"]
|
||||||
}
|
}
|
||||||
@@ -3,5 +3,5 @@
|
|||||||
"description": "NeoDLP MsgHost",
|
"description": "NeoDLP MsgHost",
|
||||||
"path": "/Applications/NeoDLP.app/Contents/Resources/neodlp-msghost",
|
"path": "/Applications/NeoDLP.app/Contents/Resources/neodlp-msghost",
|
||||||
"type": "stdio",
|
"type": "stdio",
|
||||||
"allowed_origins": ["chrome-extension://mehopeailfjmiloiiohgicphlcgpompf/", "chrome-extension://agkddibgemhefmdhlnooiakfnhihhbdb/"]
|
"allowed_origins": ["chrome-extension://mehopeailfjmiloiiohgicphlcgpompf/"]
|
||||||
}
|
}
|
||||||
@@ -3,5 +3,5 @@
|
|||||||
"description": "NeoDLP MsgHost",
|
"description": "NeoDLP MsgHost",
|
||||||
"path": "neodlp-msghost.exe",
|
"path": "neodlp-msghost.exe",
|
||||||
"type": "stdio",
|
"type": "stdio",
|
||||||
"allowed_origins": ["chrome-extension://mehopeailfjmiloiiohgicphlcgpompf/", "chrome-extension://agkddibgemhefmdhlnooiakfnhihhbdb/"]
|
"allowed_origins": ["chrome-extension://mehopeailfjmiloiiohgicphlcgpompf/"]
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "NeoDLP",
|
"productName": "NeoDLP",
|
||||||
"mainBinaryName": "neodlp",
|
"mainBinaryName": "neodlp",
|
||||||
"version": "0.1.0",
|
"version": "0.2.1",
|
||||||
"identifier": "com.neosubhamoy.neodlp",
|
"identifier": "com.neosubhamoy.neodlp",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "npm run dev",
|
"beforeDevCommand": "npm run dev",
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
{
|
{
|
||||||
"title": "NeoDLP",
|
"title": "NeoDLP",
|
||||||
"width": 1067,
|
"width": 1067,
|
||||||
"height": 600,
|
"height": 605,
|
||||||
"visible": false
|
"visible": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
{
|
{
|
||||||
"title": "NeoDLP",
|
"title": "NeoDLP",
|
||||||
"width": 1067,
|
"width": 1067,
|
||||||
"height": 600,
|
"height": 605,
|
||||||
"visible": false
|
"visible": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
{
|
{
|
||||||
"title": "NeoDLP",
|
"title": "NeoDLP",
|
||||||
"width": 1067,
|
"width": 1067,
|
||||||
"height": 600,
|
"height": 605,
|
||||||
"visible": false
|
"visible": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
{
|
{
|
||||||
"title": "NeoDLP",
|
"title": "NeoDLP",
|
||||||
"width": 1067,
|
"width": 1067,
|
||||||
"height": 600,
|
"height": 605,
|
||||||
"visible": false
|
"visible": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
{
|
{
|
||||||
"title": "NeoDLP",
|
"title": "NeoDLP",
|
||||||
"width": 1067,
|
"width": 1067,
|
||||||
"height": 600,
|
"height": 605,
|
||||||
"visible": false
|
"visible": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
174
src/App.tsx
174
src/App.tsx
@@ -7,7 +7,7 @@ import { invoke } from "@tauri-apps/api/core";
|
|||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { arch, exeExtension } from "@tauri-apps/plugin-os";
|
import { arch, exeExtension } from "@tauri-apps/plugin-os";
|
||||||
import { downloadDir, join, resourceDir, tempDir } from "@tauri-apps/api/path";
|
import { downloadDir, join, resourceDir, tempDir } from "@tauri-apps/api/path";
|
||||||
import { useBasePathsStore, useCurrentVideoMetadataStore, useDownloadStatesStore, useKvPairsStatesStore, useSettingsPageStatesStore } from "@/services/store";
|
import { useBasePathsStore, useCurrentVideoMetadataStore, useDownloaderPageStatesStore, useDownloadStatesStore, useKvPairsStatesStore, useSettingsPageStatesStore } from "@/services/store";
|
||||||
import { determineFileType, generateDownloadId, generateSafeFilePath, generateVideoId, isObjEmpty, parseProgressLine, sanitizeFilename } from "@/utils";
|
import { determineFileType, generateDownloadId, generateSafeFilePath, generateVideoId, isObjEmpty, parseProgressLine, sanitizeFilename } from "@/utils";
|
||||||
import { Command } from "@tauri-apps/plugin-shell";
|
import { Command } from "@tauri-apps/plugin-shell";
|
||||||
import { RawVideoInfo } from "@/types/video";
|
import { RawVideoInfo } from "@/types/video";
|
||||||
@@ -25,8 +25,11 @@ import { useNavigate } from "react-router-dom";
|
|||||||
import { platform } from "@tauri-apps/plugin-os";
|
import { platform } from "@tauri-apps/plugin-os";
|
||||||
import { useMacOsRegisterer } from "@/helpers/use-macos-registerer";
|
import { useMacOsRegisterer } from "@/helpers/use-macos-registerer";
|
||||||
import useAppUpdater from "@/helpers/use-app-updater";
|
import useAppUpdater from "@/helpers/use-app-updater";
|
||||||
|
import { useToast } from "@/hooks/use-toast";
|
||||||
|
|
||||||
export default function App({ children }: { children: React.ReactNode }) {
|
export default function App({ children }: { children: React.ReactNode }) {
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
const { data: downloadStates, isSuccess: isSuccessFetchingDownloadStates } = useFetchAllDownloadStates();
|
const { data: downloadStates, isSuccess: isSuccessFetchingDownloadStates } = useFetchAllDownloadStates();
|
||||||
const { data: settings, isSuccess: isSuccessFetchingSettings } = useFetchAllSettings();
|
const { data: settings, isSuccess: isSuccessFetchingSettings } = useFetchAllSettings();
|
||||||
const { data: kvPairs, isSuccess: isSuccessFetchingKvPairs } = useFetchAllkVPairs();
|
const { data: kvPairs, isSuccess: isSuccessFetchingKvPairs } = useFetchAllkVPairs();
|
||||||
@@ -40,6 +43,8 @@ export default function App({ children }: { children: React.ReactNode }) {
|
|||||||
const tempDownloadDirPath = useBasePathsStore((state) => state.tempDownloadDirPath);
|
const tempDownloadDirPath = useBasePathsStore((state) => state.tempDownloadDirPath);
|
||||||
const downloadDirPath = useBasePathsStore((state) => state.downloadDirPath);
|
const downloadDirPath = useBasePathsStore((state) => state.downloadDirPath);
|
||||||
|
|
||||||
|
const setSearchPid = useCurrentVideoMetadataStore((state) => state.setSearchPid);
|
||||||
|
|
||||||
// const isUsingDefaultSettings = useSettingsPageStatesStore((state) => state.isUsingDefaultSettings);
|
// const isUsingDefaultSettings = useSettingsPageStatesStore((state) => state.isUsingDefaultSettings);
|
||||||
const setIsUsingDefaultSettings = useSettingsPageStatesStore((state) => state.setIsUsingDefaultSettings);
|
const setIsUsingDefaultSettings = useSettingsPageStatesStore((state) => state.setIsUsingDefaultSettings);
|
||||||
const setSettingsKey = useSettingsPageStatesStore((state) => state.setSettingsKey);
|
const setSettingsKey = useSettingsPageStatesStore((state) => state.setSettingsKey);
|
||||||
@@ -53,10 +58,27 @@ export default function App({ children }: { children: React.ReactNode }) {
|
|||||||
const YTDLP_UPDATE_CHANNEL = useSettingsPageStatesStore(state => state.settings.ytdlp_update_channel);
|
const YTDLP_UPDATE_CHANNEL = useSettingsPageStatesStore(state => state.settings.ytdlp_update_channel);
|
||||||
const APP_THEME = useSettingsPageStatesStore(state => state.settings.theme);
|
const APP_THEME = useSettingsPageStatesStore(state => state.settings.theme);
|
||||||
const MAX_PARALLEL_DOWNLOADS = useSettingsPageStatesStore(state => state.settings.max_parallel_downloads);
|
const MAX_PARALLEL_DOWNLOADS = useSettingsPageStatesStore(state => state.settings.max_parallel_downloads);
|
||||||
|
const MAX_RETRIES = useSettingsPageStatesStore(state => state.settings.max_retries);
|
||||||
const DOWNLOAD_DIR = useSettingsPageStatesStore(state => state.settings.download_dir);
|
const DOWNLOAD_DIR = useSettingsPageStatesStore(state => state.settings.download_dir);
|
||||||
const PREFER_VIDEO_OVER_PLAYLIST = useSettingsPageStatesStore(state => state.settings.prefer_video_over_playlist);
|
const PREFER_VIDEO_OVER_PLAYLIST = useSettingsPageStatesStore(state => state.settings.prefer_video_over_playlist);
|
||||||
|
const STRICT_DOWNLOADABILITY_CHECK = useSettingsPageStatesStore(state => state.settings.strict_downloadablity_check);
|
||||||
const USE_PROXY = useSettingsPageStatesStore(state => state.settings.use_proxy);
|
const USE_PROXY = useSettingsPageStatesStore(state => state.settings.use_proxy);
|
||||||
const PROXY_URL = useSettingsPageStatesStore(state => state.settings.proxy_url);
|
const PROXY_URL = useSettingsPageStatesStore(state => state.settings.proxy_url);
|
||||||
|
const USE_RATE_LIMIT = useSettingsPageStatesStore(state => state.settings.use_rate_limit);
|
||||||
|
const RATE_LIMIT = useSettingsPageStatesStore(state => state.settings.rate_limit);
|
||||||
|
const VIDEO_FORMAT = useSettingsPageStatesStore(state => state.settings.video_format);
|
||||||
|
const AUDIO_FORMAT = useSettingsPageStatesStore(state => state.settings.audio_format);
|
||||||
|
const ALWAYS_REENCODE_VIDEO = useSettingsPageStatesStore(state => state.settings.always_reencode_video);
|
||||||
|
const EMBED_VIDEO_METADATA = useSettingsPageStatesStore(state => state.settings.embed_video_metadata);
|
||||||
|
const EMBED_AUDIO_METADATA = useSettingsPageStatesStore(state => state.settings.embed_audio_metadata);
|
||||||
|
const EMBED_AUDIO_THUMBNAIL = useSettingsPageStatesStore(state => state.settings.embed_audio_thumbnail);
|
||||||
|
|
||||||
|
const isErrored = useDownloaderPageStatesStore((state) => state.isErrored);
|
||||||
|
const isErrorExpected = useDownloaderPageStatesStore((state) => state.isErrorExpected);
|
||||||
|
const erroredDownloadId = useDownloaderPageStatesStore((state) => state.erroredDownloadId);
|
||||||
|
const setIsErrored = useDownloaderPageStatesStore((state) => state.setIsErrored);
|
||||||
|
const setIsErrorExpected = useDownloaderPageStatesStore((state) => state.setIsErrorExpected);
|
||||||
|
const setErroredDownloadId = useDownloaderPageStatesStore((state) => state.setErroredDownloadId);
|
||||||
|
|
||||||
const appWindow = getCurrentWebviewWindow()
|
const appWindow = getCurrentWebviewWindow()
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -85,10 +107,12 @@ export default function App({ children }: { children: React.ReactNode }) {
|
|||||||
|
|
||||||
const fetchVideoMetadata = async (url: string, formatId?: string, playlistIndex?: string): Promise<RawVideoInfo | null> => {
|
const fetchVideoMetadata = async (url: string, formatId?: string, playlistIndex?: string): Promise<RawVideoInfo | null> => {
|
||||||
try {
|
try {
|
||||||
const args = [url, '--dump-single-json'];
|
const args = [url, '--dump-single-json', '--no-warnings'];
|
||||||
if (formatId) args.push('-f', formatId);
|
if (formatId) args.push('-f', formatId);
|
||||||
if (playlistIndex) args.push('--playlist-items', playlistIndex);
|
if (playlistIndex) args.push('--playlist-items', playlistIndex);
|
||||||
if (PREFER_VIDEO_OVER_PLAYLIST) args.push('--no-playlist');
|
if (PREFER_VIDEO_OVER_PLAYLIST) args.push('--no-playlist');
|
||||||
|
if (STRICT_DOWNLOADABILITY_CHECK && !formatId) args.push('--check-all-formats');
|
||||||
|
if (STRICT_DOWNLOADABILITY_CHECK && formatId) args.push('--check-formats');
|
||||||
if (USE_PROXY && PROXY_URL) args.push('--proxy', PROXY_URL);
|
if (USE_PROXY && PROXY_URL) args.push('--proxy', PROXY_URL);
|
||||||
const command = Command.sidecar('binaries/yt-dlp', args);
|
const command = Command.sidecar('binaries/yt-dlp', args);
|
||||||
|
|
||||||
@@ -99,15 +123,20 @@ export default function App({ children }: { children: React.ReactNode }) {
|
|||||||
jsonOutput += line;
|
jsonOutput += line;
|
||||||
});
|
});
|
||||||
|
|
||||||
command.on('close', async () => {
|
command.on('close', async (data) => {
|
||||||
|
if (data.code !== 0) {
|
||||||
|
console.error(`yt-dlp failed to fetch metadata with code ${data.code}`);
|
||||||
|
resolve(null);
|
||||||
|
} else {
|
||||||
try {
|
try {
|
||||||
const data: RawVideoInfo = JSON.parse(jsonOutput);
|
const parsedData: RawVideoInfo = JSON.parse(jsonOutput);
|
||||||
resolve(data);
|
resolve(parsedData);
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
console.error(`Failed to parse JSON: ${e}`);
|
console.error(`Failed to parse JSON: ${e}`);
|
||||||
resolve(null);
|
resolve(null);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
command.on('error', error => {
|
command.on('error', error => {
|
||||||
@@ -115,7 +144,9 @@ export default function App({ children }: { children: React.ReactNode }) {
|
|||||||
resolve(null);
|
resolve(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
command.spawn().catch(e => {
|
command.spawn().then(child => {
|
||||||
|
setSearchPid(child.pid);
|
||||||
|
}).catch(e => {
|
||||||
console.error(`Failed to spawn command: ${e}`);
|
console.error(`Failed to spawn command: ${e}`);
|
||||||
resolve(null);
|
resolve(null);
|
||||||
});
|
});
|
||||||
@@ -127,6 +158,11 @@ export default function App({ children }: { children: React.ReactNode }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const startDownload = async (url: string, selectedFormat: string, selectedSubtitles?: string | null, resumeState?: DownloadState, playlistItems?: string) => {
|
const startDownload = async (url: string, selectedFormat: string, selectedSubtitles?: string | null, resumeState?: DownloadState, playlistItems?: string) => {
|
||||||
|
// set error states to default
|
||||||
|
setIsErrored(false);
|
||||||
|
setIsErrorExpected(false);
|
||||||
|
setErroredDownloadId(null);
|
||||||
|
|
||||||
console.log('Starting download:', { url, selectedFormat, selectedSubtitles, resumeState, playlistItems });
|
console.log('Starting download:', { url, selectedFormat, selectedSubtitles, resumeState, playlistItems });
|
||||||
if (!ffmpegPath || !tempDownloadDirPath || !downloadDirPath) {
|
if (!ffmpegPath || !tempDownloadDirPath || !downloadDirPath) {
|
||||||
console.error('FFmpeg or download paths not found');
|
console.error('FFmpeg or download paths not found');
|
||||||
@@ -138,12 +174,24 @@ export default function App({ children }: { children: React.ReactNode }) {
|
|||||||
let videoMetadata = await fetchVideoMetadata(url, selectedFormat, isPlaylist && playlistIndex && typeof playlistIndex === 'string' ? playlistIndex : undefined);
|
let videoMetadata = await fetchVideoMetadata(url, selectedFormat, isPlaylist && playlistIndex && typeof playlistIndex === 'string' ? playlistIndex : undefined);
|
||||||
if (!videoMetadata) {
|
if (!videoMetadata) {
|
||||||
console.error('Failed to fetch video metadata');
|
console.error('Failed to fetch video metadata');
|
||||||
|
toast({
|
||||||
|
title: 'Download Failed',
|
||||||
|
description: 'yt-dlp failed to fetch video metadata. Please try again later.',
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Video Metadata:', videoMetadata);
|
console.log('Video Metadata:', videoMetadata);
|
||||||
videoMetadata = isPlaylist ? videoMetadata.entries[0] : videoMetadata;
|
videoMetadata = isPlaylist ? videoMetadata.entries[0] : videoMetadata;
|
||||||
|
|
||||||
|
const fileType = determineFileType(videoMetadata.vcodec, videoMetadata.acodec);
|
||||||
|
|
||||||
|
if (fileType !== 'unknown' && (VIDEO_FORMAT !== 'auto' || AUDIO_FORMAT !== 'auto')) {
|
||||||
|
if (VIDEO_FORMAT !== 'auto' && (fileType === 'video+audio' || fileType === 'video')) videoMetadata.ext = VIDEO_FORMAT;
|
||||||
|
if (AUDIO_FORMAT !== 'auto' && fileType === 'audio') videoMetadata.ext = AUDIO_FORMAT;
|
||||||
|
}
|
||||||
|
|
||||||
const videoId = resumeState?.video_id || generateVideoId(videoMetadata.id, videoMetadata.webpage_url_domain);
|
const videoId = resumeState?.video_id || generateVideoId(videoMetadata.id, videoMetadata.webpage_url_domain);
|
||||||
const playlistId = isPlaylist ? (resumeState?.playlist_id || generateVideoId(videoMetadata.playlist_id, videoMetadata.webpage_url_domain)) : null;
|
const playlistId = isPlaylist ? (resumeState?.playlist_id || generateVideoId(videoMetadata.playlist_id, videoMetadata.webpage_url_domain)) : null;
|
||||||
const downloadId = resumeState?.download_id || generateDownloadId(videoMetadata.id, videoMetadata.webpage_url_domain);
|
const downloadId = resumeState?.download_id || generateDownloadId(videoMetadata.id, videoMetadata.webpage_url_domain);
|
||||||
@@ -163,6 +211,9 @@ export default function App({ children }: { children: React.ReactNode }) {
|
|||||||
'-f',
|
'-f',
|
||||||
selectedFormat,
|
selectedFormat,
|
||||||
'--no-mtime',
|
'--no-mtime',
|
||||||
|
'--no-warnings',
|
||||||
|
'--retries',
|
||||||
|
MAX_RETRIES.toString(),
|
||||||
];
|
];
|
||||||
|
|
||||||
if (selectedSubtitles) {
|
if (selectedSubtitles) {
|
||||||
@@ -173,6 +224,39 @@ export default function App({ children }: { children: React.ReactNode }) {
|
|||||||
args.push('--playlist-items', playlistIndex);
|
args.push('--playlist-items', playlistIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (fileType !== 'unknown' && (VIDEO_FORMAT !== 'auto' || AUDIO_FORMAT !== 'auto')) {
|
||||||
|
if (VIDEO_FORMAT !== 'auto' && fileType === 'video+audio') {
|
||||||
|
if (ALWAYS_REENCODE_VIDEO) {
|
||||||
|
args.push('--recode-video', VIDEO_FORMAT);
|
||||||
|
} else {
|
||||||
|
args.push('--merge-output-format', VIDEO_FORMAT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (VIDEO_FORMAT !== 'auto' && fileType === 'video') {
|
||||||
|
if (ALWAYS_REENCODE_VIDEO) {
|
||||||
|
args.push('--recode-video', VIDEO_FORMAT);
|
||||||
|
} else {
|
||||||
|
args.push('--remux-video', VIDEO_FORMAT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (AUDIO_FORMAT !== 'auto' && fileType === 'audio') {
|
||||||
|
args.push('--extract-audio', '--audio-format', AUDIO_FORMAT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fileType !== 'unknown' && (EMBED_VIDEO_METADATA || EMBED_AUDIO_METADATA)) {
|
||||||
|
if (EMBED_VIDEO_METADATA && (fileType === 'video+audio' || fileType === 'video')) {
|
||||||
|
args.push('--embed-metadata');
|
||||||
|
}
|
||||||
|
if (EMBED_AUDIO_METADATA && fileType === 'audio') {
|
||||||
|
args.push('--embed-metadata');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (EMBED_AUDIO_THUMBNAIL && fileType === 'audio') {
|
||||||
|
args.push('--embed-thumbnail');
|
||||||
|
}
|
||||||
|
|
||||||
if (resumeState) {
|
if (resumeState) {
|
||||||
args.push('--continue');
|
args.push('--continue');
|
||||||
} else {
|
} else {
|
||||||
@@ -183,26 +267,25 @@ export default function App({ children }: { children: React.ReactNode }) {
|
|||||||
args.push('--proxy', PROXY_URL);
|
args.push('--proxy', PROXY_URL);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (USE_RATE_LIMIT && RATE_LIMIT) {
|
||||||
|
args.push('--limit-rate', `${RATE_LIMIT}`);
|
||||||
|
}
|
||||||
|
|
||||||
console.log('Starting download with args:', args);
|
console.log('Starting download with args:', args);
|
||||||
const command = Command.sidecar('binaries/yt-dlp', args);
|
const command = Command.sidecar('binaries/yt-dlp', args);
|
||||||
|
|
||||||
command.on('close', async data => {
|
command.on('close', async (data) => {
|
||||||
if (data.code !== 0) {
|
if (data.code !== 0) {
|
||||||
console.error(`Download failed with code ${data.code}`);
|
console.error(`Download failed with code ${data.code}`);
|
||||||
} else {
|
if (!isErrorExpected) {
|
||||||
downloadStatusUpdater.mutate({ download_id: downloadId, download_status: 'completed' }, {
|
setIsErrored(true);
|
||||||
onSuccess: (data) => {
|
setErroredDownloadId(downloadId);
|
||||||
console.log("Download status updated successfully:", data);
|
|
||||||
queryClient.invalidateQueries({ queryKey: ['download-states'] });
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
console.error("Failed to update download status:", error);
|
|
||||||
}
|
}
|
||||||
})
|
} else {
|
||||||
|
|
||||||
if (await fs.exists(tempDownloadPath)) {
|
if (await fs.exists(tempDownloadPath)) {
|
||||||
downloadFilePath = await generateSafeFilePath(downloadFilePath);
|
downloadFilePath = await generateSafeFilePath(downloadFilePath);
|
||||||
await fs.rename(tempDownloadPath, downloadFilePath);
|
await fs.copyFile(tempDownloadPath, downloadFilePath);
|
||||||
|
await fs.remove(tempDownloadPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
downloadFilePathUpdater.mutate({ download_id: downloadId, filepath: downloadFilePath }, {
|
downloadFilePathUpdater.mutate({ download_id: downloadId, filepath: downloadFilePath }, {
|
||||||
@@ -214,11 +297,23 @@ export default function App({ children }: { children: React.ReactNode }) {
|
|||||||
console.error("Failed to update download filepath:", error);
|
console.error("Failed to update download filepath:", error);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
downloadStatusUpdater.mutate({ download_id: downloadId, download_status: 'completed' }, {
|
||||||
|
onSuccess: (data) => {
|
||||||
|
console.log("Download status updated successfully:", data);
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['download-states'] });
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
console.error("Failed to update download status:", error);
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
command.on('error', error => {
|
command.on('error', error => {
|
||||||
console.error(`Error: ${error}`);
|
console.error(`Error: ${error}`);
|
||||||
|
setIsErrored(true);
|
||||||
|
setErroredDownloadId(downloadId);
|
||||||
});
|
});
|
||||||
|
|
||||||
command.stdout.on('data', line => {
|
command.stdout.on('data', line => {
|
||||||
@@ -380,8 +475,11 @@ export default function App({ children }: { children: React.ReactNode }) {
|
|||||||
|
|
||||||
const pauseDownload = async (downloadState: DownloadState) => {
|
const pauseDownload = async (downloadState: DownloadState) => {
|
||||||
try {
|
try {
|
||||||
|
if ((downloadState.download_status === 'downloading' && downloadState.process_id) || (downloadState.download_status === 'starting' && downloadState.process_id)) {
|
||||||
|
setIsErrorExpected(true); // Set error expected to true to handle UI state
|
||||||
console.log("Killing process with PID:", downloadState.process_id);
|
console.log("Killing process with PID:", downloadState.process_id);
|
||||||
await invoke('kill_all_process', { pid: downloadState.process_id });
|
await invoke('kill_all_process', { pid: downloadState.process_id });
|
||||||
|
}
|
||||||
downloadStatusUpdater.mutate({ download_id: downloadState.download_id, download_status: 'paused' }, {
|
downloadStatusUpdater.mutate({ download_id: downloadState.download_id, download_status: 'paused' }, {
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
console.log("Download status updated successfully:", data);
|
console.log("Download status updated successfully:", data);
|
||||||
@@ -424,6 +522,7 @@ export default function App({ children }: { children: React.ReactNode }) {
|
|||||||
const cancelDownload = async (downloadState: DownloadState) => {
|
const cancelDownload = async (downloadState: DownloadState) => {
|
||||||
try {
|
try {
|
||||||
if ((downloadState.download_status === 'downloading' && downloadState.process_id) || (downloadState.download_status === 'starting' && downloadState.process_id)) {
|
if ((downloadState.download_status === 'downloading' && downloadState.process_id) || (downloadState.download_status === 'starting' && downloadState.process_id)) {
|
||||||
|
setIsErrorExpected(true); // Set error expected to true to handle UI state
|
||||||
console.log("Killing process with PID:", downloadState.process_id);
|
console.log("Killing process with PID:", downloadState.process_id);
|
||||||
await invoke('kill_all_process', { pid: downloadState.process_id });
|
await invoke('kill_all_process', { pid: downloadState.process_id });
|
||||||
}
|
}
|
||||||
@@ -637,7 +736,7 @@ export default function App({ children }: { children: React.ReactNode }) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
initPaths();
|
initPaths();
|
||||||
}, [setPath]);
|
}, [DOWNLOAD_DIR, setPath]);
|
||||||
|
|
||||||
// Fetch app version
|
// Fetch app version
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -757,6 +856,43 @@ export default function App({ children }: { children: React.ReactNode }) {
|
|||||||
return () => clearTimeout(timeoutId);
|
return () => clearTimeout(timeoutId);
|
||||||
}, [processQueuedDownloads, ongoingDownloads, queuedDownloads]);
|
}, [processQueuedDownloads, ongoingDownloads, queuedDownloads]);
|
||||||
|
|
||||||
|
// show a toast and pause the download when yt-dlp exits unexpectedly
|
||||||
|
useEffect(() => {
|
||||||
|
if (isErrored && !isErrorExpected) {
|
||||||
|
toast({
|
||||||
|
title: "Download Failed",
|
||||||
|
description: "yt-dlp exited unexpectedly. Please try again later",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
if (erroredDownloadId) {
|
||||||
|
downloadStatusUpdater.mutate({ download_id: erroredDownloadId, download_status: 'paused' }, {
|
||||||
|
onSuccess: (data) => {
|
||||||
|
console.log("Download status updated successfully:", data);
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['download-states'] });
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
console.error("Failed to update download status:", error);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
setErroredDownloadId(null);
|
||||||
|
}
|
||||||
|
setIsErrored(false);
|
||||||
|
setIsErrorExpected(false);
|
||||||
|
}
|
||||||
|
}, [isErrored, isErrorExpected, erroredDownloadId, setIsErrored, setIsErrorExpected, setErroredDownloadId]);
|
||||||
|
|
||||||
|
// auto reset error states after 3 seconds of expecting an error
|
||||||
|
useEffect(() => {
|
||||||
|
if (isErrorExpected) {
|
||||||
|
const timeoutId = setTimeout(() => {
|
||||||
|
setIsErrored(false);
|
||||||
|
setIsErrorExpected(false);
|
||||||
|
setErroredDownloadId(null);
|
||||||
|
}, 3000);
|
||||||
|
return () => clearTimeout(timeoutId);
|
||||||
|
}
|
||||||
|
}, [isErrorExpected, setIsErrorExpected]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppContext.Provider value={{ fetchVideoMetadata, startDownload, pauseDownload, resumeDownload, cancelDownload }}>
|
<AppContext.Provider value={{ fetchVideoMetadata, startDownload, pauseDownload, resumeDownload, cancelDownload }}>
|
||||||
<ThemeProvider defaultTheme={APP_THEME || "system"} storageKey="vite-ui-theme">
|
<ThemeProvider defaultTheme={APP_THEME || "system"} storageKey="vite-ui-theme">
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ const IndeterminateProgress = React.forwardRef<
|
|||||||
<ProgressPrimitive.Indicator
|
<ProgressPrimitive.Indicator
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-full w-full flex-1 bg-primary transition-all",
|
"h-full w-full flex-1 bg-primary transition-all",
|
||||||
indeterminate && "animate-indeterminate-progress origin-left"
|
indeterminate && "animate-[indeterminate-progress_1s_infinite_linear] origin-left"
|
||||||
)}
|
)}
|
||||||
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
||||||
/>
|
/>
|
||||||
|
|||||||
59
src/components/custom/legacyToggleGroup.tsx
Normal file
59
src/components/custom/legacyToggleGroup.tsx
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
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",
|
||||||
|
})
|
||||||
|
|
||||||
|
const ToggleGroup = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ToggleGroupPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Root> &
|
||||||
|
VariantProps<typeof toggleVariants>
|
||||||
|
>(({ className, variant, size, children, ...props }, ref) => (
|
||||||
|
<ToggleGroupPrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
className={cn("flex items-center justify-center gap-1", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ToggleGroupContext.Provider value={{ variant, size }}>
|
||||||
|
{children}
|
||||||
|
</ToggleGroupContext.Provider>
|
||||||
|
</ToggleGroupPrimitive.Root>
|
||||||
|
))
|
||||||
|
|
||||||
|
ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName
|
||||||
|
|
||||||
|
const ToggleGroupItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ToggleGroupPrimitive.Item>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Item> &
|
||||||
|
VariantProps<typeof toggleVariants>
|
||||||
|
>(({ className, children, variant, size, ...props }, ref) => {
|
||||||
|
const context = React.useContext(ToggleGroupContext)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ToggleGroupPrimitive.Item
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
toggleVariants({
|
||||||
|
variant: context.variant || variant,
|
||||||
|
size: context.size || size,
|
||||||
|
}),
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</ToggleGroupPrimitive.Item>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName
|
||||||
|
|
||||||
|
export { ToggleGroup, ToggleGroupItem }
|
||||||
52
src/components/custom/slidingButton.tsx
Normal file
52
src/components/custom/slidingButton.tsx
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import React, { type ReactNode } from "react";
|
||||||
|
|
||||||
|
export const SlidingButton = ({
|
||||||
|
children,
|
||||||
|
slidingContent,
|
||||||
|
as: Tag = "button",
|
||||||
|
href,
|
||||||
|
target,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: {
|
||||||
|
children: ReactNode;
|
||||||
|
slidingContent: ReactNode;
|
||||||
|
as?: React.ElementType;
|
||||||
|
href?: string;
|
||||||
|
target?: string;
|
||||||
|
className?: string;
|
||||||
|
} & (
|
||||||
|
| React.ComponentPropsWithoutRef<"a">
|
||||||
|
| React.ComponentPropsWithoutRef<"button">
|
||||||
|
)) => {
|
||||||
|
return (
|
||||||
|
<Tag
|
||||||
|
className={cn(
|
||||||
|
"px-4 py-2 rounded-md bg-black dark:bg-white dark:text-black text-white text-center relative overflow-hidden cursor-pointer flex justify-center",
|
||||||
|
`group/sliding-button`,
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
href={href}
|
||||||
|
target={target}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
'text-center transition duration-500 flex flex-col justify-center items-center',
|
||||||
|
`group-hover/sliding-button:translate-x-60`
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'flex items-center justify-center absolute inset-0 transition duration-500 text-white z-20',
|
||||||
|
`-translate-x-60 group-hover/sliding-button:translate-x-0`
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{slidingContent}
|
||||||
|
</div>
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -29,6 +29,7 @@ export function AppSidebar() {
|
|||||||
const { open } = useSidebar();
|
const { open } = useSidebar();
|
||||||
const { downloadAndInstallAppUpdate } = useAppUpdater();
|
const { downloadAndInstallAppUpdate } = useAppUpdater();
|
||||||
const [showBadge, setShowBadge] = useState(false);
|
const [showBadge, setShowBadge] = useState(false);
|
||||||
|
const [showUpdateCard, setShowUpdateCard] = useState(false);
|
||||||
|
|
||||||
const topItems: Array<RoutesObj> = [
|
const topItems: Array<RoutesObj> = [
|
||||||
{
|
{
|
||||||
@@ -56,9 +57,11 @@ export function AppSidebar() {
|
|||||||
if (open) {
|
if (open) {
|
||||||
timeout = setTimeout(() => {
|
timeout = setTimeout(() => {
|
||||||
setShowBadge(true);
|
setShowBadge(true);
|
||||||
|
setShowUpdateCard(true);
|
||||||
}, 300);
|
}, 300);
|
||||||
} else {
|
} else {
|
||||||
setShowBadge(false);
|
setShowBadge(false);
|
||||||
|
setShowUpdateCard(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
@@ -124,7 +127,7 @@ export function AppSidebar() {
|
|||||||
<item.icon />
|
<item.icon />
|
||||||
<span>{item.title}</span>
|
<span>{item.title}</span>
|
||||||
{item.title === "Library" && ongoingDownloads.length > 0 && showBadge && (
|
{item.title === "Library" && ongoingDownloads.length > 0 && showBadge && (
|
||||||
<Badge className="absolute right-2 inset-y-auto rounded-full font-bold bg-foreground/80">{ongoingDownloads.length}</Badge>
|
<Badge className="absolute right-2 inset-y-auto h-5 min-w-5 rounded-full px-1 font-mono tabular-nums">{ongoingDownloads.length}</Badge>
|
||||||
)}
|
)}
|
||||||
</Link>
|
</Link>
|
||||||
</SidebarMenuButton>
|
</SidebarMenuButton>
|
||||||
@@ -148,13 +151,14 @@ export function AppSidebar() {
|
|||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
{appUpdate && open && (
|
{appUpdate && open && showUpdateCard && (
|
||||||
<Card>
|
<Card className="gap-4 py-0">
|
||||||
<CardHeader className="p-4 pb-0">
|
<CardHeader className="p-4 pb-0">
|
||||||
<CardTitle className="text-sm">Update Available (v{appUpdate.version})</CardTitle>
|
<CardTitle className="text-sm">Update Available (v{appUpdate.version})</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
A new version of {config.appName} is available. Please update to the latest version for the best experience.
|
A newer version of {config.appName} is available. Please update to the latest version for the best experience.
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
|
<a className="text-xs font-semibold cursor-pointer mt-1" href={`https://github.com/neosubhamoy/neodlp/releases/tag/v${appUpdate.version}`} target="_blank">✨ Read Changelog</a>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="grid gap-2.5 p-4">
|
<CardContent className="grid gap-2.5 p-4">
|
||||||
<AlertDialog>
|
<AlertDialog>
|
||||||
|
|||||||
@@ -1,55 +1,64 @@
|
|||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as AccordionPrimitive from "@radix-ui/react-accordion"
|
import * as AccordionPrimitive from "@radix-ui/react-accordion"
|
||||||
import { ChevronDown } from "lucide-react"
|
import { ChevronDownIcon } from "lucide-react"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const Accordion = AccordionPrimitive.Root
|
function Accordion({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AccordionPrimitive.Root>) {
|
||||||
|
return <AccordionPrimitive.Root data-slot="accordion" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
const AccordionItem = React.forwardRef<
|
function AccordionItem({
|
||||||
React.ElementRef<typeof AccordionPrimitive.Item>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.ComponentProps<typeof AccordionPrimitive.Item>) {
|
||||||
|
return (
|
||||||
<AccordionPrimitive.Item
|
<AccordionPrimitive.Item
|
||||||
ref={ref}
|
data-slot="accordion-item"
|
||||||
className={cn("border-b", className)}
|
className={cn("border-b last:border-b-0", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
)
|
||||||
AccordionItem.displayName = "AccordionItem"
|
}
|
||||||
|
|
||||||
const AccordionTrigger = React.forwardRef<
|
function AccordionTrigger({
|
||||||
React.ElementRef<typeof AccordionPrimitive.Trigger>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
|
children,
|
||||||
>(({ className, children, ...props }, ref) => (
|
...props
|
||||||
|
}: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
|
||||||
|
return (
|
||||||
<AccordionPrimitive.Header className="flex">
|
<AccordionPrimitive.Header className="flex">
|
||||||
<AccordionPrimitive.Trigger
|
<AccordionPrimitive.Trigger
|
||||||
ref={ref}
|
data-slot="accordion-trigger"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex flex-1 items-center justify-between py-4 text-sm font-medium transition-all hover:underline text-left [&[data-state=open]>svg]:rotate-180",
|
"focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 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
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
<ChevronDown className="h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-200" />
|
<ChevronDownIcon className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" />
|
||||||
</AccordionPrimitive.Trigger>
|
</AccordionPrimitive.Trigger>
|
||||||
</AccordionPrimitive.Header>
|
</AccordionPrimitive.Header>
|
||||||
))
|
)
|
||||||
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
|
}
|
||||||
|
|
||||||
const AccordionContent = React.forwardRef<
|
function AccordionContent({
|
||||||
React.ElementRef<typeof AccordionPrimitive.Content>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
|
children,
|
||||||
>(({ className, children, ...props }, ref) => (
|
...props
|
||||||
|
}: React.ComponentProps<typeof AccordionPrimitive.Content>) {
|
||||||
|
return (
|
||||||
<AccordionPrimitive.Content
|
<AccordionPrimitive.Content
|
||||||
ref={ref}
|
data-slot="accordion-content"
|
||||||
className="overflow-hidden text-sm data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
|
className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<div className={cn("pb-4 pt-0", className)}>{children}</div>
|
<div className={cn("pt-0 pb-4", className)}>{children}</div>
|
||||||
</AccordionPrimitive.Content>
|
</AccordionPrimitive.Content>
|
||||||
))
|
)
|
||||||
AccordionContent.displayName = AccordionPrimitive.Content.displayName
|
}
|
||||||
|
|
||||||
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
|
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
|
||||||
|
|||||||
@@ -1,128 +1,146 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
|
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import { buttonVariants } from "@/components/ui/button"
|
import { buttonVariants } from "@/components/ui/button"
|
||||||
|
|
||||||
const AlertDialog = AlertDialogPrimitive.Root
|
function AlertDialog({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
|
||||||
|
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
const AlertDialogTrigger = AlertDialogPrimitive.Trigger
|
function AlertDialogTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
|
||||||
|
return (
|
||||||
|
<AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const AlertDialogPortal = AlertDialogPrimitive.Portal
|
function AlertDialogPortal({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
|
||||||
|
return (
|
||||||
|
<AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const AlertDialogOverlay = React.forwardRef<
|
function AlertDialogOverlay({
|
||||||
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
|
||||||
|
return (
|
||||||
<AlertDialogPrimitive.Overlay
|
<AlertDialogPrimitive.Overlay
|
||||||
|
data-slot="alert-dialog-overlay"
|
||||||
className={cn(
|
className={cn(
|
||||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
"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
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
ref={ref}
|
|
||||||
/>
|
/>
|
||||||
))
|
)
|
||||||
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
|
}
|
||||||
|
|
||||||
const AlertDialogContent = React.forwardRef<
|
function AlertDialogContent({
|
||||||
React.ElementRef<typeof AlertDialogPrimitive.Content>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Content>) {
|
||||||
|
return (
|
||||||
<AlertDialogPortal>
|
<AlertDialogPortal>
|
||||||
<AlertDialogOverlay />
|
<AlertDialogOverlay />
|
||||||
<AlertDialogPrimitive.Content
|
<AlertDialogPrimitive.Content
|
||||||
ref={ref}
|
data-slot="alert-dialog-content"
|
||||||
className={cn(
|
className={cn(
|
||||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
"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
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</AlertDialogPortal>
|
</AlertDialogPortal>
|
||||||
))
|
)
|
||||||
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
|
}
|
||||||
|
|
||||||
const AlertDialogHeader = ({
|
function AlertDialogHeader({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
}: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
<div
|
<div
|
||||||
|
data-slot="alert-dialog-header"
|
||||||
|
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertDialogFooter({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="alert-dialog-footer"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex flex-col space-y-2 text-center sm:text-left",
|
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
AlertDialogHeader.displayName = "AlertDialogHeader"
|
}
|
||||||
|
|
||||||
const AlertDialogFooter = ({
|
function AlertDialogTitle({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
|
||||||
<div
|
return (
|
||||||
className={cn(
|
|
||||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
AlertDialogFooter.displayName = "AlertDialogFooter"
|
|
||||||
|
|
||||||
const AlertDialogTitle = React.forwardRef<
|
|
||||||
React.ElementRef<typeof AlertDialogPrimitive.Title>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<AlertDialogPrimitive.Title
|
<AlertDialogPrimitive.Title
|
||||||
ref={ref}
|
data-slot="alert-dialog-title"
|
||||||
className={cn("text-lg font-semibold", className)}
|
className={cn("text-lg font-semibold", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
)
|
||||||
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
|
}
|
||||||
|
|
||||||
const AlertDialogDescription = React.forwardRef<
|
function AlertDialogDescription({
|
||||||
React.ElementRef<typeof AlertDialogPrimitive.Description>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
|
||||||
|
return (
|
||||||
<AlertDialogPrimitive.Description
|
<AlertDialogPrimitive.Description
|
||||||
ref={ref}
|
data-slot="alert-dialog-description"
|
||||||
className={cn("text-sm text-muted-foreground", className)}
|
className={cn("text-muted-foreground text-sm", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
)
|
||||||
AlertDialogDescription.displayName =
|
}
|
||||||
AlertDialogPrimitive.Description.displayName
|
|
||||||
|
|
||||||
const AlertDialogAction = React.forwardRef<
|
function AlertDialogAction({
|
||||||
React.ElementRef<typeof AlertDialogPrimitive.Action>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Action>) {
|
||||||
|
return (
|
||||||
<AlertDialogPrimitive.Action
|
<AlertDialogPrimitive.Action
|
||||||
ref={ref}
|
|
||||||
className={cn(buttonVariants(), className)}
|
className={cn(buttonVariants(), className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
)
|
||||||
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
|
}
|
||||||
|
|
||||||
const AlertDialogCancel = React.forwardRef<
|
function AlertDialogCancel({
|
||||||
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) {
|
||||||
|
return (
|
||||||
<AlertDialogPrimitive.Cancel
|
<AlertDialogPrimitive.Cancel
|
||||||
ref={ref}
|
className={cn(buttonVariants({ variant: "outline" }), className)}
|
||||||
className={cn(
|
|
||||||
buttonVariants({ variant: "outline" }),
|
|
||||||
"mt-2 sm:mt-0",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
)
|
||||||
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
|
|||||||
@@ -4,13 +4,13 @@ import { cva, type VariantProps } from "class-variance-authority"
|
|||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const alertVariants = cva(
|
const alertVariants = cva(
|
||||||
"relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7",
|
"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: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default: "bg-background text-foreground",
|
default: "bg-card text-card-foreground",
|
||||||
destructive:
|
destructive:
|
||||||
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
|
"text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
@@ -19,41 +19,48 @@ const alertVariants = cva(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const Alert = React.forwardRef<
|
function Alert({
|
||||||
HTMLDivElement,
|
className,
|
||||||
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
|
variant,
|
||||||
>(({ className, variant, ...props }, ref) => (
|
...props
|
||||||
|
}: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
|
||||||
|
return (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
data-slot="alert"
|
||||||
role="alert"
|
role="alert"
|
||||||
className={cn(alertVariants({ variant }), className)}
|
className={cn(alertVariants({ variant }), className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
)
|
||||||
Alert.displayName = "Alert"
|
}
|
||||||
|
|
||||||
const AlertTitle = React.forwardRef<
|
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
HTMLParagraphElement,
|
return (
|
||||||
React.HTMLAttributes<HTMLHeadingElement>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<h5
|
|
||||||
ref={ref}
|
|
||||||
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
AlertTitle.displayName = "AlertTitle"
|
|
||||||
|
|
||||||
const AlertDescription = React.forwardRef<
|
|
||||||
HTMLParagraphElement,
|
|
||||||
React.HTMLAttributes<HTMLParagraphElement>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
data-slot="alert-title"
|
||||||
className={cn("text-sm [&_p]:leading-relaxed", className)}
|
className={cn(
|
||||||
|
"col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
|
||||||
|
className
|
||||||
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
)
|
||||||
AlertDescription.displayName = "AlertDescription"
|
}
|
||||||
|
|
||||||
|
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 }
|
export { Alert, AlertTitle, AlertDescription }
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
|
import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
|
||||||
|
|
||||||
const AspectRatio = AspectRatioPrimitive.Root
|
function AspectRatio({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AspectRatioPrimitive.Root>) {
|
||||||
|
return <AspectRatioPrimitive.Root data-slot="aspect-ratio" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
export { AspectRatio }
|
export { AspectRatio }
|
||||||
|
|||||||
@@ -1,48 +1,53 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as AvatarPrimitive from "@radix-ui/react-avatar"
|
import * as AvatarPrimitive from "@radix-ui/react-avatar"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const Avatar = React.forwardRef<
|
function Avatar({
|
||||||
React.ElementRef<typeof AvatarPrimitive.Root>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.ComponentProps<typeof AvatarPrimitive.Root>) {
|
||||||
|
return (
|
||||||
<AvatarPrimitive.Root
|
<AvatarPrimitive.Root
|
||||||
ref={ref}
|
data-slot="avatar"
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
|
"relative flex size-8 shrink-0 overflow-hidden rounded-full",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
)
|
||||||
Avatar.displayName = AvatarPrimitive.Root.displayName
|
}
|
||||||
|
|
||||||
const AvatarImage = React.forwardRef<
|
function AvatarImage({
|
||||||
React.ElementRef<typeof AvatarPrimitive.Image>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
|
||||||
|
return (
|
||||||
<AvatarPrimitive.Image
|
<AvatarPrimitive.Image
|
||||||
ref={ref}
|
data-slot="avatar-image"
|
||||||
className={cn("aspect-square h-full w-full", className)}
|
className={cn("aspect-square size-full", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
)
|
||||||
AvatarImage.displayName = AvatarPrimitive.Image.displayName
|
}
|
||||||
|
|
||||||
const AvatarFallback = React.forwardRef<
|
function AvatarFallback({
|
||||||
React.ElementRef<typeof AvatarPrimitive.Fallback>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
|
||||||
|
return (
|
||||||
<AvatarPrimitive.Fallback
|
<AvatarPrimitive.Fallback
|
||||||
ref={ref}
|
data-slot="avatar-fallback"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex h-full w-full items-center justify-center rounded-full bg-muted",
|
"bg-muted flex size-full items-center justify-center rounded-full",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
)
|
||||||
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
|
}
|
||||||
|
|
||||||
export { Avatar, AvatarImage, AvatarFallback }
|
export { Avatar, AvatarImage, AvatarFallback }
|
||||||
|
|||||||
@@ -1,20 +1,22 @@
|
|||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
|
import { Slot } from "@radix-ui/react-slot"
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const badgeVariants = cva(
|
const badgeVariants = cva(
|
||||||
"inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
"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: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default:
|
default:
|
||||||
"border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
|
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
|
||||||
secondary:
|
secondary:
|
||||||
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
|
||||||
destructive:
|
destructive:
|
||||||
"border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
|
"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",
|
outline:
|
||||||
|
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
@@ -23,13 +25,21 @@ const badgeVariants = cva(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
export interface BadgeProps
|
function Badge({
|
||||||
extends React.HTMLAttributes<HTMLDivElement>,
|
className,
|
||||||
VariantProps<typeof badgeVariants> {}
|
variant,
|
||||||
|
asChild = false,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"span"> &
|
||||||
|
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
||||||
|
const Comp = asChild ? Slot : "span"
|
||||||
|
|
||||||
function Badge({ className, variant, ...props }: BadgeProps) {
|
|
||||||
return (
|
return (
|
||||||
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
<Comp
|
||||||
|
data-slot="badge"
|
||||||
|
className={cn(badgeVariants({ variant }), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,105 +4,99 @@ import { ChevronRight, MoreHorizontal } from "lucide-react"
|
|||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const Breadcrumb = React.forwardRef<
|
function Breadcrumb({ ...props }: React.ComponentProps<"nav">) {
|
||||||
HTMLElement,
|
return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />
|
||||||
React.ComponentPropsWithoutRef<"nav"> & {
|
|
||||||
separator?: React.ReactNode
|
|
||||||
}
|
}
|
||||||
>(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />)
|
|
||||||
Breadcrumb.displayName = "Breadcrumb"
|
|
||||||
|
|
||||||
const BreadcrumbList = React.forwardRef<
|
function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
|
||||||
HTMLOListElement,
|
return (
|
||||||
React.ComponentPropsWithoutRef<"ol">
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<ol
|
<ol
|
||||||
ref={ref}
|
data-slot="breadcrumb-list"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5",
|
"text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
)
|
||||||
BreadcrumbList.displayName = "BreadcrumbList"
|
}
|
||||||
|
|
||||||
const BreadcrumbItem = React.forwardRef<
|
function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
|
||||||
HTMLLIElement,
|
return (
|
||||||
React.ComponentPropsWithoutRef<"li">
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<li
|
<li
|
||||||
ref={ref}
|
data-slot="breadcrumb-item"
|
||||||
className={cn("inline-flex items-center gap-1.5", className)}
|
className={cn("inline-flex items-center gap-1.5", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
)
|
||||||
BreadcrumbItem.displayName = "BreadcrumbItem"
|
|
||||||
|
|
||||||
const BreadcrumbLink = React.forwardRef<
|
|
||||||
HTMLAnchorElement,
|
|
||||||
React.ComponentPropsWithoutRef<"a"> & {
|
|
||||||
asChild?: boolean
|
|
||||||
}
|
}
|
||||||
>(({ asChild, className, ...props }, ref) => {
|
|
||||||
|
function BreadcrumbLink({
|
||||||
|
asChild,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"a"> & {
|
||||||
|
asChild?: boolean
|
||||||
|
}) {
|
||||||
const Comp = asChild ? Slot : "a"
|
const Comp = asChild ? Slot : "a"
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Comp
|
<Comp
|
||||||
ref={ref}
|
data-slot="breadcrumb-link"
|
||||||
className={cn("transition-colors hover:text-foreground", className)}
|
className={cn("hover:text-foreground transition-colors", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
BreadcrumbLink.displayName = "BreadcrumbLink"
|
|
||||||
|
|
||||||
const BreadcrumbPage = React.forwardRef<
|
function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
|
||||||
HTMLSpanElement,
|
return (
|
||||||
React.ComponentPropsWithoutRef<"span">
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<span
|
<span
|
||||||
ref={ref}
|
data-slot="breadcrumb-page"
|
||||||
role="link"
|
role="link"
|
||||||
aria-disabled="true"
|
aria-disabled="true"
|
||||||
aria-current="page"
|
aria-current="page"
|
||||||
className={cn("font-normal text-foreground", className)}
|
className={cn("text-foreground font-normal", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
)
|
||||||
BreadcrumbPage.displayName = "BreadcrumbPage"
|
}
|
||||||
|
|
||||||
const BreadcrumbSeparator = ({
|
function BreadcrumbSeparator({
|
||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<"li">) => (
|
}: React.ComponentProps<"li">) {
|
||||||
|
return (
|
||||||
<li
|
<li
|
||||||
|
data-slot="breadcrumb-separator"
|
||||||
role="presentation"
|
role="presentation"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
className={cn("[&>svg]:w-3.5 [&>svg]:h-3.5", className)}
|
className={cn("[&>svg]:size-3.5", className)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children ?? <ChevronRight />}
|
{children ?? <ChevronRight />}
|
||||||
</li>
|
</li>
|
||||||
)
|
)
|
||||||
BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
|
}
|
||||||
|
|
||||||
const BreadcrumbEllipsis = ({
|
function BreadcrumbEllipsis({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<"span">) => (
|
}: React.ComponentProps<"span">) {
|
||||||
|
return (
|
||||||
<span
|
<span
|
||||||
|
data-slot="breadcrumb-ellipsis"
|
||||||
role="presentation"
|
role="presentation"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
className={cn("flex h-9 w-9 items-center justify-center", className)}
|
className={cn("flex size-9 items-center justify-center", className)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<MoreHorizontal className="h-4 w-4" />
|
<MoreHorizontal className="size-4" />
|
||||||
<span className="sr-only">More</span>
|
<span className="sr-only">More</span>
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
BreadcrumbEllipsis.displayName = "BreadcrumbElipssis"
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Breadcrumb,
|
Breadcrumb,
|
||||||
|
|||||||
@@ -5,26 +5,27 @@ import { cva, type VariantProps } from "class-variance-authority"
|
|||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const buttonVariants = cva(
|
const buttonVariants = cva(
|
||||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
"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: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default:
|
default:
|
||||||
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
|
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
|
||||||
destructive:
|
destructive:
|
||||||
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
|
"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:
|
outline:
|
||||||
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
|
"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:
|
secondary:
|
||||||
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
|
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
|
||||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
ghost:
|
||||||
|
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||||
link: "text-primary underline-offset-4 hover:underline",
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
default: "h-9 px-4 py-2",
|
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||||
sm: "h-8 rounded-md px-3 text-xs",
|
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
||||||
lg: "h-10 rounded-md px-8",
|
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||||
icon: "h-9 w-9",
|
icon: "size-9",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
@@ -34,24 +35,25 @@ const buttonVariants = cva(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
export interface ButtonProps
|
function Button({
|
||||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
className,
|
||||||
VariantProps<typeof buttonVariants> {
|
variant,
|
||||||
|
size,
|
||||||
|
asChild = false,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"button"> &
|
||||||
|
VariantProps<typeof buttonVariants> & {
|
||||||
asChild?: boolean
|
asChild?: boolean
|
||||||
}
|
}) {
|
||||||
|
|
||||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
||||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
|
||||||
const Comp = asChild ? Slot : "button"
|
const Comp = asChild ? Slot : "button"
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Comp
|
<Comp
|
||||||
|
data-slot="button"
|
||||||
className={cn(buttonVariants({ variant, size, className }))}
|
className={cn(buttonVariants({ variant, size, className }))}
|
||||||
ref={ref}
|
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
|
||||||
Button.displayName = "Button"
|
|
||||||
|
|
||||||
export { Button, buttonVariants }
|
export { Button, buttonVariants }
|
||||||
|
|||||||
@@ -1,74 +1,208 @@
|
|||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import { ChevronLeft, ChevronRight } from "lucide-react"
|
import {
|
||||||
import { DayPicker } from "react-day-picker"
|
ChevronDownIcon,
|
||||||
|
ChevronLeftIcon,
|
||||||
|
ChevronRightIcon,
|
||||||
|
} from "lucide-react"
|
||||||
|
import { DayButton, DayPicker, getDefaultClassNames } from "react-day-picker"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import { buttonVariants } from "@/components/ui/button"
|
import { Button, buttonVariants } from "@/components/ui/button"
|
||||||
|
|
||||||
export type CalendarProps = React.ComponentProps<typeof DayPicker>
|
|
||||||
|
|
||||||
function Calendar({
|
function Calendar({
|
||||||
className,
|
className,
|
||||||
classNames,
|
classNames,
|
||||||
showOutsideDays = true,
|
showOutsideDays = true,
|
||||||
|
captionLayout = "label",
|
||||||
|
buttonVariant = "ghost",
|
||||||
|
formatters,
|
||||||
|
components,
|
||||||
...props
|
...props
|
||||||
}: CalendarProps) {
|
}: React.ComponentProps<typeof DayPicker> & {
|
||||||
|
buttonVariant?: React.ComponentProps<typeof Button>["variant"]
|
||||||
|
}) {
|
||||||
|
const defaultClassNames = getDefaultClassNames()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DayPicker
|
<DayPicker
|
||||||
showOutsideDays={showOutsideDays}
|
showOutsideDays={showOutsideDays}
|
||||||
className={cn("p-3", className)}
|
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={{
|
classNames={{
|
||||||
months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
|
root: cn("w-fit", defaultClassNames.root),
|
||||||
month: "space-y-4",
|
months: cn(
|
||||||
caption: "flex justify-center pt-1 relative items-center",
|
"flex gap-4 flex-col md:flex-row relative",
|
||||||
caption_label: "text-sm font-medium",
|
defaultClassNames.months
|
||||||
nav: "space-x-1 flex items-center",
|
|
||||||
nav_button: cn(
|
|
||||||
buttonVariants({ variant: "outline" }),
|
|
||||||
"h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100"
|
|
||||||
),
|
),
|
||||||
nav_button_previous: "absolute left-1",
|
month: cn("flex flex-col w-full gap-4", defaultClassNames.month),
|
||||||
nav_button_next: "absolute right-1",
|
nav: cn(
|
||||||
table: "w-full border-collapse space-y-1",
|
"flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between",
|
||||||
head_row: "flex",
|
defaultClassNames.nav
|
||||||
head_cell:
|
),
|
||||||
"text-muted-foreground rounded-md w-8 font-normal text-[0.8rem]",
|
button_previous: cn(
|
||||||
row: "flex w-full mt-2",
|
buttonVariants({ variant: buttonVariant }),
|
||||||
cell: cn(
|
"size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
|
||||||
"relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-accent [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected].day-range-end)]:rounded-r-md",
|
defaultClassNames.button_previous
|
||||||
props.mode === "range"
|
),
|
||||||
? "[&:has(>.day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md"
|
button_next: cn(
|
||||||
: "[&:has([aria-selected])]:rounded-md"
|
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 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(
|
day: cn(
|
||||||
buttonVariants({ variant: "ghost" }),
|
"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",
|
||||||
"h-8 w-8 p-0 font-normal aria-selected:opacity-100"
|
defaultClassNames.day
|
||||||
),
|
),
|
||||||
day_range_start: "day-range-start",
|
range_start: cn(
|
||||||
day_range_end: "day-range-end",
|
"rounded-l-md bg-accent",
|
||||||
day_selected:
|
defaultClassNames.range_start
|
||||||
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
|
),
|
||||||
day_today: "bg-accent text-accent-foreground",
|
range_middle: cn("rounded-none", defaultClassNames.range_middle),
|
||||||
day_outside:
|
range_end: cn("rounded-r-md bg-accent", defaultClassNames.range_end),
|
||||||
"day-outside text-muted-foreground aria-selected:bg-accent/50 aria-selected:text-muted-foreground",
|
today: cn(
|
||||||
day_disabled: "text-muted-foreground opacity-50",
|
"bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none",
|
||||||
day_range_middle:
|
defaultClassNames.today
|
||||||
"aria-selected:bg-accent aria-selected:text-accent-foreground",
|
),
|
||||||
day_hidden: "invisible",
|
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,
|
...classNames,
|
||||||
}}
|
}}
|
||||||
components={{
|
components={{
|
||||||
IconLeft: ({ className, ...props }) => (
|
Root: ({ className, rootRef, ...props }) => {
|
||||||
<ChevronLeft className={cn("h-4 w-4", className)} {...props} />
|
return (
|
||||||
),
|
<div
|
||||||
IconRight: ({ className, ...props }) => (
|
data-slot="calendar"
|
||||||
<ChevronRight className={cn("h-4 w-4", className)} {...props} />
|
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}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Calendar.displayName = "Calendar"
|
|
||||||
|
|
||||||
export { Calendar }
|
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 }
|
||||||
|
|||||||
@@ -2,75 +2,91 @@ import * as React from "react"
|
|||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const Card = React.forwardRef<
|
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
HTMLDivElement,
|
return (
|
||||||
React.HTMLAttributes<HTMLDivElement>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
data-slot="card"
|
||||||
className={cn(
|
className={cn(
|
||||||
"rounded-xl border bg-card text-card-foreground shadow",
|
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
)
|
||||||
Card.displayName = "Card"
|
}
|
||||||
|
|
||||||
const CardHeader = React.forwardRef<
|
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
HTMLDivElement,
|
return (
|
||||||
React.HTMLAttributes<HTMLDivElement>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
data-slot="card-header"
|
||||||
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
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}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
)
|
||||||
CardHeader.displayName = "CardHeader"
|
}
|
||||||
|
|
||||||
const CardTitle = React.forwardRef<
|
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
HTMLDivElement,
|
return (
|
||||||
React.HTMLAttributes<HTMLDivElement>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
data-slot="card-title"
|
||||||
className={cn("font-semibold leading-none tracking-tight", className)}
|
className={cn("leading-none font-semibold", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
)
|
||||||
CardTitle.displayName = "CardTitle"
|
}
|
||||||
|
|
||||||
const CardDescription = React.forwardRef<
|
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
HTMLDivElement,
|
return (
|
||||||
React.HTMLAttributes<HTMLDivElement>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
data-slot="card-description"
|
||||||
className={cn("text-sm text-muted-foreground", className)}
|
className={cn("text-muted-foreground text-sm", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
)
|
||||||
CardDescription.displayName = "CardDescription"
|
}
|
||||||
|
|
||||||
const CardContent = React.forwardRef<
|
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
HTMLDivElement,
|
return (
|
||||||
React.HTMLAttributes<HTMLDivElement>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
|
||||||
))
|
|
||||||
CardContent.displayName = "CardContent"
|
|
||||||
|
|
||||||
const CardFooter = React.forwardRef<
|
|
||||||
HTMLDivElement,
|
|
||||||
React.HTMLAttributes<HTMLDivElement>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
data-slot="card-action"
|
||||||
className={cn("flex items-center p-6 pt-0", className)}
|
className={cn(
|
||||||
|
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
||||||
|
className
|
||||||
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
)
|
||||||
CardFooter.displayName = "CardFooter"
|
}
|
||||||
|
|
||||||
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
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,
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import useEmblaCarousel, {
|
import useEmblaCarousel, {
|
||||||
type UseEmblaCarouselType,
|
type UseEmblaCarouselType,
|
||||||
@@ -40,12 +42,7 @@ function useCarousel() {
|
|||||||
return context
|
return context
|
||||||
}
|
}
|
||||||
|
|
||||||
const Carousel = React.forwardRef<
|
function Carousel({
|
||||||
HTMLDivElement,
|
|
||||||
React.HTMLAttributes<HTMLDivElement> & CarouselProps
|
|
||||||
>(
|
|
||||||
(
|
|
||||||
{
|
|
||||||
orientation = "horizontal",
|
orientation = "horizontal",
|
||||||
opts,
|
opts,
|
||||||
setApi,
|
setApi,
|
||||||
@@ -53,9 +50,7 @@ const Carousel = React.forwardRef<
|
|||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
},
|
}: React.ComponentProps<"div"> & CarouselProps) {
|
||||||
ref
|
|
||||||
) => {
|
|
||||||
const [carouselRef, api] = useEmblaCarousel(
|
const [carouselRef, api] = useEmblaCarousel(
|
||||||
{
|
{
|
||||||
...opts,
|
...opts,
|
||||||
@@ -67,10 +62,7 @@ const Carousel = React.forwardRef<
|
|||||||
const [canScrollNext, setCanScrollNext] = React.useState(false)
|
const [canScrollNext, setCanScrollNext] = React.useState(false)
|
||||||
|
|
||||||
const onSelect = React.useCallback((api: CarouselApi) => {
|
const onSelect = React.useCallback((api: CarouselApi) => {
|
||||||
if (!api) {
|
if (!api) return
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setCanScrollPrev(api.canScrollPrev())
|
setCanScrollPrev(api.canScrollPrev())
|
||||||
setCanScrollNext(api.canScrollNext())
|
setCanScrollNext(api.canScrollNext())
|
||||||
}, [])
|
}, [])
|
||||||
@@ -97,18 +89,12 @@ const Carousel = React.forwardRef<
|
|||||||
)
|
)
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!api || !setApi) {
|
if (!api || !setApi) return
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setApi(api)
|
setApi(api)
|
||||||
}, [api, setApi])
|
}, [api, setApi])
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!api) {
|
if (!api) return
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
onSelect(api)
|
onSelect(api)
|
||||||
api.on("reInit", onSelect)
|
api.on("reInit", onSelect)
|
||||||
api.on("select", onSelect)
|
api.on("select", onSelect)
|
||||||
@@ -133,11 +119,11 @@ const Carousel = React.forwardRef<
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
|
||||||
onKeyDownCapture={handleKeyDown}
|
onKeyDownCapture={handleKeyDown}
|
||||||
className={cn("relative", className)}
|
className={cn("relative", className)}
|
||||||
role="region"
|
role="region"
|
||||||
aria-roledescription="carousel"
|
aria-roledescription="carousel"
|
||||||
|
data-slot="carousel"
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
@@ -145,19 +131,17 @@ const Carousel = React.forwardRef<
|
|||||||
</CarouselContext.Provider>
|
</CarouselContext.Provider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
|
||||||
Carousel.displayName = "Carousel"
|
|
||||||
|
|
||||||
const CarouselContent = React.forwardRef<
|
function CarouselContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
HTMLDivElement,
|
|
||||||
React.HTMLAttributes<HTMLDivElement>
|
|
||||||
>(({ className, ...props }, ref) => {
|
|
||||||
const { carouselRef, orientation } = useCarousel()
|
const { carouselRef, orientation } = useCarousel()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={carouselRef} className="overflow-hidden">
|
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={carouselRef}
|
||||||
|
className="overflow-hidden"
|
||||||
|
data-slot="carousel-content"
|
||||||
|
>
|
||||||
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex",
|
"flex",
|
||||||
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
|
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
|
||||||
@@ -167,20 +151,16 @@ const CarouselContent = React.forwardRef<
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
CarouselContent.displayName = "CarouselContent"
|
|
||||||
|
|
||||||
const CarouselItem = React.forwardRef<
|
function CarouselItem({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
HTMLDivElement,
|
|
||||||
React.HTMLAttributes<HTMLDivElement>
|
|
||||||
>(({ className, ...props }, ref) => {
|
|
||||||
const { orientation } = useCarousel()
|
const { orientation } = useCarousel()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
|
||||||
role="group"
|
role="group"
|
||||||
aria-roledescription="slide"
|
aria-roledescription="slide"
|
||||||
|
data-slot="carousel-item"
|
||||||
className={cn(
|
className={cn(
|
||||||
"min-w-0 shrink-0 grow-0 basis-full",
|
"min-w-0 shrink-0 grow-0 basis-full",
|
||||||
orientation === "horizontal" ? "pl-4" : "pt-4",
|
orientation === "horizontal" ? "pl-4" : "pt-4",
|
||||||
@@ -189,24 +169,25 @@ const CarouselItem = React.forwardRef<
|
|||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
CarouselItem.displayName = "CarouselItem"
|
|
||||||
|
|
||||||
const CarouselPrevious = React.forwardRef<
|
function CarouselPrevious({
|
||||||
HTMLButtonElement,
|
className,
|
||||||
React.ComponentProps<typeof Button>
|
variant = "outline",
|
||||||
>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
|
size = "icon",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof Button>) {
|
||||||
const { orientation, scrollPrev, canScrollPrev } = useCarousel()
|
const { orientation, scrollPrev, canScrollPrev } = useCarousel()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
ref={ref}
|
data-slot="carousel-previous"
|
||||||
variant={variant}
|
variant={variant}
|
||||||
size={size}
|
size={size}
|
||||||
className={cn(
|
className={cn(
|
||||||
"absolute h-8 w-8 rounded-full",
|
"absolute size-8 rounded-full",
|
||||||
orientation === "horizontal"
|
orientation === "horizontal"
|
||||||
? "-left-12 top-1/2 -translate-y-1/2"
|
? "top-1/2 -left-12 -translate-y-1/2"
|
||||||
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
|
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
@@ -214,28 +195,29 @@ const CarouselPrevious = React.forwardRef<
|
|||||||
onClick={scrollPrev}
|
onClick={scrollPrev}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<ArrowLeft className="h-4 w-4" />
|
<ArrowLeft />
|
||||||
<span className="sr-only">Previous slide</span>
|
<span className="sr-only">Previous slide</span>
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
CarouselPrevious.displayName = "CarouselPrevious"
|
|
||||||
|
|
||||||
const CarouselNext = React.forwardRef<
|
function CarouselNext({
|
||||||
HTMLButtonElement,
|
className,
|
||||||
React.ComponentProps<typeof Button>
|
variant = "outline",
|
||||||
>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
|
size = "icon",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof Button>) {
|
||||||
const { orientation, scrollNext, canScrollNext } = useCarousel()
|
const { orientation, scrollNext, canScrollNext } = useCarousel()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
ref={ref}
|
data-slot="carousel-next"
|
||||||
variant={variant}
|
variant={variant}
|
||||||
size={size}
|
size={size}
|
||||||
className={cn(
|
className={cn(
|
||||||
"absolute h-8 w-8 rounded-full",
|
"absolute size-8 rounded-full",
|
||||||
orientation === "horizontal"
|
orientation === "horizontal"
|
||||||
? "-right-12 top-1/2 -translate-y-1/2"
|
? "top-1/2 -right-12 -translate-y-1/2"
|
||||||
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
|
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
@@ -243,12 +225,11 @@ const CarouselNext = React.forwardRef<
|
|||||||
onClick={scrollNext}
|
onClick={scrollNext}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<ArrowRight className="h-4 w-4" />
|
<ArrowRight />
|
||||||
<span className="sr-only">Next slide</span>
|
<span className="sr-only">Next slide</span>
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
CarouselNext.displayName = "CarouselNext"
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
type CarouselApi,
|
type CarouselApi,
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as RechartsPrimitive from "recharts"
|
import * as RechartsPrimitive from "recharts"
|
||||||
|
|
||||||
@@ -34,25 +32,28 @@ function useChart() {
|
|||||||
return context
|
return context
|
||||||
}
|
}
|
||||||
|
|
||||||
const ChartContainer = React.forwardRef<
|
function ChartContainer({
|
||||||
HTMLDivElement,
|
id,
|
||||||
React.ComponentProps<"div"> & {
|
className,
|
||||||
|
children,
|
||||||
|
config,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div"> & {
|
||||||
config: ChartConfig
|
config: ChartConfig
|
||||||
children: React.ComponentProps<
|
children: React.ComponentProps<
|
||||||
typeof RechartsPrimitive.ResponsiveContainer
|
typeof RechartsPrimitive.ResponsiveContainer
|
||||||
>["children"]
|
>["children"]
|
||||||
}
|
}) {
|
||||||
>(({ id, className, children, config, ...props }, ref) => {
|
|
||||||
const uniqueId = React.useId()
|
const uniqueId = React.useId()
|
||||||
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
|
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ChartContext.Provider value={{ config }}>
|
<ChartContext.Provider value={{ config }}>
|
||||||
<div
|
<div
|
||||||
|
data-slot="chart"
|
||||||
data-chart={chartId}
|
data-chart={chartId}
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none",
|
"[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border flex aspect-video justify-center text-xs [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -64,8 +65,7 @@ const ChartContainer = React.forwardRef<
|
|||||||
</div>
|
</div>
|
||||||
</ChartContext.Provider>
|
</ChartContext.Provider>
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
ChartContainer.displayName = "Chart"
|
|
||||||
|
|
||||||
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
|
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
|
||||||
const colorConfig = Object.entries(config).filter(
|
const colorConfig = Object.entries(config).filter(
|
||||||
@@ -102,19 +102,7 @@ ${colorConfig
|
|||||||
|
|
||||||
const ChartTooltip = RechartsPrimitive.Tooltip
|
const ChartTooltip = RechartsPrimitive.Tooltip
|
||||||
|
|
||||||
const ChartTooltipContent = React.forwardRef<
|
function ChartTooltipContent({
|
||||||
HTMLDivElement,
|
|
||||||
React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
|
|
||||||
React.ComponentProps<"div"> & {
|
|
||||||
hideLabel?: boolean
|
|
||||||
hideIndicator?: boolean
|
|
||||||
indicator?: "line" | "dot" | "dashed"
|
|
||||||
nameKey?: string
|
|
||||||
labelKey?: string
|
|
||||||
}
|
|
||||||
>(
|
|
||||||
(
|
|
||||||
{
|
|
||||||
active,
|
active,
|
||||||
payload,
|
payload,
|
||||||
className,
|
className,
|
||||||
@@ -128,9 +116,14 @@ const ChartTooltipContent = React.forwardRef<
|
|||||||
color,
|
color,
|
||||||
nameKey,
|
nameKey,
|
||||||
labelKey,
|
labelKey,
|
||||||
},
|
}: React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
|
||||||
ref
|
React.ComponentProps<"div"> & {
|
||||||
) => {
|
hideLabel?: boolean
|
||||||
|
hideIndicator?: boolean
|
||||||
|
indicator?: "line" | "dot" | "dashed"
|
||||||
|
nameKey?: string
|
||||||
|
labelKey?: string
|
||||||
|
}) {
|
||||||
const { config } = useChart()
|
const { config } = useChart()
|
||||||
|
|
||||||
const tooltipLabel = React.useMemo(() => {
|
const tooltipLabel = React.useMemo(() => {
|
||||||
@@ -139,7 +132,7 @@ const ChartTooltipContent = React.forwardRef<
|
|||||||
}
|
}
|
||||||
|
|
||||||
const [item] = payload
|
const [item] = payload
|
||||||
const key = `${labelKey || item.dataKey || item.name || "value"}`
|
const key = `${labelKey || item?.dataKey || item?.name || "value"}`
|
||||||
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
||||||
const value =
|
const value =
|
||||||
!labelKey && typeof label === "string"
|
!labelKey && typeof label === "string"
|
||||||
@@ -177,9 +170,8 @@ const ChartTooltipContent = React.forwardRef<
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
className={cn(
|
||||||
"grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl",
|
"border-border/50 bg-background grid min-w-[8rem] items-start gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -194,7 +186,7 @@ const ChartTooltipContent = React.forwardRef<
|
|||||||
<div
|
<div
|
||||||
key={item.dataKey}
|
key={item.dataKey}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
|
"[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5",
|
||||||
indicator === "dot" && "items-center"
|
indicator === "dot" && "items-center"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -208,7 +200,7 @@ const ChartTooltipContent = React.forwardRef<
|
|||||||
!hideIndicator && (
|
!hideIndicator && (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"shrink-0 rounded-[2px] border-[--color-border] bg-[--color-bg]",
|
"shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)",
|
||||||
{
|
{
|
||||||
"h-2.5 w-2.5": indicator === "dot",
|
"h-2.5 w-2.5": indicator === "dot",
|
||||||
"w-1": indicator === "line",
|
"w-1": indicator === "line",
|
||||||
@@ -239,7 +231,7 @@ const ChartTooltipContent = React.forwardRef<
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{item.value && (
|
{item.value && (
|
||||||
<span className="font-mono font-medium tabular-nums text-foreground">
|
<span className="text-foreground font-mono font-medium tabular-nums">
|
||||||
{item.value.toLocaleString()}
|
{item.value.toLocaleString()}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -253,23 +245,20 @@ const ChartTooltipContent = React.forwardRef<
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
|
||||||
ChartTooltipContent.displayName = "ChartTooltip"
|
|
||||||
|
|
||||||
const ChartLegend = RechartsPrimitive.Legend
|
const ChartLegend = RechartsPrimitive.Legend
|
||||||
|
|
||||||
const ChartLegendContent = React.forwardRef<
|
function ChartLegendContent({
|
||||||
HTMLDivElement,
|
className,
|
||||||
React.ComponentProps<"div"> &
|
hideIcon = false,
|
||||||
|
payload,
|
||||||
|
verticalAlign = "bottom",
|
||||||
|
nameKey,
|
||||||
|
}: React.ComponentProps<"div"> &
|
||||||
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
|
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
|
||||||
hideIcon?: boolean
|
hideIcon?: boolean
|
||||||
nameKey?: string
|
nameKey?: string
|
||||||
}
|
}) {
|
||||||
>(
|
|
||||||
(
|
|
||||||
{ className, hideIcon = false, payload, verticalAlign = "bottom", nameKey },
|
|
||||||
ref
|
|
||||||
) => {
|
|
||||||
const { config } = useChart()
|
const { config } = useChart()
|
||||||
|
|
||||||
if (!payload?.length) {
|
if (!payload?.length) {
|
||||||
@@ -278,7 +267,6 @@ const ChartLegendContent = React.forwardRef<
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center justify-center gap-4",
|
"flex items-center justify-center gap-4",
|
||||||
verticalAlign === "top" ? "pb-3" : "pt-3",
|
verticalAlign === "top" ? "pb-3" : "pt-3",
|
||||||
@@ -293,7 +281,7 @@ const ChartLegendContent = React.forwardRef<
|
|||||||
<div
|
<div
|
||||||
key={item.value}
|
key={item.value}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground"
|
"[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{itemConfig?.icon && !hideIcon ? (
|
{itemConfig?.icon && !hideIcon ? (
|
||||||
@@ -313,8 +301,6 @@ const ChartLegendContent = React.forwardRef<
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
|
||||||
ChartLegendContent.displayName = "ChartLegend"
|
|
||||||
|
|
||||||
// Helper to extract item config from a payload.
|
// Helper to extract item config from a payload.
|
||||||
function getPayloadConfigFromPayload(
|
function getPayloadConfigFromPayload(
|
||||||
|
|||||||
@@ -1,28 +1,32 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
|
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
|
||||||
import { Check } from "lucide-react"
|
import { CheckIcon } from "lucide-react"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const Checkbox = React.forwardRef<
|
function Checkbox({
|
||||||
React.ElementRef<typeof CheckboxPrimitive.Root>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
|
||||||
|
return (
|
||||||
<CheckboxPrimitive.Root
|
<CheckboxPrimitive.Root
|
||||||
ref={ref}
|
data-slot="checkbox"
|
||||||
className={cn(
|
className={cn(
|
||||||
"peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
|
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-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 size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<CheckboxPrimitive.Indicator
|
<CheckboxPrimitive.Indicator
|
||||||
className={cn("flex items-center justify-center text-current")}
|
data-slot="checkbox-indicator"
|
||||||
|
className="flex items-center justify-center text-current transition-none"
|
||||||
>
|
>
|
||||||
<Check className="h-4 w-4" />
|
<CheckIcon className="size-3.5" />
|
||||||
</CheckboxPrimitive.Indicator>
|
</CheckboxPrimitive.Indicator>
|
||||||
</CheckboxPrimitive.Root>
|
</CheckboxPrimitive.Root>
|
||||||
))
|
)
|
||||||
Checkbox.displayName = CheckboxPrimitive.Root.displayName
|
}
|
||||||
|
|
||||||
export { Checkbox }
|
export { Checkbox }
|
||||||
|
|||||||
@@ -1,11 +1,31 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
|
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
|
||||||
|
|
||||||
const Collapsible = CollapsiblePrimitive.Root
|
function Collapsible({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
|
||||||
|
return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
|
function CollapsibleTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {
|
||||||
|
return (
|
||||||
|
<CollapsiblePrimitive.CollapsibleTrigger
|
||||||
|
data-slot="collapsible-trigger"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
|
function CollapsibleContent({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) {
|
||||||
|
return (
|
||||||
|
<CollapsiblePrimitive.CollapsibleContent
|
||||||
|
data-slot="collapsible-content"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export { Collapsible, CollapsibleTrigger, CollapsibleContent }
|
export { Collapsible, CollapsibleTrigger, CollapsibleContent }
|
||||||
|
|||||||
@@ -1,31 +1,58 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import { type DialogProps } from "@radix-ui/react-dialog"
|
|
||||||
import { Command as CommandPrimitive } from "cmdk"
|
import { Command as CommandPrimitive } from "cmdk"
|
||||||
import { Search } from "lucide-react"
|
import { SearchIcon } from "lucide-react"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import { Dialog, DialogContent } from "@/components/ui/dialog"
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog"
|
||||||
|
|
||||||
const Command = React.forwardRef<
|
function Command({
|
||||||
React.ElementRef<typeof CommandPrimitive>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.ComponentProps<typeof CommandPrimitive>) {
|
||||||
|
return (
|
||||||
<CommandPrimitive
|
<CommandPrimitive
|
||||||
ref={ref}
|
data-slot="command"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
|
"bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
)
|
||||||
Command.displayName = CommandPrimitive.displayName
|
}
|
||||||
|
|
||||||
const CommandDialog = ({ children, ...props }: DialogProps) => {
|
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 (
|
return (
|
||||||
<Dialog {...props}>
|
<Dialog {...props}>
|
||||||
<DialogContent className="overflow-hidden p-0">
|
<DialogHeader className="sr-only">
|
||||||
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
<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}
|
{children}
|
||||||
</Command>
|
</Command>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
@@ -33,110 +60,116 @@ const CommandDialog = ({ children, ...props }: DialogProps) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const CommandInput = React.forwardRef<
|
function CommandInput({
|
||||||
React.ElementRef<typeof CommandPrimitive.Input>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.ComponentProps<typeof CommandPrimitive.Input>) {
|
||||||
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
|
return (
|
||||||
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
<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
|
<CommandPrimitive.Input
|
||||||
ref={ref}
|
data-slot="command-input"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
|
"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
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))
|
)
|
||||||
|
}
|
||||||
|
|
||||||
CommandInput.displayName = CommandPrimitive.Input.displayName
|
function CommandList({
|
||||||
|
|
||||||
const CommandList = React.forwardRef<
|
|
||||||
React.ElementRef<typeof CommandPrimitive.List>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<CommandPrimitive.List
|
|
||||||
ref={ref}
|
|
||||||
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
|
|
||||||
CommandList.displayName = CommandPrimitive.List.displayName
|
|
||||||
|
|
||||||
const CommandEmpty = React.forwardRef<
|
|
||||||
React.ElementRef<typeof CommandPrimitive.Empty>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
|
|
||||||
>((props, ref) => (
|
|
||||||
<CommandPrimitive.Empty
|
|
||||||
ref={ref}
|
|
||||||
className="py-6 text-center text-sm"
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
|
|
||||||
CommandEmpty.displayName = CommandPrimitive.Empty.displayName
|
|
||||||
|
|
||||||
const CommandGroup = React.forwardRef<
|
|
||||||
React.ElementRef<typeof CommandPrimitive.Group>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<CommandPrimitive.Group
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
|
|
||||||
CommandGroup.displayName = CommandPrimitive.Group.displayName
|
|
||||||
|
|
||||||
const CommandSeparator = React.forwardRef<
|
|
||||||
React.ElementRef<typeof CommandPrimitive.Separator>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<CommandPrimitive.Separator
|
|
||||||
ref={ref}
|
|
||||||
className={cn("-mx-1 h-px bg-border", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
CommandSeparator.displayName = CommandPrimitive.Separator.displayName
|
|
||||||
|
|
||||||
const CommandItem = React.forwardRef<
|
|
||||||
React.ElementRef<typeof CommandPrimitive.Item>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<CommandPrimitive.Item
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"relative flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
|
|
||||||
CommandItem.displayName = CommandPrimitive.Item.displayName
|
|
||||||
|
|
||||||
const CommandShortcut = ({
|
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
}: React.ComponentProps<typeof CommandPrimitive.List>) {
|
||||||
return (
|
return (
|
||||||
<span
|
<CommandPrimitive.List
|
||||||
|
data-slot="command-list"
|
||||||
className={cn(
|
className={cn(
|
||||||
"ml-auto text-xs tracking-widest text-muted-foreground",
|
"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
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
CommandShortcut.displayName = "CommandShortcut"
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Command,
|
Command,
|
||||||
|
|||||||
@@ -1,183 +1,237 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"
|
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"
|
||||||
import { Check, ChevronRight, Circle } from "lucide-react"
|
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const ContextMenu = ContextMenuPrimitive.Root
|
function ContextMenu({
|
||||||
|
...props
|
||||||
const ContextMenuTrigger = ContextMenuPrimitive.Trigger
|
}: React.ComponentProps<typeof ContextMenuPrimitive.Root>) {
|
||||||
|
return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} />
|
||||||
const ContextMenuGroup = ContextMenuPrimitive.Group
|
|
||||||
|
|
||||||
const ContextMenuPortal = ContextMenuPrimitive.Portal
|
|
||||||
|
|
||||||
const ContextMenuSub = ContextMenuPrimitive.Sub
|
|
||||||
|
|
||||||
const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup
|
|
||||||
|
|
||||||
const ContextMenuSubTrigger = React.forwardRef<
|
|
||||||
React.ElementRef<typeof ContextMenuPrimitive.SubTrigger>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger> & {
|
|
||||||
inset?: boolean
|
|
||||||
}
|
}
|
||||||
>(({ className, inset, children, ...props }, ref) => (
|
|
||||||
|
function ContextMenuTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ContextMenuPrimitive.Trigger>) {
|
||||||
|
return (
|
||||||
|
<ContextMenuPrimitive.Trigger data-slot="context-menu-trigger" {...props} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContextMenuGroup({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ContextMenuPrimitive.Group>) {
|
||||||
|
return (
|
||||||
|
<ContextMenuPrimitive.Group data-slot="context-menu-group" {...props} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContextMenuPortal({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ContextMenuPrimitive.Portal>) {
|
||||||
|
return (
|
||||||
|
<ContextMenuPrimitive.Portal data-slot="context-menu-portal" {...props} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContextMenuSub({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ContextMenuPrimitive.Sub>) {
|
||||||
|
return <ContextMenuPrimitive.Sub data-slot="context-menu-sub" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContextMenuRadioGroup({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ContextMenuPrimitive.RadioGroup>) {
|
||||||
|
return (
|
||||||
|
<ContextMenuPrimitive.RadioGroup
|
||||||
|
data-slot="context-menu-radio-group"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContextMenuSubTrigger({
|
||||||
|
className,
|
||||||
|
inset,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ContextMenuPrimitive.SubTrigger> & {
|
||||||
|
inset?: boolean
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
<ContextMenuPrimitive.SubTrigger
|
<ContextMenuPrimitive.SubTrigger
|
||||||
ref={ref}
|
data-slot="context-menu-sub-trigger"
|
||||||
|
data-inset={inset}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
|
"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 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
inset && "pl-8",
|
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
<ChevronRight className="ml-auto h-4 w-4" />
|
<ChevronRightIcon className="ml-auto" />
|
||||||
</ContextMenuPrimitive.SubTrigger>
|
</ContextMenuPrimitive.SubTrigger>
|
||||||
))
|
)
|
||||||
ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName
|
|
||||||
|
|
||||||
const ContextMenuSubContent = React.forwardRef<
|
|
||||||
React.ElementRef<typeof ContextMenuPrimitive.SubContent>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubContent>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<ContextMenuPrimitive.SubContent
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName
|
|
||||||
|
|
||||||
const ContextMenuContent = React.forwardRef<
|
|
||||||
React.ElementRef<typeof ContextMenuPrimitive.Content>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<ContextMenuPrimitive.Portal>
|
|
||||||
<ContextMenuPrimitive.Content
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
</ContextMenuPrimitive.Portal>
|
|
||||||
))
|
|
||||||
ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName
|
|
||||||
|
|
||||||
const ContextMenuItem = React.forwardRef<
|
|
||||||
React.ElementRef<typeof ContextMenuPrimitive.Item>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & {
|
|
||||||
inset?: boolean
|
|
||||||
}
|
}
|
||||||
>(({ className, inset, ...props }, ref) => (
|
|
||||||
<ContextMenuPrimitive.Item
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
||||||
inset && "pl-8",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName
|
|
||||||
|
|
||||||
const ContextMenuCheckboxItem = React.forwardRef<
|
function ContextMenuSubContent({
|
||||||
React.ElementRef<typeof ContextMenuPrimitive.CheckboxItem>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.CheckboxItem>
|
|
||||||
>(({ className, children, checked, ...props }, ref) => (
|
|
||||||
<ContextMenuPrimitive.CheckboxItem
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
checked={checked}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
||||||
<ContextMenuPrimitive.ItemIndicator>
|
|
||||||
<Check className="h-4 w-4" />
|
|
||||||
</ContextMenuPrimitive.ItemIndicator>
|
|
||||||
</span>
|
|
||||||
{children}
|
|
||||||
</ContextMenuPrimitive.CheckboxItem>
|
|
||||||
))
|
|
||||||
ContextMenuCheckboxItem.displayName =
|
|
||||||
ContextMenuPrimitive.CheckboxItem.displayName
|
|
||||||
|
|
||||||
const ContextMenuRadioItem = React.forwardRef<
|
|
||||||
React.ElementRef<typeof ContextMenuPrimitive.RadioItem>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.RadioItem>
|
|
||||||
>(({ className, children, ...props }, ref) => (
|
|
||||||
<ContextMenuPrimitive.RadioItem
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
||||||
<ContextMenuPrimitive.ItemIndicator>
|
|
||||||
<Circle className="h-4 w-4 fill-current" />
|
|
||||||
</ContextMenuPrimitive.ItemIndicator>
|
|
||||||
</span>
|
|
||||||
{children}
|
|
||||||
</ContextMenuPrimitive.RadioItem>
|
|
||||||
))
|
|
||||||
ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName
|
|
||||||
|
|
||||||
const ContextMenuLabel = React.forwardRef<
|
|
||||||
React.ElementRef<typeof ContextMenuPrimitive.Label>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label> & {
|
|
||||||
inset?: boolean
|
|
||||||
}
|
|
||||||
>(({ className, inset, ...props }, ref) => (
|
|
||||||
<ContextMenuPrimitive.Label
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"px-2 py-1.5 text-sm font-semibold text-foreground",
|
|
||||||
inset && "pl-8",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName
|
|
||||||
|
|
||||||
const ContextMenuSeparator = React.forwardRef<
|
|
||||||
React.ElementRef<typeof ContextMenuPrimitive.Separator>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Separator>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<ContextMenuPrimitive.Separator
|
|
||||||
ref={ref}
|
|
||||||
className={cn("-mx-1 my-1 h-px bg-border", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName
|
|
||||||
|
|
||||||
const ContextMenuShortcut = ({
|
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
}: React.ComponentProps<typeof ContextMenuPrimitive.SubContent>) {
|
||||||
return (
|
return (
|
||||||
<span
|
<ContextMenuPrimitive.SubContent
|
||||||
|
data-slot="context-menu-sub-content"
|
||||||
className={cn(
|
className={cn(
|
||||||
"ml-auto text-xs tracking-widest text-muted-foreground",
|
"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-context-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContextMenuContent({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ContextMenuPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<ContextMenuPrimitive.Portal>
|
||||||
|
<ContextMenuPrimitive.Content
|
||||||
|
data-slot="context-menu-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 max-h-(--radix-context-menu-content-available-height) min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</ContextMenuPrimitive.Portal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContextMenuItem({
|
||||||
|
className,
|
||||||
|
inset,
|
||||||
|
variant = "default",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ContextMenuPrimitive.Item> & {
|
||||||
|
inset?: boolean
|
||||||
|
variant?: "default" | "destructive"
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<ContextMenuPrimitive.Item
|
||||||
|
data-slot="context-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 ContextMenuCheckboxItem({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
checked,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ContextMenuPrimitive.CheckboxItem>) {
|
||||||
|
return (
|
||||||
|
<ContextMenuPrimitive.CheckboxItem
|
||||||
|
data-slot="context-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">
|
||||||
|
<ContextMenuPrimitive.ItemIndicator>
|
||||||
|
<CheckIcon className="size-4" />
|
||||||
|
</ContextMenuPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
{children}
|
||||||
|
</ContextMenuPrimitive.CheckboxItem>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContextMenuRadioItem({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ContextMenuPrimitive.RadioItem>) {
|
||||||
|
return (
|
||||||
|
<ContextMenuPrimitive.RadioItem
|
||||||
|
data-slot="context-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">
|
||||||
|
<ContextMenuPrimitive.ItemIndicator>
|
||||||
|
<CircleIcon className="size-2 fill-current" />
|
||||||
|
</ContextMenuPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
{children}
|
||||||
|
</ContextMenuPrimitive.RadioItem>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContextMenuLabel({
|
||||||
|
className,
|
||||||
|
inset,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ContextMenuPrimitive.Label> & {
|
||||||
|
inset?: boolean
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<ContextMenuPrimitive.Label
|
||||||
|
data-slot="context-menu-label"
|
||||||
|
data-inset={inset}
|
||||||
|
className={cn(
|
||||||
|
"text-foreground px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContextMenuSeparator({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ContextMenuPrimitive.Separator>) {
|
||||||
|
return (
|
||||||
|
<ContextMenuPrimitive.Separator
|
||||||
|
data-slot="context-menu-separator"
|
||||||
|
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContextMenuShortcut({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"span">) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
data-slot="context-menu-shortcut"
|
||||||
|
className={cn(
|
||||||
|
"text-muted-foreground ml-auto text-xs tracking-widest",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
ContextMenuShortcut.displayName = "ContextMenuShortcut"
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
ContextMenu,
|
ContextMenu,
|
||||||
|
|||||||
@@ -1,122 +1,141 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||||
import { X } from "lucide-react"
|
import { XIcon } from "lucide-react"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const Dialog = DialogPrimitive.Root
|
function Dialog({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
||||||
|
return <DialogPrimitive.Root data-slot="dialog" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
const DialogTrigger = DialogPrimitive.Trigger
|
function DialogTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
||||||
|
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
const DialogPortal = DialogPrimitive.Portal
|
function DialogPortal({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
||||||
|
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
const DialogClose = DialogPrimitive.Close
|
function DialogClose({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
||||||
|
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
const DialogOverlay = React.forwardRef<
|
function DialogOverlay({
|
||||||
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
|
||||||
|
return (
|
||||||
<DialogPrimitive.Overlay
|
<DialogPrimitive.Overlay
|
||||||
ref={ref}
|
data-slot="dialog-overlay"
|
||||||
className={cn(
|
className={cn(
|
||||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
"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
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
)
|
||||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
|
}
|
||||||
|
|
||||||
const DialogContent = React.forwardRef<
|
function DialogContent({
|
||||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
children,
|
||||||
>(({ className, children, ...props }, ref) => (
|
showCloseButton = true,
|
||||||
<DialogPortal>
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
||||||
|
showCloseButton?: boolean
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<DialogPortal data-slot="dialog-portal">
|
||||||
<DialogOverlay />
|
<DialogOverlay />
|
||||||
<DialogPrimitive.Content
|
<DialogPrimitive.Content
|
||||||
ref={ref}
|
data-slot="dialog-content"
|
||||||
className={cn(
|
className={cn(
|
||||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
"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
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
{showCloseButton && (
|
||||||
<X className="h-4 w-4" />
|
<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>
|
<span className="sr-only">Close</span>
|
||||||
</DialogPrimitive.Close>
|
</DialogPrimitive.Close>
|
||||||
|
)}
|
||||||
</DialogPrimitive.Content>
|
</DialogPrimitive.Content>
|
||||||
</DialogPortal>
|
</DialogPortal>
|
||||||
))
|
)
|
||||||
DialogContent.displayName = DialogPrimitive.Content.displayName
|
}
|
||||||
|
|
||||||
const DialogHeader = ({
|
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
className,
|
return (
|
||||||
...props
|
|
||||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
|
||||||
<div
|
<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(
|
className={cn(
|
||||||
"flex flex-col space-y-1.5 text-center sm:text-left",
|
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
DialogHeader.displayName = "DialogHeader"
|
}
|
||||||
|
|
||||||
const DialogFooter = ({
|
function DialogTitle({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
||||||
<div
|
return (
|
||||||
className={cn(
|
|
||||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
DialogFooter.displayName = "DialogFooter"
|
|
||||||
|
|
||||||
const DialogTitle = React.forwardRef<
|
|
||||||
React.ElementRef<typeof DialogPrimitive.Title>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<DialogPrimitive.Title
|
<DialogPrimitive.Title
|
||||||
ref={ref}
|
data-slot="dialog-title"
|
||||||
className={cn(
|
className={cn("text-lg leading-none font-semibold", className)}
|
||||||
"text-lg font-semibold leading-none tracking-tight",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
)
|
||||||
DialogTitle.displayName = DialogPrimitive.Title.displayName
|
}
|
||||||
|
|
||||||
const DialogDescription = React.forwardRef<
|
function DialogDescription({
|
||||||
React.ElementRef<typeof DialogPrimitive.Description>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
|
||||||
|
return (
|
||||||
<DialogPrimitive.Description
|
<DialogPrimitive.Description
|
||||||
ref={ref}
|
data-slot="dialog-description"
|
||||||
className={cn("text-sm text-muted-foreground", className)}
|
className={cn("text-muted-foreground text-sm", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
)
|
||||||
DialogDescription.displayName = DialogPrimitive.Description.displayName
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogPortal,
|
|
||||||
DialogOverlay,
|
|
||||||
DialogTrigger,
|
|
||||||
DialogClose,
|
DialogClose,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogHeader,
|
|
||||||
DialogFooter,
|
|
||||||
DialogTitle,
|
|
||||||
DialogDescription,
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogOverlay,
|
||||||
|
DialogPortal,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,108 +1,123 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import { Drawer as DrawerPrimitive } from "vaul"
|
import { Drawer as DrawerPrimitive } from "vaul"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const Drawer = ({
|
function Drawer({
|
||||||
shouldScaleBackground = true,
|
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DrawerPrimitive.Root>) => (
|
}: React.ComponentProps<typeof DrawerPrimitive.Root>) {
|
||||||
<DrawerPrimitive.Root
|
return <DrawerPrimitive.Root data-slot="drawer" {...props} />
|
||||||
shouldScaleBackground={shouldScaleBackground}
|
}
|
||||||
|
|
||||||
|
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}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
Drawer.displayName = "Drawer"
|
}
|
||||||
|
|
||||||
const DrawerTrigger = DrawerPrimitive.Trigger
|
function DrawerContent({
|
||||||
|
className,
|
||||||
const DrawerPortal = DrawerPrimitive.Portal
|
children,
|
||||||
|
...props
|
||||||
const DrawerClose = DrawerPrimitive.Close
|
}: React.ComponentProps<typeof DrawerPrimitive.Content>) {
|
||||||
|
return (
|
||||||
const DrawerOverlay = React.forwardRef<
|
<DrawerPortal data-slot="drawer-portal">
|
||||||
React.ElementRef<typeof DrawerPrimitive.Overlay>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<DrawerPrimitive.Overlay
|
|
||||||
ref={ref}
|
|
||||||
className={cn("fixed inset-0 z-50 bg-black/80", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName
|
|
||||||
|
|
||||||
const DrawerContent = React.forwardRef<
|
|
||||||
React.ElementRef<typeof DrawerPrimitive.Content>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content>
|
|
||||||
>(({ className, children, ...props }, ref) => (
|
|
||||||
<DrawerPortal>
|
|
||||||
<DrawerOverlay />
|
<DrawerOverlay />
|
||||||
<DrawerPrimitive.Content
|
<DrawerPrimitive.Content
|
||||||
ref={ref}
|
data-slot="drawer-content"
|
||||||
className={cn(
|
className={cn(
|
||||||
"fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background",
|
"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
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<div className="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" />
|
<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}
|
{children}
|
||||||
</DrawerPrimitive.Content>
|
</DrawerPrimitive.Content>
|
||||||
</DrawerPortal>
|
</DrawerPortal>
|
||||||
))
|
|
||||||
DrawerContent.displayName = "DrawerContent"
|
|
||||||
|
|
||||||
const DrawerHeader = ({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
|
||||||
<div
|
|
||||||
className={cn("grid gap-1.5 p-4 text-center sm:text-left", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
)
|
||||||
DrawerHeader.displayName = "DrawerHeader"
|
}
|
||||||
|
|
||||||
const DrawerFooter = ({
|
function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
className,
|
return (
|
||||||
...props
|
|
||||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
|
||||||
<div
|
<div
|
||||||
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
data-slot="drawer-header"
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
DrawerFooter.displayName = "DrawerFooter"
|
|
||||||
|
|
||||||
const DrawerTitle = React.forwardRef<
|
|
||||||
React.ElementRef<typeof DrawerPrimitive.Title>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<DrawerPrimitive.Title
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-lg font-semibold leading-none tracking-tight",
|
"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
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
)
|
||||||
DrawerTitle.displayName = DrawerPrimitive.Title.displayName
|
}
|
||||||
|
|
||||||
const DrawerDescription = React.forwardRef<
|
function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
React.ElementRef<typeof DrawerPrimitive.Description>,
|
return (
|
||||||
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description>
|
<div
|
||||||
>(({ className, ...props }, ref) => (
|
data-slot="drawer-footer"
|
||||||
<DrawerPrimitive.Description
|
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
||||||
ref={ref}
|
|
||||||
className={cn("text-sm text-muted-foreground", className)}
|
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
)
|
||||||
DrawerDescription.displayName = DrawerPrimitive.Description.displayName
|
}
|
||||||
|
|
||||||
|
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 {
|
export {
|
||||||
Drawer,
|
Drawer,
|
||||||
|
|||||||
@@ -1,199 +1,257 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
||||||
import { Check, ChevronRight, Circle } from "lucide-react"
|
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const DropdownMenu = DropdownMenuPrimitive.Root
|
function DropdownMenu({
|
||||||
|
...props
|
||||||
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
|
||||||
|
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
|
||||||
const DropdownMenuGroup = DropdownMenuPrimitive.Group
|
|
||||||
|
|
||||||
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
|
|
||||||
|
|
||||||
const DropdownMenuSub = DropdownMenuPrimitive.Sub
|
|
||||||
|
|
||||||
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
|
|
||||||
|
|
||||||
const DropdownMenuSubTrigger = React.forwardRef<
|
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
|
||||||
inset?: boolean
|
|
||||||
}
|
}
|
||||||
>(({ className, inset, children, ...props }, ref) => (
|
|
||||||
<DropdownMenuPrimitive.SubTrigger
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
|
||||||
inset && "pl-8",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
<ChevronRight className="ml-auto" />
|
|
||||||
</DropdownMenuPrimitive.SubTrigger>
|
|
||||||
))
|
|
||||||
DropdownMenuSubTrigger.displayName =
|
|
||||||
DropdownMenuPrimitive.SubTrigger.displayName
|
|
||||||
|
|
||||||
const DropdownMenuSubContent = React.forwardRef<
|
function DropdownMenuPortal({
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
...props
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
|
||||||
>(({ className, ...props }, ref) => (
|
return (
|
||||||
<DropdownMenuPrimitive.SubContent
|
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
|
||||||
ref={ref}
|
)
|
||||||
className={cn(
|
}
|
||||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
|
||||||
className
|
function DropdownMenuTrigger({
|
||||||
)}
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Trigger
|
||||||
|
data-slot="dropdown-menu-trigger"
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
)
|
||||||
DropdownMenuSubContent.displayName =
|
}
|
||||||
DropdownMenuPrimitive.SubContent.displayName
|
|
||||||
|
|
||||||
const DropdownMenuContent = React.forwardRef<
|
function DropdownMenuContent({
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
sideOffset = 4,
|
||||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
|
||||||
|
return (
|
||||||
<DropdownMenuPrimitive.Portal>
|
<DropdownMenuPrimitive.Portal>
|
||||||
<DropdownMenuPrimitive.Content
|
<DropdownMenuPrimitive.Content
|
||||||
ref={ref}
|
data-slot="dropdown-menu-content"
|
||||||
sideOffset={sideOffset}
|
sideOffset={sideOffset}
|
||||||
className={cn(
|
className={cn(
|
||||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
|
"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",
|
||||||
"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",
|
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</DropdownMenuPrimitive.Portal>
|
</DropdownMenuPrimitive.Portal>
|
||||||
))
|
)
|
||||||
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
|
|
||||||
|
|
||||||
const DropdownMenuItem = React.forwardRef<
|
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
|
||||||
inset?: boolean
|
|
||||||
}
|
}
|
||||||
>(({ className, inset, ...props }, ref) => (
|
|
||||||
|
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
|
<DropdownMenuPrimitive.Item
|
||||||
ref={ref}
|
data-slot="dropdown-menu-item"
|
||||||
|
data-inset={inset}
|
||||||
|
data-variant={variant}
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0",
|
"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",
|
||||||
inset && "pl-8",
|
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
)
|
||||||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
|
}
|
||||||
|
|
||||||
const DropdownMenuCheckboxItem = React.forwardRef<
|
function DropdownMenuCheckboxItem({
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
children,
|
||||||
>(({ className, children, checked, ...props }, ref) => (
|
checked,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
|
||||||
|
return (
|
||||||
<DropdownMenuPrimitive.CheckboxItem
|
<DropdownMenuPrimitive.CheckboxItem
|
||||||
ref={ref}
|
data-slot="dropdown-menu-checkbox-item"
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
"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
|
className
|
||||||
)}
|
)}
|
||||||
checked={checked}
|
checked={checked}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||||
<DropdownMenuPrimitive.ItemIndicator>
|
<DropdownMenuPrimitive.ItemIndicator>
|
||||||
<Check className="h-4 w-4" />
|
<CheckIcon className="size-4" />
|
||||||
</DropdownMenuPrimitive.ItemIndicator>
|
</DropdownMenuPrimitive.ItemIndicator>
|
||||||
</span>
|
</span>
|
||||||
{children}
|
{children}
|
||||||
</DropdownMenuPrimitive.CheckboxItem>
|
</DropdownMenuPrimitive.CheckboxItem>
|
||||||
))
|
)
|
||||||
DropdownMenuCheckboxItem.displayName =
|
|
||||||
DropdownMenuPrimitive.CheckboxItem.displayName
|
|
||||||
|
|
||||||
const DropdownMenuRadioItem = React.forwardRef<
|
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
|
||||||
>(({ className, children, ...props }, ref) => (
|
|
||||||
<DropdownMenuPrimitive.RadioItem
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
||||||
<DropdownMenuPrimitive.ItemIndicator>
|
|
||||||
<Circle className="h-2 w-2 fill-current" />
|
|
||||||
</DropdownMenuPrimitive.ItemIndicator>
|
|
||||||
</span>
|
|
||||||
{children}
|
|
||||||
</DropdownMenuPrimitive.RadioItem>
|
|
||||||
))
|
|
||||||
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
|
|
||||||
|
|
||||||
const DropdownMenuLabel = React.forwardRef<
|
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
|
||||||
inset?: boolean
|
|
||||||
}
|
}
|
||||||
>(({ className, inset, ...props }, ref) => (
|
|
||||||
<DropdownMenuPrimitive.Label
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"px-2 py-1.5 text-sm font-semibold",
|
|
||||||
inset && "pl-8",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
|
|
||||||
|
|
||||||
const DropdownMenuSeparator = React.forwardRef<
|
function DropdownMenuRadioGroup({
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<DropdownMenuPrimitive.Separator
|
|
||||||
ref={ref}
|
|
||||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
|
|
||||||
|
|
||||||
const DropdownMenuShortcut = ({
|
|
||||||
className,
|
|
||||||
...props
|
...props
|
||||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
|
||||||
return (
|
return (
|
||||||
<span
|
<DropdownMenuPrimitive.RadioGroup
|
||||||
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
|
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuSubContent({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.SubContent
|
||||||
|
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}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
|
DropdownMenuPortal,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
|
DropdownMenuGroup,
|
||||||
|
DropdownMenuLabel,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuCheckboxItem,
|
DropdownMenuCheckboxItem,
|
||||||
|
DropdownMenuRadioGroup,
|
||||||
DropdownMenuRadioItem,
|
DropdownMenuRadioItem,
|
||||||
DropdownMenuLabel,
|
|
||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuShortcut,
|
DropdownMenuShortcut,
|
||||||
DropdownMenuGroup,
|
|
||||||
DropdownMenuPortal,
|
|
||||||
DropdownMenuSub,
|
DropdownMenuSub,
|
||||||
DropdownMenuSubContent,
|
|
||||||
DropdownMenuSubTrigger,
|
DropdownMenuSubTrigger,
|
||||||
DropdownMenuRadioGroup,
|
DropdownMenuSubContent,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||||
import { Slot } from "@radix-ui/react-slot"
|
import { Slot } from "@radix-ui/react-slot"
|
||||||
import {
|
import {
|
||||||
Controller,
|
Controller,
|
||||||
ControllerProps,
|
|
||||||
FieldPath,
|
|
||||||
FieldValues,
|
|
||||||
FormProvider,
|
FormProvider,
|
||||||
useFormContext,
|
useFormContext,
|
||||||
|
useFormState,
|
||||||
|
type ControllerProps,
|
||||||
|
type FieldPath,
|
||||||
|
type FieldValues,
|
||||||
} from "react-hook-form"
|
} from "react-hook-form"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
@@ -19,7 +18,7 @@ const Form = FormProvider
|
|||||||
|
|
||||||
type FormFieldContextValue<
|
type FormFieldContextValue<
|
||||||
TFieldValues extends FieldValues = FieldValues,
|
TFieldValues extends FieldValues = FieldValues,
|
||||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
|
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||||
> = {
|
> = {
|
||||||
name: TName
|
name: TName
|
||||||
}
|
}
|
||||||
@@ -30,7 +29,7 @@ const FormFieldContext = React.createContext<FormFieldContextValue>(
|
|||||||
|
|
||||||
const FormField = <
|
const FormField = <
|
||||||
TFieldValues extends FieldValues = FieldValues,
|
TFieldValues extends FieldValues = FieldValues,
|
||||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
|
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||||
>({
|
>({
|
||||||
...props
|
...props
|
||||||
}: ControllerProps<TFieldValues, TName>) => {
|
}: ControllerProps<TFieldValues, TName>) => {
|
||||||
@@ -44,8 +43,8 @@ const FormField = <
|
|||||||
const useFormField = () => {
|
const useFormField = () => {
|
||||||
const fieldContext = React.useContext(FormFieldContext)
|
const fieldContext = React.useContext(FormFieldContext)
|
||||||
const itemContext = React.useContext(FormItemContext)
|
const itemContext = React.useContext(FormItemContext)
|
||||||
const { getFieldState, formState } = useFormContext()
|
const { getFieldState } = useFormContext()
|
||||||
|
const formState = useFormState({ name: fieldContext.name })
|
||||||
const fieldState = getFieldState(fieldContext.name, formState)
|
const fieldState = getFieldState(fieldContext.name, formState)
|
||||||
|
|
||||||
if (!fieldContext) {
|
if (!fieldContext) {
|
||||||
@@ -72,46 +71,43 @@ const FormItemContext = React.createContext<FormItemContextValue>(
|
|||||||
{} as FormItemContextValue
|
{} as FormItemContextValue
|
||||||
)
|
)
|
||||||
|
|
||||||
const FormItem = React.forwardRef<
|
function FormItem({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
HTMLDivElement,
|
|
||||||
React.HTMLAttributes<HTMLDivElement>
|
|
||||||
>(({ className, ...props }, ref) => {
|
|
||||||
const id = React.useId()
|
const id = React.useId()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormItemContext.Provider value={{ id }}>
|
<FormItemContext.Provider value={{ id }}>
|
||||||
<div ref={ref} className={cn("space-y-2", className)} {...props} />
|
<div
|
||||||
|
data-slot="form-item"
|
||||||
|
className={cn("grid gap-2", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
</FormItemContext.Provider>
|
</FormItemContext.Provider>
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
FormItem.displayName = "FormItem"
|
|
||||||
|
|
||||||
const FormLabel = React.forwardRef<
|
function FormLabel({
|
||||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
|
...props
|
||||||
>(({ className, ...props }, ref) => {
|
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
||||||
const { error, formItemId } = useFormField()
|
const { error, formItemId } = useFormField()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Label
|
<Label
|
||||||
ref={ref}
|
data-slot="form-label"
|
||||||
className={cn(error && "text-destructive", className)}
|
data-error={!!error}
|
||||||
|
className={cn("data-[error=true]:text-destructive", className)}
|
||||||
htmlFor={formItemId}
|
htmlFor={formItemId}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
FormLabel.displayName = "FormLabel"
|
|
||||||
|
|
||||||
const FormControl = React.forwardRef<
|
function FormControl({ ...props }: React.ComponentProps<typeof Slot>) {
|
||||||
React.ElementRef<typeof Slot>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof Slot>
|
|
||||||
>(({ ...props }, ref) => {
|
|
||||||
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
|
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Slot
|
<Slot
|
||||||
ref={ref}
|
data-slot="form-control"
|
||||||
id={formItemId}
|
id={formItemId}
|
||||||
aria-describedby={
|
aria-describedby={
|
||||||
!error
|
!error
|
||||||
@@ -122,32 +118,24 @@ const FormControl = React.forwardRef<
|
|||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
FormControl.displayName = "FormControl"
|
|
||||||
|
|
||||||
const FormDescription = React.forwardRef<
|
function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
|
||||||
HTMLParagraphElement,
|
|
||||||
React.HTMLAttributes<HTMLParagraphElement>
|
|
||||||
>(({ className, ...props }, ref) => {
|
|
||||||
const { formDescriptionId } = useFormField()
|
const { formDescriptionId } = useFormField()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<p
|
<p
|
||||||
ref={ref}
|
data-slot="form-description"
|
||||||
id={formDescriptionId}
|
id={formDescriptionId}
|
||||||
className={cn("text-[0.8rem] text-muted-foreground", className)}
|
className={cn("text-muted-foreground text-sm", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
FormDescription.displayName = "FormDescription"
|
|
||||||
|
|
||||||
const FormMessage = React.forwardRef<
|
function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
|
||||||
HTMLParagraphElement,
|
|
||||||
React.HTMLAttributes<HTMLParagraphElement>
|
|
||||||
>(({ className, children, ...props }, ref) => {
|
|
||||||
const { error, formMessageId } = useFormField()
|
const { error, formMessageId } = useFormField()
|
||||||
const body = error ? String(error?.message) : children
|
const body = error ? String(error?.message ?? "") : props.children
|
||||||
|
|
||||||
if (!body) {
|
if (!body) {
|
||||||
return null
|
return null
|
||||||
@@ -155,16 +143,15 @@ const FormMessage = React.forwardRef<
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<p
|
<p
|
||||||
ref={ref}
|
data-slot="form-message"
|
||||||
id={formMessageId}
|
id={formMessageId}
|
||||||
className={cn("text-[0.8rem] font-medium text-destructive", className)}
|
className={cn("text-destructive text-sm", className)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{body}
|
{body}
|
||||||
</p>
|
</p>
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
FormMessage.displayName = "FormMessage"
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
useFormField,
|
useFormField,
|
||||||
|
|||||||
@@ -1,29 +1,42 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as HoverCardPrimitive from "@radix-ui/react-hover-card"
|
import * as HoverCardPrimitive from "@radix-ui/react-hover-card"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const HoverCard = HoverCardPrimitive.Root
|
function HoverCard({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof HoverCardPrimitive.Root>) {
|
||||||
|
return <HoverCardPrimitive.Root data-slot="hover-card" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
const HoverCardTrigger = HoverCardPrimitive.Trigger
|
function HoverCardTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof HoverCardPrimitive.Trigger>) {
|
||||||
|
return (
|
||||||
|
<HoverCardPrimitive.Trigger data-slot="hover-card-trigger" {...props} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const HoverCardContent = React.forwardRef<
|
function HoverCardContent({
|
||||||
React.ElementRef<typeof HoverCardPrimitive.Content>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content>
|
align = "center",
|
||||||
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
|
sideOffset = 4,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof HoverCardPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<HoverCardPrimitive.Portal data-slot="hover-card-portal">
|
||||||
<HoverCardPrimitive.Content
|
<HoverCardPrimitive.Content
|
||||||
ref={ref}
|
data-slot="hover-card-content"
|
||||||
align={align}
|
align={align}
|
||||||
sideOffset={sideOffset}
|
sideOffset={sideOffset}
|
||||||
className={cn(
|
className={cn(
|
||||||
"z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
"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-64 origin-(--radix-hover-card-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
</HoverCardPrimitive.Portal>
|
||||||
HoverCardContent.displayName = HoverCardPrimitive.Content.displayName
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export { HoverCard, HoverCardTrigger, HoverCardContent }
|
export { HoverCard, HoverCardTrigger, HoverCardContent }
|
||||||
|
|||||||
@@ -1,46 +1,57 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import { OTPInput, OTPInputContext } from "input-otp"
|
import { OTPInput, OTPInputContext } from "input-otp"
|
||||||
import { Minus } from "lucide-react"
|
import { MinusIcon } from "lucide-react"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const InputOTP = React.forwardRef<
|
function InputOTP({
|
||||||
React.ElementRef<typeof OTPInput>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof OTPInput>
|
containerClassName,
|
||||||
>(({ className, containerClassName, ...props }, ref) => (
|
...props
|
||||||
|
}: React.ComponentProps<typeof OTPInput> & {
|
||||||
|
containerClassName?: string
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
<OTPInput
|
<OTPInput
|
||||||
ref={ref}
|
data-slot="input-otp"
|
||||||
containerClassName={cn(
|
containerClassName={cn(
|
||||||
"flex items-center gap-2 has-[:disabled]:opacity-50",
|
"flex items-center gap-2 has-disabled:opacity-50",
|
||||||
containerClassName
|
containerClassName
|
||||||
)}
|
)}
|
||||||
className={cn("disabled:cursor-not-allowed", className)}
|
className={cn("disabled:cursor-not-allowed", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
)
|
||||||
InputOTP.displayName = "InputOTP"
|
}
|
||||||
|
|
||||||
const InputOTPGroup = React.forwardRef<
|
function InputOTPGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
React.ElementRef<"div">,
|
return (
|
||||||
React.ComponentPropsWithoutRef<"div">
|
<div
|
||||||
>(({ className, ...props }, ref) => (
|
data-slot="input-otp-group"
|
||||||
<div ref={ref} className={cn("flex items-center", className)} {...props} />
|
className={cn("flex items-center", className)}
|
||||||
))
|
{...props}
|
||||||
InputOTPGroup.displayName = "InputOTPGroup"
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const InputOTPSlot = React.forwardRef<
|
function InputOTPSlot({
|
||||||
React.ElementRef<"div">,
|
index,
|
||||||
React.ComponentPropsWithoutRef<"div"> & { index: number }
|
className,
|
||||||
>(({ index, className, ...props }, ref) => {
|
...props
|
||||||
|
}: React.ComponentProps<"div"> & {
|
||||||
|
index: number
|
||||||
|
}) {
|
||||||
const inputOTPContext = React.useContext(OTPInputContext)
|
const inputOTPContext = React.useContext(OTPInputContext)
|
||||||
const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index]
|
const { char, hasFakeCaret, isActive } = inputOTPContext?.slots[index] ?? {}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
data-slot="input-otp-slot"
|
||||||
|
data-active={isActive}
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative flex h-9 w-9 items-center justify-center border-y border-r border-input text-sm shadow-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md",
|
"data-[active=true]:border-ring data-[active=true]:ring-ring/50 data-[active=true]:aria-invalid:ring-destructive/20 dark:data-[active=true]:aria-invalid:ring-destructive/40 aria-invalid:border-destructive data-[active=true]:aria-invalid:border-destructive dark:bg-input/30 border-input relative flex h-9 w-9 items-center justify-center border-y border-r text-sm shadow-xs transition-all outline-none first:rounded-l-md first:border-l last:rounded-r-md data-[active=true]:z-10 data-[active=true]:ring-[3px]",
|
||||||
isActive && "z-10 ring-1 ring-ring",
|
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -48,22 +59,19 @@ const InputOTPSlot = React.forwardRef<
|
|||||||
{char}
|
{char}
|
||||||
{hasFakeCaret && (
|
{hasFakeCaret && (
|
||||||
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
|
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
|
||||||
<div className="h-4 w-px animate-caret-blink bg-foreground duration-1000" />
|
<div className="animate-caret-blink bg-foreground h-4 w-px duration-1000" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
InputOTPSlot.displayName = "InputOTPSlot"
|
|
||||||
|
|
||||||
const InputOTPSeparator = React.forwardRef<
|
function InputOTPSeparator({ ...props }: React.ComponentProps<"div">) {
|
||||||
React.ElementRef<"div">,
|
return (
|
||||||
React.ComponentPropsWithoutRef<"div">
|
<div data-slot="input-otp-separator" role="separator" {...props}>
|
||||||
>(({ ...props }, ref) => (
|
<MinusIcon />
|
||||||
<div ref={ref} role="separator" {...props}>
|
|
||||||
<Minus />
|
|
||||||
</div>
|
</div>
|
||||||
))
|
)
|
||||||
InputOTPSeparator.displayName = "InputOTPSeparator"
|
}
|
||||||
|
|
||||||
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator }
|
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator }
|
||||||
|
|||||||
@@ -2,21 +2,20 @@ import * as React from "react"
|
|||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
|
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||||
({ className, type, ...props }, ref) => {
|
|
||||||
return (
|
return (
|
||||||
<input
|
<input
|
||||||
type={type}
|
type={type}
|
||||||
|
data-slot="input"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
"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
|
className
|
||||||
)}
|
)}
|
||||||
ref={ref}
|
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
|
||||||
Input.displayName = "Input"
|
|
||||||
|
|
||||||
export { Input }
|
export { Input }
|
||||||
|
|||||||
@@ -1,24 +1,24 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const labelVariants = cva(
|
function Label({
|
||||||
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
className,
|
||||||
)
|
...props
|
||||||
|
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
||||||
const Label = React.forwardRef<
|
return (
|
||||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
|
|
||||||
VariantProps<typeof labelVariants>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<LabelPrimitive.Root
|
<LabelPrimitive.Root
|
||||||
ref={ref}
|
data-slot="label"
|
||||||
className={cn(labelVariants(), className)}
|
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",
|
||||||
|
className
|
||||||
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
)
|
||||||
Label.displayName = LabelPrimitive.Root.displayName
|
}
|
||||||
|
|
||||||
export { Label }
|
export { Label }
|
||||||
|
|||||||
@@ -1,33 +1,211 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as MenubarPrimitive from "@radix-ui/react-menubar"
|
import * as MenubarPrimitive from "@radix-ui/react-menubar"
|
||||||
import { Check, ChevronRight, Circle } from "lucide-react"
|
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
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 border p-1 shadow-xs",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function MenubarMenu({
|
function MenubarMenu({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof MenubarPrimitive.Menu>) {
|
}: React.ComponentProps<typeof MenubarPrimitive.Menu>) {
|
||||||
return <MenubarPrimitive.Menu {...props} />
|
return <MenubarPrimitive.Menu data-slot="menubar-menu" {...props} />
|
||||||
}
|
}
|
||||||
|
|
||||||
function MenubarGroup({
|
function MenubarGroup({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof MenubarPrimitive.Group>) {
|
}: React.ComponentProps<typeof MenubarPrimitive.Group>) {
|
||||||
return <MenubarPrimitive.Group {...props} />
|
return <MenubarPrimitive.Group data-slot="menubar-group" {...props} />
|
||||||
}
|
}
|
||||||
|
|
||||||
function MenubarPortal({
|
function MenubarPortal({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof MenubarPrimitive.Portal>) {
|
}: React.ComponentProps<typeof MenubarPrimitive.Portal>) {
|
||||||
return <MenubarPrimitive.Portal {...props} />
|
return <MenubarPrimitive.Portal data-slot="menubar-portal" {...props} />
|
||||||
}
|
}
|
||||||
|
|
||||||
function MenubarRadioGroup({
|
function MenubarRadioGroup({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof MenubarPrimitive.RadioGroup>) {
|
}: React.ComponentProps<typeof MenubarPrimitive.RadioGroup>) {
|
||||||
return <MenubarPrimitive.RadioGroup {...props} />
|
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({
|
function MenubarSub({
|
||||||
@@ -36,221 +214,61 @@ function MenubarSub({
|
|||||||
return <MenubarPrimitive.Sub data-slot="menubar-sub" {...props} />
|
return <MenubarPrimitive.Sub data-slot="menubar-sub" {...props} />
|
||||||
}
|
}
|
||||||
|
|
||||||
const Menubar = React.forwardRef<
|
function MenubarSubTrigger({
|
||||||
React.ElementRef<typeof MenubarPrimitive.Root>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Root>
|
inset,
|
||||||
>(({ className, ...props }, ref) => (
|
children,
|
||||||
<MenubarPrimitive.Root
|
...props
|
||||||
ref={ref}
|
}: React.ComponentProps<typeof MenubarPrimitive.SubTrigger> & {
|
||||||
className={cn(
|
|
||||||
"flex h-9 items-center space-x-1 rounded-md border bg-background p-1 shadow-sm",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
Menubar.displayName = MenubarPrimitive.Root.displayName
|
|
||||||
|
|
||||||
const MenubarTrigger = React.forwardRef<
|
|
||||||
React.ElementRef<typeof MenubarPrimitive.Trigger>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Trigger>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<MenubarPrimitive.Trigger
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"flex cursor-default select-none items-center rounded-sm px-3 py-1 text-sm font-medium outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
MenubarTrigger.displayName = MenubarPrimitive.Trigger.displayName
|
|
||||||
|
|
||||||
const MenubarSubTrigger = React.forwardRef<
|
|
||||||
React.ElementRef<typeof MenubarPrimitive.SubTrigger>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubTrigger> & {
|
|
||||||
inset?: boolean
|
inset?: boolean
|
||||||
}
|
}) {
|
||||||
>(({ className, inset, children, ...props }, ref) => (
|
return (
|
||||||
<MenubarPrimitive.SubTrigger
|
<MenubarPrimitive.SubTrigger
|
||||||
ref={ref}
|
data-slot="menubar-sub-trigger"
|
||||||
|
data-inset={inset}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
|
"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",
|
||||||
inset && "pl-8",
|
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
<ChevronRight className="ml-auto h-4 w-4" />
|
<ChevronRightIcon className="ml-auto h-4 w-4" />
|
||||||
</MenubarPrimitive.SubTrigger>
|
</MenubarPrimitive.SubTrigger>
|
||||||
))
|
|
||||||
MenubarSubTrigger.displayName = MenubarPrimitive.SubTrigger.displayName
|
|
||||||
|
|
||||||
const MenubarSubContent = React.forwardRef<
|
|
||||||
React.ElementRef<typeof MenubarPrimitive.SubContent>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubContent>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<MenubarPrimitive.SubContent
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
MenubarSubContent.displayName = MenubarPrimitive.SubContent.displayName
|
|
||||||
|
|
||||||
const MenubarContent = React.forwardRef<
|
|
||||||
React.ElementRef<typeof MenubarPrimitive.Content>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Content>
|
|
||||||
>(
|
|
||||||
(
|
|
||||||
{ className, align = "start", alignOffset = -4, sideOffset = 8, ...props },
|
|
||||||
ref
|
|
||||||
) => (
|
|
||||||
<MenubarPrimitive.Portal>
|
|
||||||
<MenubarPrimitive.Content
|
|
||||||
ref={ref}
|
|
||||||
align={align}
|
|
||||||
alignOffset={alignOffset}
|
|
||||||
sideOffset={sideOffset}
|
|
||||||
className={cn(
|
|
||||||
"z-50 min-w-[12rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md 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",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
</MenubarPrimitive.Portal>
|
|
||||||
)
|
)
|
||||||
)
|
|
||||||
MenubarContent.displayName = MenubarPrimitive.Content.displayName
|
|
||||||
|
|
||||||
const MenubarItem = React.forwardRef<
|
|
||||||
React.ElementRef<typeof MenubarPrimitive.Item>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Item> & {
|
|
||||||
inset?: boolean
|
|
||||||
}
|
}
|
||||||
>(({ className, inset, ...props }, ref) => (
|
|
||||||
<MenubarPrimitive.Item
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
||||||
inset && "pl-8",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
MenubarItem.displayName = MenubarPrimitive.Item.displayName
|
|
||||||
|
|
||||||
const MenubarCheckboxItem = React.forwardRef<
|
function MenubarSubContent({
|
||||||
React.ElementRef<typeof MenubarPrimitive.CheckboxItem>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.CheckboxItem>
|
|
||||||
>(({ className, children, checked, ...props }, ref) => (
|
|
||||||
<MenubarPrimitive.CheckboxItem
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
checked={checked}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
||||||
<MenubarPrimitive.ItemIndicator>
|
|
||||||
<Check className="h-4 w-4" />
|
|
||||||
</MenubarPrimitive.ItemIndicator>
|
|
||||||
</span>
|
|
||||||
{children}
|
|
||||||
</MenubarPrimitive.CheckboxItem>
|
|
||||||
))
|
|
||||||
MenubarCheckboxItem.displayName = MenubarPrimitive.CheckboxItem.displayName
|
|
||||||
|
|
||||||
const MenubarRadioItem = React.forwardRef<
|
|
||||||
React.ElementRef<typeof MenubarPrimitive.RadioItem>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.RadioItem>
|
|
||||||
>(({ className, children, ...props }, ref) => (
|
|
||||||
<MenubarPrimitive.RadioItem
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
||||||
<MenubarPrimitive.ItemIndicator>
|
|
||||||
<Circle className="h-4 w-4 fill-current" />
|
|
||||||
</MenubarPrimitive.ItemIndicator>
|
|
||||||
</span>
|
|
||||||
{children}
|
|
||||||
</MenubarPrimitive.RadioItem>
|
|
||||||
))
|
|
||||||
MenubarRadioItem.displayName = MenubarPrimitive.RadioItem.displayName
|
|
||||||
|
|
||||||
const MenubarLabel = React.forwardRef<
|
|
||||||
React.ElementRef<typeof MenubarPrimitive.Label>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Label> & {
|
|
||||||
inset?: boolean
|
|
||||||
}
|
|
||||||
>(({ className, inset, ...props }, ref) => (
|
|
||||||
<MenubarPrimitive.Label
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"px-2 py-1.5 text-sm font-semibold",
|
|
||||||
inset && "pl-8",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
MenubarLabel.displayName = MenubarPrimitive.Label.displayName
|
|
||||||
|
|
||||||
const MenubarSeparator = React.forwardRef<
|
|
||||||
React.ElementRef<typeof MenubarPrimitive.Separator>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Separator>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<MenubarPrimitive.Separator
|
|
||||||
ref={ref}
|
|
||||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
MenubarSeparator.displayName = MenubarPrimitive.Separator.displayName
|
|
||||||
|
|
||||||
const MenubarShortcut = ({
|
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
}: React.ComponentProps<typeof MenubarPrimitive.SubContent>) {
|
||||||
return (
|
return (
|
||||||
<span
|
<MenubarPrimitive.SubContent
|
||||||
|
data-slot="menubar-sub-content"
|
||||||
className={cn(
|
className={cn(
|
||||||
"ml-auto text-xs tracking-widest text-muted-foreground",
|
"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
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
MenubarShortcut.displayname = "MenubarShortcut"
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Menubar,
|
Menubar,
|
||||||
|
MenubarPortal,
|
||||||
MenubarMenu,
|
MenubarMenu,
|
||||||
MenubarTrigger,
|
MenubarTrigger,
|
||||||
MenubarContent,
|
MenubarContent,
|
||||||
MenubarItem,
|
MenubarGroup,
|
||||||
MenubarSeparator,
|
MenubarSeparator,
|
||||||
MenubarLabel,
|
MenubarLabel,
|
||||||
|
MenubarItem,
|
||||||
|
MenubarShortcut,
|
||||||
MenubarCheckboxItem,
|
MenubarCheckboxItem,
|
||||||
MenubarRadioGroup,
|
MenubarRadioGroup,
|
||||||
MenubarRadioItem,
|
MenubarRadioItem,
|
||||||
MenubarPortal,
|
|
||||||
MenubarSubContent,
|
|
||||||
MenubarSubTrigger,
|
|
||||||
MenubarGroup,
|
|
||||||
MenubarSub,
|
MenubarSub,
|
||||||
MenubarShortcut,
|
MenubarSubTrigger,
|
||||||
|
MenubarSubContent,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,122 +1,161 @@
|
|||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu"
|
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu"
|
||||||
import { cva } from "class-variance-authority"
|
import { cva } from "class-variance-authority"
|
||||||
import { ChevronDown } from "lucide-react"
|
import { ChevronDownIcon } from "lucide-react"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const NavigationMenu = React.forwardRef<
|
function NavigationMenu({
|
||||||
React.ElementRef<typeof NavigationMenuPrimitive.Root>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root>
|
children,
|
||||||
>(({ className, children, ...props }, ref) => (
|
viewport = true,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof NavigationMenuPrimitive.Root> & {
|
||||||
|
viewport?: boolean
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
<NavigationMenuPrimitive.Root
|
<NavigationMenuPrimitive.Root
|
||||||
ref={ref}
|
data-slot="navigation-menu"
|
||||||
|
data-viewport={viewport}
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative z-10 flex max-w-max flex-1 items-center justify-center",
|
"group/navigation-menu relative flex max-w-max flex-1 items-center justify-center",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
<NavigationMenuViewport />
|
{viewport && <NavigationMenuViewport />}
|
||||||
</NavigationMenuPrimitive.Root>
|
</NavigationMenuPrimitive.Root>
|
||||||
))
|
)
|
||||||
NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName
|
}
|
||||||
|
|
||||||
const NavigationMenuList = React.forwardRef<
|
function NavigationMenuList({
|
||||||
React.ElementRef<typeof NavigationMenuPrimitive.List>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.List>
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.ComponentProps<typeof NavigationMenuPrimitive.List>) {
|
||||||
|
return (
|
||||||
<NavigationMenuPrimitive.List
|
<NavigationMenuPrimitive.List
|
||||||
ref={ref}
|
data-slot="navigation-menu-list"
|
||||||
className={cn(
|
className={cn(
|
||||||
"group flex flex-1 list-none items-center justify-center space-x-1",
|
"group flex flex-1 list-none items-center justify-center gap-1",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
)
|
||||||
NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName
|
}
|
||||||
|
|
||||||
const NavigationMenuItem = NavigationMenuPrimitive.Item
|
function NavigationMenuItem({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof NavigationMenuPrimitive.Item>) {
|
||||||
|
return (
|
||||||
|
<NavigationMenuPrimitive.Item
|
||||||
|
data-slot="navigation-menu-item"
|
||||||
|
className={cn("relative", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const navigationMenuTriggerStyle = cva(
|
const navigationMenuTriggerStyle = cva(
|
||||||
"group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50"
|
"group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=open]:hover:bg-accent data-[state=open]:text-accent-foreground data-[state=open]:focus:bg-accent data-[state=open]:bg-accent/50 focus-visible:ring-ring/50 outline-none transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1"
|
||||||
)
|
)
|
||||||
|
|
||||||
const NavigationMenuTrigger = React.forwardRef<
|
function NavigationMenuTrigger({
|
||||||
React.ElementRef<typeof NavigationMenuPrimitive.Trigger>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger>
|
children,
|
||||||
>(({ className, children, ...props }, ref) => (
|
...props
|
||||||
|
}: React.ComponentProps<typeof NavigationMenuPrimitive.Trigger>) {
|
||||||
|
return (
|
||||||
<NavigationMenuPrimitive.Trigger
|
<NavigationMenuPrimitive.Trigger
|
||||||
ref={ref}
|
data-slot="navigation-menu-trigger"
|
||||||
className={cn(navigationMenuTriggerStyle(), "group", className)}
|
className={cn(navigationMenuTriggerStyle(), "group", className)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}{" "}
|
{children}{" "}
|
||||||
<ChevronDown
|
<ChevronDownIcon
|
||||||
className="relative top-[1px] ml-1 h-3 w-3 transition duration-300 group-data-[state=open]:rotate-180"
|
className="relative top-[1px] ml-1 size-3 transition duration-300 group-data-[state=open]:rotate-180"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
</NavigationMenuPrimitive.Trigger>
|
</NavigationMenuPrimitive.Trigger>
|
||||||
))
|
)
|
||||||
NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName
|
}
|
||||||
|
|
||||||
const NavigationMenuContent = React.forwardRef<
|
function NavigationMenuContent({
|
||||||
React.ElementRef<typeof NavigationMenuPrimitive.Content>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Content>
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.ComponentProps<typeof NavigationMenuPrimitive.Content>) {
|
||||||
|
return (
|
||||||
<NavigationMenuPrimitive.Content
|
<NavigationMenuPrimitive.Content
|
||||||
ref={ref}
|
data-slot="navigation-menu-content"
|
||||||
className={cn(
|
className={cn(
|
||||||
"left-0 top-0 w-full data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 md:absolute md:w-auto ",
|
"data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 top-0 left-0 w-full p-2 pr-2.5 md:absolute md:w-auto",
|
||||||
|
"group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:data-[state=open]:animate-in group-data-[viewport=false]/navigation-menu:data-[state=closed]:animate-out group-data-[viewport=false]/navigation-menu:data-[state=closed]:zoom-out-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:fade-in-0 group-data-[viewport=false]/navigation-menu:data-[state=closed]:fade-out-0 group-data-[viewport=false]/navigation-menu:top-full group-data-[viewport=false]/navigation-menu:mt-1.5 group-data-[viewport=false]/navigation-menu:overflow-hidden group-data-[viewport=false]/navigation-menu:rounded-md group-data-[viewport=false]/navigation-menu:border group-data-[viewport=false]/navigation-menu:shadow group-data-[viewport=false]/navigation-menu:duration-200 **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
)
|
||||||
NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName
|
}
|
||||||
|
|
||||||
const NavigationMenuLink = NavigationMenuPrimitive.Link
|
function NavigationMenuViewport({
|
||||||
|
className,
|
||||||
const NavigationMenuViewport = React.forwardRef<
|
...props
|
||||||
React.ElementRef<typeof NavigationMenuPrimitive.Viewport>,
|
}: React.ComponentProps<typeof NavigationMenuPrimitive.Viewport>) {
|
||||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport>
|
return (
|
||||||
>(({ className, ...props }, ref) => (
|
<div
|
||||||
<div className={cn("absolute left-0 top-full flex justify-center")}>
|
|
||||||
<NavigationMenuPrimitive.Viewport
|
|
||||||
className={cn(
|
className={cn(
|
||||||
"origin-top-center relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border bg-popover text-popover-foreground shadow data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 md:w-[var(--radix-navigation-menu-viewport-width)]",
|
"absolute top-full left-0 isolate z-50 flex justify-center"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<NavigationMenuPrimitive.Viewport
|
||||||
|
data-slot="navigation-menu-viewport"
|
||||||
|
className={cn(
|
||||||
|
"origin-top-center bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border shadow md:w-[var(--radix-navigation-menu-viewport-width)]",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
ref={ref}
|
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))
|
)
|
||||||
NavigationMenuViewport.displayName =
|
}
|
||||||
NavigationMenuPrimitive.Viewport.displayName
|
|
||||||
|
|
||||||
const NavigationMenuIndicator = React.forwardRef<
|
function NavigationMenuLink({
|
||||||
React.ElementRef<typeof NavigationMenuPrimitive.Indicator>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Indicator>
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.ComponentProps<typeof NavigationMenuPrimitive.Link>) {
|
||||||
<NavigationMenuPrimitive.Indicator
|
return (
|
||||||
ref={ref}
|
<NavigationMenuPrimitive.Link
|
||||||
|
data-slot="navigation-menu-link"
|
||||||
className={cn(
|
className={cn(
|
||||||
"top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in",
|
"data-[active=true]:focus:bg-accent data-[active=true]:hover:bg-accent data-[active=true]:bg-accent/50 data-[active=true]:text-accent-foreground hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus-visible:ring-ring/50 [&_svg:not([class*='text-'])]:text-muted-foreground flex flex-col gap-1 rounded-sm p-2 text-sm transition-all outline-none focus-visible:ring-[3px] focus-visible:outline-1 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function NavigationMenuIndicator({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof NavigationMenuPrimitive.Indicator>) {
|
||||||
|
return (
|
||||||
|
<NavigationMenuPrimitive.Indicator
|
||||||
|
data-slot="navigation-menu-indicator"
|
||||||
|
className={cn(
|
||||||
|
"data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<div className="relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-border shadow-md" />
|
<div className="bg-border relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm shadow-md" />
|
||||||
</NavigationMenuPrimitive.Indicator>
|
</NavigationMenuPrimitive.Indicator>
|
||||||
))
|
)
|
||||||
NavigationMenuIndicator.displayName =
|
}
|
||||||
NavigationMenuPrimitive.Indicator.displayName
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
navigationMenuTriggerStyle,
|
|
||||||
NavigationMenu,
|
NavigationMenu,
|
||||||
NavigationMenuList,
|
NavigationMenuList,
|
||||||
NavigationMenuItem,
|
NavigationMenuItem,
|
||||||
@@ -125,4 +164,5 @@ export {
|
|||||||
NavigationMenuLink,
|
NavigationMenuLink,
|
||||||
NavigationMenuIndicator,
|
NavigationMenuIndicator,
|
||||||
NavigationMenuViewport,
|
NavigationMenuViewport,
|
||||||
|
navigationMenuTriggerStyle,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,52 +1,58 @@
|
|||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react"
|
import {
|
||||||
|
ChevronLeftIcon,
|
||||||
|
ChevronRightIcon,
|
||||||
|
MoreHorizontalIcon,
|
||||||
|
} from "lucide-react"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import { ButtonProps, buttonVariants } from "@/components/ui/button"
|
import { Button, buttonVariants } from "@/components/ui/button"
|
||||||
|
|
||||||
const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => (
|
function Pagination({ className, ...props }: React.ComponentProps<"nav">) {
|
||||||
|
return (
|
||||||
<nav
|
<nav
|
||||||
role="navigation"
|
role="navigation"
|
||||||
aria-label="pagination"
|
aria-label="pagination"
|
||||||
|
data-slot="pagination"
|
||||||
className={cn("mx-auto flex w-full justify-center", className)}
|
className={cn("mx-auto flex w-full justify-center", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
Pagination.displayName = "Pagination"
|
}
|
||||||
|
|
||||||
const PaginationContent = React.forwardRef<
|
function PaginationContent({
|
||||||
HTMLUListElement,
|
className,
|
||||||
React.ComponentProps<"ul">
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.ComponentProps<"ul">) {
|
||||||
|
return (
|
||||||
<ul
|
<ul
|
||||||
ref={ref}
|
data-slot="pagination-content"
|
||||||
className={cn("flex flex-row items-center gap-1", className)}
|
className={cn("flex flex-row items-center gap-1", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
)
|
||||||
PaginationContent.displayName = "PaginationContent"
|
}
|
||||||
|
|
||||||
const PaginationItem = React.forwardRef<
|
function PaginationItem({ ...props }: React.ComponentProps<"li">) {
|
||||||
HTMLLIElement,
|
return <li data-slot="pagination-item" {...props} />
|
||||||
React.ComponentProps<"li">
|
}
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<li ref={ref} className={cn("", className)} {...props} />
|
|
||||||
))
|
|
||||||
PaginationItem.displayName = "PaginationItem"
|
|
||||||
|
|
||||||
type PaginationLinkProps = {
|
type PaginationLinkProps = {
|
||||||
isActive?: boolean
|
isActive?: boolean
|
||||||
} & Pick<ButtonProps, "size"> &
|
} & Pick<React.ComponentProps<typeof Button>, "size"> &
|
||||||
React.ComponentProps<"a">
|
React.ComponentProps<"a">
|
||||||
|
|
||||||
const PaginationLink = ({
|
function PaginationLink({
|
||||||
className,
|
className,
|
||||||
isActive,
|
isActive,
|
||||||
size = "icon",
|
size = "icon",
|
||||||
...props
|
...props
|
||||||
}: PaginationLinkProps) => (
|
}: PaginationLinkProps) {
|
||||||
|
return (
|
||||||
<a
|
<a
|
||||||
aria-current={isActive ? "page" : undefined}
|
aria-current={isActive ? "page" : undefined}
|
||||||
|
data-slot="pagination-link"
|
||||||
|
data-active={isActive}
|
||||||
className={cn(
|
className={cn(
|
||||||
buttonVariants({
|
buttonVariants({
|
||||||
variant: isActive ? "outline" : "ghost",
|
variant: isActive ? "outline" : "ghost",
|
||||||
@@ -57,54 +63,58 @@ const PaginationLink = ({
|
|||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
PaginationLink.displayName = "PaginationLink"
|
}
|
||||||
|
|
||||||
const PaginationPrevious = ({
|
function PaginationPrevious({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof PaginationLink>) => (
|
}: React.ComponentProps<typeof PaginationLink>) {
|
||||||
|
return (
|
||||||
<PaginationLink
|
<PaginationLink
|
||||||
aria-label="Go to previous page"
|
aria-label="Go to previous page"
|
||||||
size="default"
|
size="default"
|
||||||
className={cn("gap-1 pl-2.5", className)}
|
className={cn("gap-1 px-2.5 sm:pl-2.5", className)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<ChevronLeft className="h-4 w-4" />
|
<ChevronLeftIcon />
|
||||||
<span>Previous</span>
|
<span className="hidden sm:block">Previous</span>
|
||||||
</PaginationLink>
|
</PaginationLink>
|
||||||
)
|
)
|
||||||
PaginationPrevious.displayName = "PaginationPrevious"
|
}
|
||||||
|
|
||||||
const PaginationNext = ({
|
function PaginationNext({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof PaginationLink>) => (
|
}: React.ComponentProps<typeof PaginationLink>) {
|
||||||
|
return (
|
||||||
<PaginationLink
|
<PaginationLink
|
||||||
aria-label="Go to next page"
|
aria-label="Go to next page"
|
||||||
size="default"
|
size="default"
|
||||||
className={cn("gap-1 pr-2.5", className)}
|
className={cn("gap-1 px-2.5 sm:pr-2.5", className)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<span>Next</span>
|
<span className="hidden sm:block">Next</span>
|
||||||
<ChevronRight className="h-4 w-4" />
|
<ChevronRightIcon />
|
||||||
</PaginationLink>
|
</PaginationLink>
|
||||||
)
|
)
|
||||||
PaginationNext.displayName = "PaginationNext"
|
}
|
||||||
|
|
||||||
const PaginationEllipsis = ({
|
function PaginationEllipsis({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<"span">) => (
|
}: React.ComponentProps<"span">) {
|
||||||
|
return (
|
||||||
<span
|
<span
|
||||||
aria-hidden
|
aria-hidden
|
||||||
className={cn("flex h-9 w-9 items-center justify-center", className)}
|
data-slot="pagination-ellipsis"
|
||||||
|
className={cn("flex size-9 items-center justify-center", className)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<MoreHorizontal className="h-4 w-4" />
|
<MoreHorizontalIcon className="size-4" />
|
||||||
<span className="sr-only">More pages</span>
|
<span className="sr-only">More pages</span>
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
PaginationEllipsis.displayName = "PaginationEllipsis"
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Pagination,
|
Pagination,
|
||||||
|
|||||||
@@ -1,31 +1,48 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as PopoverPrimitive from "@radix-ui/react-popover"
|
import * as PopoverPrimitive from "@radix-ui/react-popover"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const Popover = PopoverPrimitive.Root
|
function Popover({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
|
||||||
|
return <PopoverPrimitive.Root data-slot="popover" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
const PopoverTrigger = PopoverPrimitive.Trigger
|
function PopoverTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
|
||||||
|
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
const PopoverAnchor = PopoverPrimitive.Anchor
|
function PopoverContent({
|
||||||
|
className,
|
||||||
const PopoverContent = React.forwardRef<
|
align = "center",
|
||||||
React.ElementRef<typeof PopoverPrimitive.Content>,
|
sideOffset = 4,
|
||||||
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
|
...props
|
||||||
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
|
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
|
||||||
|
return (
|
||||||
<PopoverPrimitive.Portal>
|
<PopoverPrimitive.Portal>
|
||||||
<PopoverPrimitive.Content
|
<PopoverPrimitive.Content
|
||||||
ref={ref}
|
data-slot="popover-content"
|
||||||
align={align}
|
align={align}
|
||||||
sideOffset={sideOffset}
|
sideOffset={sideOffset}
|
||||||
className={cn(
|
className={cn(
|
||||||
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
"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
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</PopoverPrimitive.Portal>
|
</PopoverPrimitive.Portal>
|
||||||
))
|
)
|
||||||
PopoverContent.displayName = PopoverPrimitive.Content.displayName
|
}
|
||||||
|
|
||||||
|
function PopoverAnchor({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
|
||||||
|
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
|
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
|
||||||
|
|||||||
@@ -1,28 +1,29 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as ProgressPrimitive from "@radix-ui/react-progress"
|
import * as ProgressPrimitive from "@radix-ui/react-progress"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const Progress = React.forwardRef<
|
function Progress({
|
||||||
React.ElementRef<typeof ProgressPrimitive.Root>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
|
value,
|
||||||
>(({ className, value, ...props }, ref) => (
|
...props
|
||||||
|
}: React.ComponentProps<typeof ProgressPrimitive.Root>) {
|
||||||
|
return (
|
||||||
<ProgressPrimitive.Root
|
<ProgressPrimitive.Root
|
||||||
ref={ref}
|
data-slot="progress"
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative h-2 w-full overflow-hidden rounded-full bg-primary/20",
|
"bg-primary/20 relative h-2 w-full overflow-hidden rounded-full",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<ProgressPrimitive.Indicator
|
<ProgressPrimitive.Indicator
|
||||||
className="h-full w-full flex-1 bg-primary transition-all"
|
data-slot="progress-indicator"
|
||||||
|
className="bg-primary h-full w-full flex-1 transition-all"
|
||||||
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
||||||
/>
|
/>
|
||||||
</ProgressPrimitive.Root>
|
</ProgressPrimitive.Root>
|
||||||
))
|
)
|
||||||
Progress.displayName = ProgressPrimitive.Root.displayName
|
}
|
||||||
|
|
||||||
export { Progress }
|
export { Progress }
|
||||||
|
|||||||
@@ -1,42 +1,45 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
|
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
|
||||||
import { Circle } from "lucide-react"
|
import { CircleIcon } from "lucide-react"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const RadioGroup = React.forwardRef<
|
function RadioGroup({
|
||||||
React.ElementRef<typeof RadioGroupPrimitive.Root>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
|
...props
|
||||||
>(({ className, ...props }, ref) => {
|
}: React.ComponentProps<typeof RadioGroupPrimitive.Root>) {
|
||||||
return (
|
return (
|
||||||
<RadioGroupPrimitive.Root
|
<RadioGroupPrimitive.Root
|
||||||
className={cn("grid gap-2", className)}
|
data-slot="radio-group"
|
||||||
|
className={cn("grid gap-3", className)}
|
||||||
{...props}
|
{...props}
|
||||||
ref={ref}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
|
|
||||||
|
|
||||||
const RadioGroupItem = React.forwardRef<
|
function RadioGroupItem({
|
||||||
React.ElementRef<typeof RadioGroupPrimitive.Item>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
|
...props
|
||||||
>(({ className, ...props }, ref) => {
|
}: React.ComponentProps<typeof RadioGroupPrimitive.Item>) {
|
||||||
return (
|
return (
|
||||||
<RadioGroupPrimitive.Item
|
<RadioGroupPrimitive.Item
|
||||||
ref={ref}
|
data-slot="radio-group-item"
|
||||||
className={cn(
|
className={cn(
|
||||||
"aspect-square h-4 w-4 rounded-full border border-primary text-primary shadow focus:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
|
"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
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
|
<RadioGroupPrimitive.Indicator
|
||||||
<Circle className="h-3.5 w-3.5 fill-primary" />
|
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.Indicator>
|
||||||
</RadioGroupPrimitive.Item>
|
</RadioGroupPrimitive.Item>
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName
|
|
||||||
|
|
||||||
export { RadioGroup, RadioGroupItem }
|
export { RadioGroup, RadioGroupItem }
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
"use client"
|
import * as React from "react"
|
||||||
|
import { GripVerticalIcon } from "lucide-react"
|
||||||
import { GripVertical } from "lucide-react"
|
|
||||||
import * as ResizablePrimitive from "react-resizable-panels"
|
import * as ResizablePrimitive from "react-resizable-panels"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const ResizablePanelGroup = ({
|
function ResizablePanelGroup({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) => (
|
}: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) {
|
||||||
|
return (
|
||||||
<ResizablePrimitive.PanelGroup
|
<ResizablePrimitive.PanelGroup
|
||||||
|
data-slot="resizable-panel-group"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex h-full w-full data-[panel-group-direction=vertical]:flex-col",
|
"flex h-full w-full data-[panel-group-direction=vertical]:flex-col",
|
||||||
className
|
className
|
||||||
@@ -17,29 +18,37 @@ const ResizablePanelGroup = ({
|
|||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const ResizablePanel = ResizablePrimitive.Panel
|
function ResizablePanel({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ResizablePrimitive.Panel>) {
|
||||||
|
return <ResizablePrimitive.Panel data-slot="resizable-panel" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
const ResizableHandle = ({
|
function ResizableHandle({
|
||||||
withHandle,
|
withHandle,
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {
|
}: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {
|
||||||
withHandle?: boolean
|
withHandle?: boolean
|
||||||
}) => (
|
}) {
|
||||||
|
return (
|
||||||
<ResizablePrimitive.PanelResizeHandle
|
<ResizablePrimitive.PanelResizeHandle
|
||||||
|
data-slot="resizable-handle"
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative flex w-px items-center justify-center bg-border after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 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-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 [&[data-panel-group-direction=vertical]>div]:rotate-90",
|
"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
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{withHandle && (
|
{withHandle && (
|
||||||
<div className="z-10 flex h-4 w-3 items-center justify-center rounded-sm border bg-border">
|
<div className="bg-border z-10 flex h-4 w-3 items-center justify-center rounded-xs border">
|
||||||
<GripVertical className="h-2.5 w-2.5" />
|
<GripVerticalIcon className="size-2.5" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</ResizablePrimitive.PanelResizeHandle>
|
</ResizablePrimitive.PanelResizeHandle>
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export { ResizablePanelGroup, ResizablePanel, ResizableHandle }
|
export { ResizablePanelGroup, ResizablePanel, ResizableHandle }
|
||||||
|
|||||||
@@ -1,46 +1,58 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
|
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const ScrollArea = React.forwardRef<
|
function ScrollArea({
|
||||||
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
|
children,
|
||||||
>(({ className, children, ...props }, ref) => (
|
...props
|
||||||
|
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
|
||||||
|
return (
|
||||||
<ScrollAreaPrimitive.Root
|
<ScrollAreaPrimitive.Root
|
||||||
ref={ref}
|
data-slot="scroll-area"
|
||||||
className={cn("relative overflow-hidden", className)}
|
className={cn("relative", className)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
|
<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}
|
{children}
|
||||||
</ScrollAreaPrimitive.Viewport>
|
</ScrollAreaPrimitive.Viewport>
|
||||||
<ScrollBar />
|
<ScrollBar />
|
||||||
<ScrollAreaPrimitive.Corner />
|
<ScrollAreaPrimitive.Corner />
|
||||||
</ScrollAreaPrimitive.Root>
|
</ScrollAreaPrimitive.Root>
|
||||||
))
|
)
|
||||||
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
|
}
|
||||||
|
|
||||||
const ScrollBar = React.forwardRef<
|
function ScrollBar({
|
||||||
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
|
orientation = "vertical",
|
||||||
>(({ className, orientation = "vertical", ...props }, ref) => (
|
...props
|
||||||
|
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
|
||||||
|
return (
|
||||||
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||||
ref={ref}
|
data-slot="scroll-area-scrollbar"
|
||||||
orientation={orientation}
|
orientation={orientation}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex touch-none select-none transition-colors",
|
"flex touch-none p-px transition-colors select-none",
|
||||||
orientation === "vertical" &&
|
orientation === "vertical" &&
|
||||||
"h-full w-2.5 border-l border-l-transparent p-[1px]",
|
"h-full w-2.5 border-l border-l-transparent",
|
||||||
orientation === "horizontal" &&
|
orientation === "horizontal" &&
|
||||||
"h-2.5 flex-col border-t border-t-transparent p-[1px]",
|
"h-2.5 flex-col border-t border-t-transparent",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
|
<ScrollAreaPrimitive.ScrollAreaThumb
|
||||||
|
data-slot="scroll-area-thumb"
|
||||||
|
className="bg-border relative flex-1 rounded-full"
|
||||||
|
/>
|
||||||
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||||
))
|
)
|
||||||
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
|
}
|
||||||
|
|
||||||
export { ScrollArea, ScrollBar }
|
export { ScrollArea, ScrollBar }
|
||||||
|
|||||||
@@ -1,81 +1,65 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as SelectPrimitive from "@radix-ui/react-select"
|
import * as SelectPrimitive from "@radix-ui/react-select"
|
||||||
import { Check, ChevronDown, ChevronUp } from "lucide-react"
|
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const Select = SelectPrimitive.Root
|
function Select({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
|
||||||
|
return <SelectPrimitive.Root data-slot="select" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
const SelectGroup = SelectPrimitive.Group
|
function SelectGroup({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
|
||||||
|
return <SelectPrimitive.Group data-slot="select-group" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
const SelectValue = SelectPrimitive.Value
|
function SelectValue({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
|
||||||
|
return <SelectPrimitive.Value data-slot="select-value" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
const SelectTrigger = React.forwardRef<
|
function SelectTrigger({
|
||||||
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
size = "default",
|
||||||
>(({ className, children, ...props }, ref) => (
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
|
||||||
|
size?: "sm" | "default"
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
<SelectPrimitive.Trigger
|
<SelectPrimitive.Trigger
|
||||||
ref={ref}
|
data-slot="select-trigger"
|
||||||
|
data-size={size}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
"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
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
<SelectPrimitive.Icon asChild>
|
<SelectPrimitive.Icon asChild>
|
||||||
<ChevronDown className="h-4 w-4 opacity-50" />
|
<ChevronDownIcon className="size-4 opacity-50" />
|
||||||
</SelectPrimitive.Icon>
|
</SelectPrimitive.Icon>
|
||||||
</SelectPrimitive.Trigger>
|
</SelectPrimitive.Trigger>
|
||||||
))
|
)
|
||||||
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
|
}
|
||||||
|
|
||||||
const SelectScrollUpButton = React.forwardRef<
|
function SelectContent({
|
||||||
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
|
children,
|
||||||
>(({ className, ...props }, ref) => (
|
position = "popper",
|
||||||
<SelectPrimitive.ScrollUpButton
|
...props
|
||||||
ref={ref}
|
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
|
||||||
className={cn(
|
return (
|
||||||
"flex cursor-default items-center justify-center py-1",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<ChevronUp className="h-4 w-4" />
|
|
||||||
</SelectPrimitive.ScrollUpButton>
|
|
||||||
))
|
|
||||||
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
|
|
||||||
|
|
||||||
const SelectScrollDownButton = React.forwardRef<
|
|
||||||
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<SelectPrimitive.ScrollDownButton
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"flex cursor-default items-center justify-center py-1",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<ChevronDown className="h-4 w-4" />
|
|
||||||
</SelectPrimitive.ScrollDownButton>
|
|
||||||
))
|
|
||||||
SelectScrollDownButton.displayName =
|
|
||||||
SelectPrimitive.ScrollDownButton.displayName
|
|
||||||
|
|
||||||
const SelectContent = React.forwardRef<
|
|
||||||
React.ElementRef<typeof SelectPrimitive.Content>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
|
||||||
>(({ className, children, position = "popper", ...props }, ref) => (
|
|
||||||
<SelectPrimitive.Portal>
|
<SelectPrimitive.Portal>
|
||||||
<SelectPrimitive.Content
|
<SelectPrimitive.Content
|
||||||
ref={ref}
|
data-slot="select-content"
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
"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" &&
|
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",
|
"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
|
className
|
||||||
@@ -88,7 +72,7 @@ const SelectContent = React.forwardRef<
|
|||||||
className={cn(
|
className={cn(
|
||||||
"p-1",
|
"p-1",
|
||||||
position === "popper" &&
|
position === "popper" &&
|
||||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
|
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
@@ -96,64 +80,104 @@ const SelectContent = React.forwardRef<
|
|||||||
<SelectScrollDownButton />
|
<SelectScrollDownButton />
|
||||||
</SelectPrimitive.Content>
|
</SelectPrimitive.Content>
|
||||||
</SelectPrimitive.Portal>
|
</SelectPrimitive.Portal>
|
||||||
))
|
)
|
||||||
SelectContent.displayName = SelectPrimitive.Content.displayName
|
}
|
||||||
|
|
||||||
const SelectLabel = React.forwardRef<
|
function SelectLabel({
|
||||||
React.ElementRef<typeof SelectPrimitive.Label>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
|
||||||
|
return (
|
||||||
<SelectPrimitive.Label
|
<SelectPrimitive.Label
|
||||||
ref={ref}
|
data-slot="select-label"
|
||||||
className={cn("px-2 py-1.5 text-sm font-semibold", className)}
|
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
)
|
||||||
SelectLabel.displayName = SelectPrimitive.Label.displayName
|
}
|
||||||
|
|
||||||
const SelectItem = React.forwardRef<
|
function SelectItem({
|
||||||
React.ElementRef<typeof SelectPrimitive.Item>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
children,
|
||||||
>(({ className, children, ...props }, ref) => (
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
|
||||||
|
return (
|
||||||
<SelectPrimitive.Item
|
<SelectPrimitive.Item
|
||||||
ref={ref}
|
data-slot="select-item"
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
"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
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
|
<span className="absolute right-2 flex size-3.5 items-center justify-center">
|
||||||
<SelectPrimitive.ItemIndicator>
|
<SelectPrimitive.ItemIndicator>
|
||||||
<Check className="h-4 w-4" />
|
<CheckIcon className="size-4" />
|
||||||
</SelectPrimitive.ItemIndicator>
|
</SelectPrimitive.ItemIndicator>
|
||||||
</span>
|
</span>
|
||||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||||
</SelectPrimitive.Item>
|
</SelectPrimitive.Item>
|
||||||
))
|
)
|
||||||
SelectItem.displayName = SelectPrimitive.Item.displayName
|
}
|
||||||
|
|
||||||
const SelectSeparator = React.forwardRef<
|
function SelectSeparator({
|
||||||
React.ElementRef<typeof SelectPrimitive.Separator>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
|
||||||
|
return (
|
||||||
<SelectPrimitive.Separator
|
<SelectPrimitive.Separator
|
||||||
ref={ref}
|
data-slot="select-separator"
|
||||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
)
|
||||||
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
|
}
|
||||||
|
|
||||||
|
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 {
|
export {
|
||||||
Select,
|
Select,
|
||||||
SelectGroup,
|
|
||||||
SelectValue,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectContent,
|
SelectContent,
|
||||||
SelectLabel,
|
SelectGroup,
|
||||||
SelectItem,
|
SelectItem,
|
||||||
SelectSeparator,
|
SelectLabel,
|
||||||
SelectScrollUpButton,
|
|
||||||
SelectScrollDownButton,
|
SelectScrollDownButton,
|
||||||
|
SelectScrollUpButton,
|
||||||
|
SelectSeparator,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +1,28 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as SeparatorPrimitive from "@radix-ui/react-separator"
|
import * as SeparatorPrimitive from "@radix-ui/react-separator"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const Separator = React.forwardRef<
|
function Separator({
|
||||||
React.ElementRef<typeof SeparatorPrimitive.Root>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
|
orientation = "horizontal",
|
||||||
>(
|
decorative = true,
|
||||||
(
|
...props
|
||||||
{ className, orientation = "horizontal", decorative = true, ...props },
|
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
|
||||||
ref
|
return (
|
||||||
) => (
|
|
||||||
<SeparatorPrimitive.Root
|
<SeparatorPrimitive.Root
|
||||||
ref={ref}
|
data-slot="separator"
|
||||||
decorative={decorative}
|
decorative={decorative}
|
||||||
orientation={orientation}
|
orientation={orientation}
|
||||||
className={cn(
|
className={cn(
|
||||||
"shrink-0 bg-border",
|
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
|
||||||
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
|
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
)
|
}
|
||||||
Separator.displayName = SeparatorPrimitive.Root.displayName
|
|
||||||
|
|
||||||
export { Separator }
|
export { Separator }
|
||||||
|
|||||||
@@ -1,135 +1,132 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as SheetPrimitive from "@radix-ui/react-dialog"
|
import * as SheetPrimitive from "@radix-ui/react-dialog"
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
import { XIcon } from "lucide-react"
|
||||||
import { X } from "lucide-react"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const Sheet = SheetPrimitive.Root
|
function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
|
||||||
|
return <SheetPrimitive.Root data-slot="sheet" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
const SheetTrigger = SheetPrimitive.Trigger
|
function SheetTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
|
||||||
|
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
const SheetClose = SheetPrimitive.Close
|
function SheetClose({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SheetPrimitive.Close>) {
|
||||||
|
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
const SheetPortal = SheetPrimitive.Portal
|
function SheetPortal({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SheetPrimitive.Portal>) {
|
||||||
|
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
const SheetOverlay = React.forwardRef<
|
function SheetOverlay({
|
||||||
React.ElementRef<typeof SheetPrimitive.Overlay>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.ComponentProps<typeof SheetPrimitive.Overlay>) {
|
||||||
|
return (
|
||||||
<SheetPrimitive.Overlay
|
<SheetPrimitive.Overlay
|
||||||
|
data-slot="sheet-overlay"
|
||||||
className={cn(
|
className={cn(
|
||||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
"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
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
ref={ref}
|
|
||||||
/>
|
/>
|
||||||
))
|
|
||||||
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
|
|
||||||
|
|
||||||
const sheetVariants = cva(
|
|
||||||
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out",
|
|
||||||
{
|
|
||||||
variants: {
|
|
||||||
side: {
|
|
||||||
top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
|
|
||||||
bottom:
|
|
||||||
"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
|
|
||||||
left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
|
|
||||||
right:
|
|
||||||
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaultVariants: {
|
|
||||||
side: "right",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
interface SheetContentProps
|
function SheetContent({
|
||||||
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
|
className,
|
||||||
VariantProps<typeof sheetVariants> {}
|
children,
|
||||||
|
side = "right",
|
||||||
const SheetContent = React.forwardRef<
|
...props
|
||||||
React.ElementRef<typeof SheetPrimitive.Content>,
|
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
|
||||||
SheetContentProps
|
side?: "top" | "right" | "bottom" | "left"
|
||||||
>(({ side = "right", className, children, ...props }, ref) => (
|
}) {
|
||||||
|
return (
|
||||||
<SheetPortal>
|
<SheetPortal>
|
||||||
<SheetOverlay />
|
<SheetOverlay />
|
||||||
<SheetPrimitive.Content
|
<SheetPrimitive.Content
|
||||||
ref={ref}
|
data-slot="sheet-content"
|
||||||
className={cn(sheetVariants({ side }), className)}
|
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}
|
{...props}
|
||||||
>
|
>
|
||||||
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
|
{children}
|
||||||
<X className="h-4 w-4" />
|
<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>
|
<span className="sr-only">Close</span>
|
||||||
</SheetPrimitive.Close>
|
</SheetPrimitive.Close>
|
||||||
{children}
|
|
||||||
</SheetPrimitive.Content>
|
</SheetPrimitive.Content>
|
||||||
</SheetPortal>
|
</SheetPortal>
|
||||||
))
|
)
|
||||||
SheetContent.displayName = SheetPrimitive.Content.displayName
|
}
|
||||||
|
|
||||||
const SheetHeader = ({
|
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
className,
|
return (
|
||||||
...props
|
|
||||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
|
||||||
<div
|
<div
|
||||||
className={cn(
|
data-slot="sheet-header"
|
||||||
"flex flex-col space-y-2 text-center sm:text-left",
|
className={cn("flex flex-col gap-1.5 p-4", className)}
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
SheetHeader.displayName = "SheetHeader"
|
}
|
||||||
|
|
||||||
const SheetFooter = ({
|
function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
className,
|
return (
|
||||||
...props
|
|
||||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
|
||||||
<div
|
<div
|
||||||
className={cn(
|
data-slot="sheet-footer"
|
||||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
SheetFooter.displayName = "SheetFooter"
|
}
|
||||||
|
|
||||||
const SheetTitle = React.forwardRef<
|
function SheetTitle({
|
||||||
React.ElementRef<typeof SheetPrimitive.Title>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.ComponentProps<typeof SheetPrimitive.Title>) {
|
||||||
|
return (
|
||||||
<SheetPrimitive.Title
|
<SheetPrimitive.Title
|
||||||
ref={ref}
|
data-slot="sheet-title"
|
||||||
className={cn("text-lg font-semibold text-foreground", className)}
|
className={cn("text-foreground font-semibold", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
)
|
||||||
SheetTitle.displayName = SheetPrimitive.Title.displayName
|
}
|
||||||
|
|
||||||
const SheetDescription = React.forwardRef<
|
function SheetDescription({
|
||||||
React.ElementRef<typeof SheetPrimitive.Description>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.ComponentProps<typeof SheetPrimitive.Description>) {
|
||||||
|
return (
|
||||||
<SheetPrimitive.Description
|
<SheetPrimitive.Description
|
||||||
ref={ref}
|
data-slot="sheet-description"
|
||||||
className={cn("text-sm text-muted-foreground", className)}
|
className={cn("text-muted-foreground text-sm", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
)
|
||||||
SheetDescription.displayName = SheetPrimitive.Description.displayName
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Sheet,
|
Sheet,
|
||||||
SheetPortal,
|
|
||||||
SheetOverlay,
|
|
||||||
SheetTrigger,
|
SheetTrigger,
|
||||||
SheetClose,
|
SheetClose,
|
||||||
SheetContent,
|
SheetContent,
|
||||||
|
|||||||
@@ -1,14 +1,22 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import { Slot } from "@radix-ui/react-slot"
|
import { Slot } from "@radix-ui/react-slot"
|
||||||
import { VariantProps, cva } from "class-variance-authority"
|
import { cva, VariantProps } from "class-variance-authority"
|
||||||
import { PanelLeft } from "lucide-react"
|
import { PanelLeftIcon } from "lucide-react"
|
||||||
|
|
||||||
import { useIsMobile } from "@/hooks/use-mobile"
|
import { useIsMobile } from "@/hooks/use-mobile"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
import { Separator } from "@/components/ui/separator"
|
import { Separator } from "@/components/ui/separator"
|
||||||
import { Sheet, SheetContent } from "@/components/ui/sheet"
|
import {
|
||||||
|
Sheet,
|
||||||
|
SheetContent,
|
||||||
|
SheetDescription,
|
||||||
|
SheetHeader,
|
||||||
|
SheetTitle,
|
||||||
|
} from "@/components/ui/sheet"
|
||||||
import { Skeleton } from "@/components/ui/skeleton"
|
import { Skeleton } from "@/components/ui/skeleton"
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
@@ -24,7 +32,7 @@ const SIDEBAR_WIDTH_MOBILE = "18rem"
|
|||||||
const SIDEBAR_WIDTH_ICON = "3rem"
|
const SIDEBAR_WIDTH_ICON = "3rem"
|
||||||
const SIDEBAR_KEYBOARD_SHORTCUT = "b"
|
const SIDEBAR_KEYBOARD_SHORTCUT = "b"
|
||||||
|
|
||||||
type SidebarContext = {
|
type SidebarContextProps = {
|
||||||
state: "expanded" | "collapsed"
|
state: "expanded" | "collapsed"
|
||||||
open: boolean
|
open: boolean
|
||||||
setOpen: (open: boolean) => void
|
setOpen: (open: boolean) => void
|
||||||
@@ -34,7 +42,7 @@ type SidebarContext = {
|
|||||||
toggleSidebar: () => void
|
toggleSidebar: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const SidebarContext = React.createContext<SidebarContext | null>(null)
|
const SidebarContext = React.createContext<SidebarContextProps | null>(null)
|
||||||
|
|
||||||
function useSidebar() {
|
function useSidebar() {
|
||||||
const context = React.useContext(SidebarContext)
|
const context = React.useContext(SidebarContext)
|
||||||
@@ -45,16 +53,7 @@ function useSidebar() {
|
|||||||
return context
|
return context
|
||||||
}
|
}
|
||||||
|
|
||||||
const SidebarProvider = React.forwardRef<
|
function SidebarProvider({
|
||||||
HTMLDivElement,
|
|
||||||
React.ComponentProps<"div"> & {
|
|
||||||
defaultOpen?: boolean
|
|
||||||
open?: boolean
|
|
||||||
onOpenChange?: (open: boolean) => void
|
|
||||||
}
|
|
||||||
>(
|
|
||||||
(
|
|
||||||
{
|
|
||||||
defaultOpen = true,
|
defaultOpen = true,
|
||||||
open: openProp,
|
open: openProp,
|
||||||
onOpenChange: setOpenProp,
|
onOpenChange: setOpenProp,
|
||||||
@@ -62,9 +61,11 @@ const SidebarProvider = React.forwardRef<
|
|||||||
style,
|
style,
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
},
|
}: React.ComponentProps<"div"> & {
|
||||||
ref
|
defaultOpen?: boolean
|
||||||
) => {
|
open?: boolean
|
||||||
|
onOpenChange?: (open: boolean) => void
|
||||||
|
}) {
|
||||||
const isMobile = useIsMobile()
|
const isMobile = useIsMobile()
|
||||||
const [openMobile, setOpenMobile] = React.useState(false)
|
const [openMobile, setOpenMobile] = React.useState(false)
|
||||||
|
|
||||||
@@ -89,9 +90,7 @@ const SidebarProvider = React.forwardRef<
|
|||||||
|
|
||||||
// Helper to toggle the sidebar.
|
// Helper to toggle the sidebar.
|
||||||
const toggleSidebar = React.useCallback(() => {
|
const toggleSidebar = React.useCallback(() => {
|
||||||
return isMobile
|
return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open)
|
||||||
? setOpenMobile((open) => !open)
|
|
||||||
: setOpen((open) => !open)
|
|
||||||
}, [isMobile, setOpen, setOpenMobile])
|
}, [isMobile, setOpen, setOpenMobile])
|
||||||
|
|
||||||
// Adds a keyboard shortcut to toggle the sidebar.
|
// Adds a keyboard shortcut to toggle the sidebar.
|
||||||
@@ -114,7 +113,7 @@ const SidebarProvider = React.forwardRef<
|
|||||||
// This makes it easier to style the sidebar with Tailwind classes.
|
// This makes it easier to style the sidebar with Tailwind classes.
|
||||||
const state = open ? "expanded" : "collapsed"
|
const state = open ? "expanded" : "collapsed"
|
||||||
|
|
||||||
const contextValue = React.useMemo<SidebarContext>(
|
const contextValue = React.useMemo<SidebarContextProps>(
|
||||||
() => ({
|
() => ({
|
||||||
state,
|
state,
|
||||||
open,
|
open,
|
||||||
@@ -131,6 +130,7 @@ const SidebarProvider = React.forwardRef<
|
|||||||
<SidebarContext.Provider value={contextValue}>
|
<SidebarContext.Provider value={contextValue}>
|
||||||
<TooltipProvider delayDuration={0}>
|
<TooltipProvider delayDuration={0}>
|
||||||
<div
|
<div
|
||||||
|
data-slot="sidebar-wrapper"
|
||||||
style={
|
style={
|
||||||
{
|
{
|
||||||
"--sidebar-width": SIDEBAR_WIDTH,
|
"--sidebar-width": SIDEBAR_WIDTH,
|
||||||
@@ -139,10 +139,9 @@ const SidebarProvider = React.forwardRef<
|
|||||||
} as React.CSSProperties
|
} as React.CSSProperties
|
||||||
}
|
}
|
||||||
className={cn(
|
className={cn(
|
||||||
"group/sidebar-wrapper flex min-h-svh w-full has-[[data-variant=inset]]:bg-sidebar",
|
"group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex min-h-svh w-full",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
ref={ref}
|
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
@@ -151,38 +150,29 @@ const SidebarProvider = React.forwardRef<
|
|||||||
</SidebarContext.Provider>
|
</SidebarContext.Provider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
|
||||||
SidebarProvider.displayName = "SidebarProvider"
|
|
||||||
|
|
||||||
const Sidebar = React.forwardRef<
|
function Sidebar({
|
||||||
HTMLDivElement,
|
|
||||||
React.ComponentProps<"div"> & {
|
|
||||||
side?: "left" | "right"
|
|
||||||
variant?: "sidebar" | "floating" | "inset"
|
|
||||||
collapsible?: "offcanvas" | "icon" | "none"
|
|
||||||
}
|
|
||||||
>(
|
|
||||||
(
|
|
||||||
{
|
|
||||||
side = "left",
|
side = "left",
|
||||||
variant = "sidebar",
|
variant = "sidebar",
|
||||||
collapsible = "offcanvas",
|
collapsible = "offcanvas",
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
},
|
}: React.ComponentProps<"div"> & {
|
||||||
ref
|
side?: "left" | "right"
|
||||||
) => {
|
variant?: "sidebar" | "floating" | "inset"
|
||||||
|
collapsible?: "offcanvas" | "icon" | "none"
|
||||||
|
}) {
|
||||||
const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
|
const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
|
||||||
|
|
||||||
if (collapsible === "none") {
|
if (collapsible === "none") {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
data-slot="sidebar"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex h-full w-[--sidebar-width] flex-col bg-sidebar text-sidebar-foreground",
|
"bg-sidebar text-sidebar-foreground flex h-full w-(--sidebar-width) flex-col",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
ref={ref}
|
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
@@ -195,8 +185,9 @@ const Sidebar = React.forwardRef<
|
|||||||
<Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
|
<Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
|
||||||
<SheetContent
|
<SheetContent
|
||||||
data-sidebar="sidebar"
|
data-sidebar="sidebar"
|
||||||
|
data-slot="sidebar"
|
||||||
data-mobile="true"
|
data-mobile="true"
|
||||||
className="w-[--sidebar-width] bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden"
|
className="bg-sidebar text-sidebar-foreground w-(--sidebar-width) p-0 [&>button]:hidden"
|
||||||
style={
|
style={
|
||||||
{
|
{
|
||||||
"--sidebar-width": SIDEBAR_WIDTH_MOBILE,
|
"--sidebar-width": SIDEBAR_WIDTH_MOBILE,
|
||||||
@@ -204,6 +195,10 @@ const Sidebar = React.forwardRef<
|
|||||||
}
|
}
|
||||||
side={side}
|
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>
|
<div className="flex h-full w-full flex-col">{children}</div>
|
||||||
</SheetContent>
|
</SheetContent>
|
||||||
</Sheet>
|
</Sheet>
|
||||||
@@ -212,41 +207,44 @@ const Sidebar = React.forwardRef<
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
className="group peer text-sidebar-foreground hidden md:block"
|
||||||
className="group peer hidden text-sidebar-foreground md:block"
|
|
||||||
data-state={state}
|
data-state={state}
|
||||||
data-collapsible={state === "collapsed" ? collapsible : ""}
|
data-collapsible={state === "collapsed" ? collapsible : ""}
|
||||||
data-variant={variant}
|
data-variant={variant}
|
||||||
data-side={side}
|
data-side={side}
|
||||||
|
data-slot="sidebar"
|
||||||
>
|
>
|
||||||
{/* This is what handles the sidebar gap on desktop */}
|
{/* This is what handles the sidebar gap on desktop */}
|
||||||
<div
|
<div
|
||||||
|
data-slot="sidebar-gap"
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative h-svh w-[--sidebar-width] bg-transparent transition-[width] duration-200 ease-linear",
|
"relative w-(--sidebar-width) bg-transparent transition-[width] duration-200 ease-linear",
|
||||||
"group-data-[collapsible=offcanvas]:w-0",
|
"group-data-[collapsible=offcanvas]:w-0",
|
||||||
"group-data-[side=right]:rotate-180",
|
"group-data-[side=right]:rotate-180",
|
||||||
variant === "floating" || variant === "inset"
|
variant === "floating" || variant === "inset"
|
||||||
? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4))]"
|
? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]"
|
||||||
: "group-data-[collapsible=icon]:w-[--sidebar-width-icon]"
|
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon)"
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
|
data-slot="sidebar-container"
|
||||||
className={cn(
|
className={cn(
|
||||||
"fixed inset-y-0 z-10 hidden h-svh w-[--sidebar-width] transition-[left,right,width] duration-200 ease-linear md:flex",
|
"fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear md:flex",
|
||||||
side === "left"
|
side === "left"
|
||||||
? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"
|
? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"
|
||||||
: "right-0 group-data-[collapsible=offcanvas]:right-[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.
|
// Adjust the padding for floating and inset variants.
|
||||||
variant === "floating" || variant === "inset"
|
variant === "floating" || variant === "inset"
|
||||||
? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4)_+2px)]"
|
? "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",
|
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
data-sidebar="sidebar"
|
data-sidebar="sidebar"
|
||||||
className="flex h-full w-full flex-col bg-sidebar group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:border-sidebar-border group-data-[variant=floating]:shadow"
|
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}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
@@ -254,54 +252,49 @@ const Sidebar = React.forwardRef<
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
|
||||||
Sidebar.displayName = "Sidebar"
|
|
||||||
|
|
||||||
const SidebarTrigger = React.forwardRef<
|
function SidebarTrigger({
|
||||||
React.ElementRef<typeof Button>,
|
className,
|
||||||
React.ComponentProps<typeof Button>
|
onClick,
|
||||||
>(({ className, onClick, ...props }, ref) => {
|
...props
|
||||||
|
}: React.ComponentProps<typeof Button>) {
|
||||||
const { toggleSidebar } = useSidebar()
|
const { toggleSidebar } = useSidebar()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
ref={ref}
|
|
||||||
data-sidebar="trigger"
|
data-sidebar="trigger"
|
||||||
|
data-slot="sidebar-trigger"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className={cn("h-7 w-7", className)}
|
className={cn("size-7", className)}
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
onClick?.(event)
|
onClick?.(event)
|
||||||
toggleSidebar()
|
toggleSidebar()
|
||||||
}}
|
}}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<PanelLeft />
|
<PanelLeftIcon />
|
||||||
<span className="sr-only">Toggle Sidebar</span>
|
<span className="sr-only">Toggle Sidebar</span>
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
SidebarTrigger.displayName = "SidebarTrigger"
|
|
||||||
|
|
||||||
const SidebarRail = React.forwardRef<
|
function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
|
||||||
HTMLButtonElement,
|
|
||||||
React.ComponentProps<"button">
|
|
||||||
>(({ className, ...props }, ref) => {
|
|
||||||
const { toggleSidebar } = useSidebar()
|
const { toggleSidebar } = useSidebar()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
ref={ref}
|
|
||||||
data-sidebar="rail"
|
data-sidebar="rail"
|
||||||
|
data-slot="sidebar-rail"
|
||||||
aria-label="Toggle Sidebar"
|
aria-label="Toggle Sidebar"
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
onClick={toggleSidebar}
|
onClick={toggleSidebar}
|
||||||
title="Toggle Sidebar"
|
title="Toggle Sidebar"
|
||||||
className={cn(
|
className={cn(
|
||||||
"absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] hover:after:bg-sidebar-border group-data-[side=left]:-right-4 group-data-[side=right]:left-0 sm:flex",
|
"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",
|
||||||
"[[data-side=left]_&]:cursor-w-resize [[data-side=right]_&]:cursor-e-resize",
|
"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",
|
"[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize",
|
||||||
"group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full group-data-[collapsible=offcanvas]:hover:bg-sidebar",
|
"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=left][data-collapsible=offcanvas]_&]:-right-2",
|
||||||
"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
|
"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
|
||||||
className
|
className
|
||||||
@@ -309,97 +302,76 @@ const SidebarRail = React.forwardRef<
|
|||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
SidebarRail.displayName = "SidebarRail"
|
|
||||||
|
|
||||||
const SidebarInset = React.forwardRef<
|
function SidebarInset({ className, ...props }: React.ComponentProps<"main">) {
|
||||||
HTMLDivElement,
|
|
||||||
React.ComponentProps<"main">
|
|
||||||
>(({ className, ...props }, ref) => {
|
|
||||||
return (
|
return (
|
||||||
<main
|
<main
|
||||||
ref={ref}
|
data-slot="sidebar-inset"
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative flex min-h-svh flex-1 flex-col bg-background",
|
"bg-background relative flex w-full flex-1 flex-col",
|
||||||
"peer-data-[variant=inset]:min-h-[calc(100svh-theme(spacing.4))] md:peer-data-[variant=inset]:m-2 md:peer-data-[state=collapsed]:peer-data-[variant=inset]:ml-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow",
|
"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
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
SidebarInset.displayName = "SidebarInset"
|
|
||||||
|
|
||||||
const SidebarInput = React.forwardRef<
|
function SidebarInput({
|
||||||
React.ElementRef<typeof Input>,
|
className,
|
||||||
React.ComponentProps<typeof Input>
|
...props
|
||||||
>(({ className, ...props }, ref) => {
|
}: React.ComponentProps<typeof Input>) {
|
||||||
return (
|
return (
|
||||||
<Input
|
<Input
|
||||||
ref={ref}
|
data-slot="sidebar-input"
|
||||||
data-sidebar="input"
|
data-sidebar="input"
|
||||||
className={cn(
|
className={cn("bg-background h-8 w-full shadow-none", className)}
|
||||||
"h-8 w-full bg-background shadow-none focus-visible:ring-2 focus-visible:ring-sidebar-ring",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
SidebarInput.displayName = "SidebarInput"
|
|
||||||
|
|
||||||
const SidebarHeader = React.forwardRef<
|
function SidebarHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
HTMLDivElement,
|
|
||||||
React.ComponentProps<"div">
|
|
||||||
>(({ className, ...props }, ref) => {
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
data-slot="sidebar-header"
|
||||||
data-sidebar="header"
|
data-sidebar="header"
|
||||||
className={cn("flex flex-col gap-2 p-2", className)}
|
className={cn("flex flex-col gap-2 p-2", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
SidebarHeader.displayName = "SidebarHeader"
|
|
||||||
|
|
||||||
const SidebarFooter = React.forwardRef<
|
function SidebarFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
HTMLDivElement,
|
|
||||||
React.ComponentProps<"div">
|
|
||||||
>(({ className, ...props }, ref) => {
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
data-slot="sidebar-footer"
|
||||||
data-sidebar="footer"
|
data-sidebar="footer"
|
||||||
className={cn("flex flex-col gap-2 p-2", className)}
|
className={cn("flex flex-col gap-2 p-2", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
SidebarFooter.displayName = "SidebarFooter"
|
|
||||||
|
|
||||||
const SidebarSeparator = React.forwardRef<
|
function SidebarSeparator({
|
||||||
React.ElementRef<typeof Separator>,
|
className,
|
||||||
React.ComponentProps<typeof Separator>
|
...props
|
||||||
>(({ className, ...props }, ref) => {
|
}: React.ComponentProps<typeof Separator>) {
|
||||||
return (
|
return (
|
||||||
<Separator
|
<Separator
|
||||||
ref={ref}
|
data-slot="sidebar-separator"
|
||||||
data-sidebar="separator"
|
data-sidebar="separator"
|
||||||
className={cn("mx-2 w-auto bg-sidebar-border", className)}
|
className={cn("bg-sidebar-border mx-2 w-auto", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
SidebarSeparator.displayName = "SidebarSeparator"
|
|
||||||
|
|
||||||
const SidebarContent = React.forwardRef<
|
function SidebarContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
HTMLDivElement,
|
|
||||||
React.ComponentProps<"div">
|
|
||||||
>(({ className, ...props }, ref) => {
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
data-slot="sidebar-content"
|
||||||
data-sidebar="content"
|
data-sidebar="content"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
|
"flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
|
||||||
@@ -408,109 +380,101 @@ const SidebarContent = React.forwardRef<
|
|||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
SidebarContent.displayName = "SidebarContent"
|
|
||||||
|
|
||||||
const SidebarGroup = React.forwardRef<
|
function SidebarGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
HTMLDivElement,
|
|
||||||
React.ComponentProps<"div">
|
|
||||||
>(({ className, ...props }, ref) => {
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
data-slot="sidebar-group"
|
||||||
data-sidebar="group"
|
data-sidebar="group"
|
||||||
className={cn("relative flex w-full min-w-0 flex-col p-2", className)}
|
className={cn("relative flex w-full min-w-0 flex-col p-2", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
SidebarGroup.displayName = "SidebarGroup"
|
|
||||||
|
|
||||||
const SidebarGroupLabel = React.forwardRef<
|
function SidebarGroupLabel({
|
||||||
HTMLDivElement,
|
className,
|
||||||
React.ComponentProps<"div"> & { asChild?: boolean }
|
asChild = false,
|
||||||
>(({ className, asChild = false, ...props }, ref) => {
|
...props
|
||||||
|
}: React.ComponentProps<"div"> & { asChild?: boolean }) {
|
||||||
const Comp = asChild ? Slot : "div"
|
const Comp = asChild ? Slot : "div"
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Comp
|
<Comp
|
||||||
ref={ref}
|
data-slot="sidebar-group-label"
|
||||||
data-sidebar="group-label"
|
data-sidebar="group-label"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium text-sidebar-foreground/70 outline-none ring-sidebar-ring transition-[margin,opa] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
"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",
|
"group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
SidebarGroupLabel.displayName = "SidebarGroupLabel"
|
|
||||||
|
|
||||||
const SidebarGroupAction = React.forwardRef<
|
function SidebarGroupAction({
|
||||||
HTMLButtonElement,
|
className,
|
||||||
React.ComponentProps<"button"> & { asChild?: boolean }
|
asChild = false,
|
||||||
>(({ className, asChild = false, ...props }, ref) => {
|
...props
|
||||||
|
}: React.ComponentProps<"button"> & { asChild?: boolean }) {
|
||||||
const Comp = asChild ? Slot : "button"
|
const Comp = asChild ? Slot : "button"
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Comp
|
<Comp
|
||||||
ref={ref}
|
data-slot="sidebar-group-action"
|
||||||
data-sidebar="group-action"
|
data-sidebar="group-action"
|
||||||
className={cn(
|
className={cn(
|
||||||
"absolute right-3 top-3.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
"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.
|
// Increases the hit area of the button on mobile.
|
||||||
"after:absolute after:-inset-2 after:md:hidden",
|
"after:absolute after:-inset-2 md:after:hidden",
|
||||||
"group-data-[collapsible=icon]:hidden",
|
"group-data-[collapsible=icon]:hidden",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
SidebarGroupAction.displayName = "SidebarGroupAction"
|
|
||||||
|
|
||||||
const SidebarGroupContent = React.forwardRef<
|
function SidebarGroupContent({
|
||||||
HTMLDivElement,
|
className,
|
||||||
React.ComponentProps<"div">
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
data-slot="sidebar-group-content"
|
||||||
data-sidebar="group-content"
|
data-sidebar="group-content"
|
||||||
className={cn("w-full text-sm", className)}
|
className={cn("w-full text-sm", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
)
|
||||||
SidebarGroupContent.displayName = "SidebarGroupContent"
|
}
|
||||||
|
|
||||||
const SidebarMenu = React.forwardRef<
|
function SidebarMenu({ className, ...props }: React.ComponentProps<"ul">) {
|
||||||
HTMLUListElement,
|
return (
|
||||||
React.ComponentProps<"ul">
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<ul
|
<ul
|
||||||
ref={ref}
|
data-slot="sidebar-menu"
|
||||||
data-sidebar="menu"
|
data-sidebar="menu"
|
||||||
className={cn("flex w-full min-w-0 flex-col gap-1", className)}
|
className={cn("flex w-full min-w-0 flex-col gap-1", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
)
|
||||||
SidebarMenu.displayName = "SidebarMenu"
|
}
|
||||||
|
|
||||||
const SidebarMenuItem = React.forwardRef<
|
function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
|
||||||
HTMLLIElement,
|
return (
|
||||||
React.ComponentProps<"li">
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<li
|
<li
|
||||||
ref={ref}
|
data-slot="sidebar-menu-item"
|
||||||
data-sidebar="menu-item"
|
data-sidebar="menu-item"
|
||||||
className={cn("group/menu-item relative", className)}
|
className={cn("group/menu-item relative", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
)
|
||||||
SidebarMenuItem.displayName = "SidebarMenuItem"
|
}
|
||||||
|
|
||||||
const sidebarMenuButtonVariants = cva(
|
const sidebarMenuButtonVariants = cva(
|
||||||
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none 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",
|
"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: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
@@ -521,7 +485,7 @@ const sidebarMenuButtonVariants = cva(
|
|||||||
size: {
|
size: {
|
||||||
default: "h-8 text-sm",
|
default: "h-8 text-sm",
|
||||||
sm: "h-7 text-xs",
|
sm: "h-7 text-xs",
|
||||||
lg: "h-12 text-sm group-data-[collapsible=icon]:!p-0",
|
lg: "h-12 text-sm group-data-[collapsible=icon]:p-0!",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
@@ -531,16 +495,7 @@ const sidebarMenuButtonVariants = cva(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const SidebarMenuButton = React.forwardRef<
|
function SidebarMenuButton({
|
||||||
HTMLButtonElement,
|
|
||||||
React.ComponentProps<"button"> & {
|
|
||||||
asChild?: boolean
|
|
||||||
isActive?: boolean
|
|
||||||
tooltip?: string | React.ComponentProps<typeof TooltipContent>
|
|
||||||
} & VariantProps<typeof sidebarMenuButtonVariants>
|
|
||||||
>(
|
|
||||||
(
|
|
||||||
{
|
|
||||||
asChild = false,
|
asChild = false,
|
||||||
isActive = false,
|
isActive = false,
|
||||||
variant = "default",
|
variant = "default",
|
||||||
@@ -548,15 +503,17 @@ const SidebarMenuButton = React.forwardRef<
|
|||||||
tooltip,
|
tooltip,
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
},
|
}: React.ComponentProps<"button"> & {
|
||||||
ref
|
asChild?: boolean
|
||||||
) => {
|
isActive?: boolean
|
||||||
|
tooltip?: string | React.ComponentProps<typeof TooltipContent>
|
||||||
|
} & VariantProps<typeof sidebarMenuButtonVariants>) {
|
||||||
const Comp = asChild ? Slot : "button"
|
const Comp = asChild ? Slot : "button"
|
||||||
const { isMobile, state } = useSidebar()
|
const { isMobile, state } = useSidebar()
|
||||||
|
|
||||||
const button = (
|
const button = (
|
||||||
<Comp
|
<Comp
|
||||||
ref={ref}
|
data-slot="sidebar-menu-button"
|
||||||
data-sidebar="menu-button"
|
data-sidebar="menu-button"
|
||||||
data-size={size}
|
data-size={size}
|
||||||
data-active={isActive}
|
data-active={isActive}
|
||||||
@@ -587,49 +544,49 @@ const SidebarMenuButton = React.forwardRef<
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
|
||||||
SidebarMenuButton.displayName = "SidebarMenuButton"
|
|
||||||
|
|
||||||
const SidebarMenuAction = React.forwardRef<
|
function SidebarMenuAction({
|
||||||
HTMLButtonElement,
|
className,
|
||||||
React.ComponentProps<"button"> & {
|
asChild = false,
|
||||||
|
showOnHover = false,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"button"> & {
|
||||||
asChild?: boolean
|
asChild?: boolean
|
||||||
showOnHover?: boolean
|
showOnHover?: boolean
|
||||||
}
|
}) {
|
||||||
>(({ className, asChild = false, showOnHover = false, ...props }, ref) => {
|
|
||||||
const Comp = asChild ? Slot : "button"
|
const Comp = asChild ? Slot : "button"
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Comp
|
<Comp
|
||||||
ref={ref}
|
data-slot="sidebar-menu-action"
|
||||||
data-sidebar="menu-action"
|
data-sidebar="menu-action"
|
||||||
className={cn(
|
className={cn(
|
||||||
"absolute right-1 top-1.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 peer-hover/menu-button:text-sidebar-accent-foreground [&>svg]:size-4 [&>svg]:shrink-0",
|
"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.
|
// Increases the hit area of the button on mobile.
|
||||||
"after:absolute after:-inset-2 after:md:hidden",
|
"after:absolute after:-inset-2 md:after:hidden",
|
||||||
"peer-data-[size=sm]/menu-button:top-1",
|
"peer-data-[size=sm]/menu-button:top-1",
|
||||||
"peer-data-[size=default]/menu-button:top-1.5",
|
"peer-data-[size=default]/menu-button:top-1.5",
|
||||||
"peer-data-[size=lg]/menu-button:top-2.5",
|
"peer-data-[size=lg]/menu-button:top-2.5",
|
||||||
"group-data-[collapsible=icon]:hidden",
|
"group-data-[collapsible=icon]:hidden",
|
||||||
showOnHover &&
|
showOnHover &&
|
||||||
"group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground md:opacity-0",
|
"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
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
SidebarMenuAction.displayName = "SidebarMenuAction"
|
|
||||||
|
|
||||||
const SidebarMenuBadge = React.forwardRef<
|
function SidebarMenuBadge({
|
||||||
HTMLDivElement,
|
className,
|
||||||
React.ComponentProps<"div">
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
data-slot="sidebar-menu-badge"
|
||||||
data-sidebar="menu-badge"
|
data-sidebar="menu-badge"
|
||||||
className={cn(
|
className={cn(
|
||||||
"pointer-events-none absolute right-1 flex h-5 min-w-5 select-none items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums text-sidebar-foreground",
|
"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-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=sm]/menu-button:top-1",
|
||||||
"peer-data-[size=default]/menu-button:top-1.5",
|
"peer-data-[size=default]/menu-button:top-1.5",
|
||||||
@@ -639,15 +596,16 @@ const SidebarMenuBadge = React.forwardRef<
|
|||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
)
|
||||||
SidebarMenuBadge.displayName = "SidebarMenuBadge"
|
|
||||||
|
|
||||||
const SidebarMenuSkeleton = React.forwardRef<
|
|
||||||
HTMLDivElement,
|
|
||||||
React.ComponentProps<"div"> & {
|
|
||||||
showIcon?: boolean
|
|
||||||
}
|
}
|
||||||
>(({ className, showIcon = false, ...props }, ref) => {
|
|
||||||
|
function SidebarMenuSkeleton({
|
||||||
|
className,
|
||||||
|
showIcon = false,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div"> & {
|
||||||
|
showIcon?: boolean
|
||||||
|
}) {
|
||||||
// Random width between 50 to 90%.
|
// Random width between 50 to 90%.
|
||||||
const width = React.useMemo(() => {
|
const width = React.useMemo(() => {
|
||||||
return `${Math.floor(Math.random() * 40) + 50}%`
|
return `${Math.floor(Math.random() * 40) + 50}%`
|
||||||
@@ -655,7 +613,7 @@ const SidebarMenuSkeleton = React.forwardRef<
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
data-slot="sidebar-menu-skeleton"
|
||||||
data-sidebar="menu-skeleton"
|
data-sidebar="menu-skeleton"
|
||||||
className={cn("flex h-8 items-center gap-2 rounded-md px-2", className)}
|
className={cn("flex h-8 items-center gap-2 rounded-md px-2", className)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -667,7 +625,7 @@ const SidebarMenuSkeleton = React.forwardRef<
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Skeleton
|
<Skeleton
|
||||||
className="h-4 max-w-[--skeleton-width] flex-1"
|
className="h-4 max-w-(--skeleton-width) flex-1"
|
||||||
data-sidebar="menu-skeleton-text"
|
data-sidebar="menu-skeleton-text"
|
||||||
style={
|
style={
|
||||||
{
|
{
|
||||||
@@ -677,50 +635,58 @@ const SidebarMenuSkeleton = React.forwardRef<
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
SidebarMenuSkeleton.displayName = "SidebarMenuSkeleton"
|
|
||||||
|
|
||||||
const SidebarMenuSub = React.forwardRef<
|
function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) {
|
||||||
HTMLUListElement,
|
return (
|
||||||
React.ComponentProps<"ul">
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<ul
|
<ul
|
||||||
ref={ref}
|
data-slot="sidebar-menu-sub"
|
||||||
data-sidebar="menu-sub"
|
data-sidebar="menu-sub"
|
||||||
className={cn(
|
className={cn(
|
||||||
"mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l border-sidebar-border px-2.5 py-0.5",
|
"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",
|
"group-data-[collapsible=icon]:hidden",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
)
|
||||||
SidebarMenuSub.displayName = "SidebarMenuSub"
|
}
|
||||||
|
|
||||||
const SidebarMenuSubItem = React.forwardRef<
|
function SidebarMenuSubItem({
|
||||||
HTMLLIElement,
|
className,
|
||||||
React.ComponentProps<"li">
|
...props
|
||||||
>(({ ...props }, ref) => <li ref={ref} {...props} />)
|
}: React.ComponentProps<"li">) {
|
||||||
SidebarMenuSubItem.displayName = "SidebarMenuSubItem"
|
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<
|
function SidebarMenuSubButton({
|
||||||
HTMLAnchorElement,
|
asChild = false,
|
||||||
React.ComponentProps<"a"> & {
|
size = "md",
|
||||||
|
isActive = false,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"a"> & {
|
||||||
asChild?: boolean
|
asChild?: boolean
|
||||||
size?: "sm" | "md"
|
size?: "sm" | "md"
|
||||||
isActive?: boolean
|
isActive?: boolean
|
||||||
}
|
}) {
|
||||||
>(({ asChild = false, size = "md", isActive, className, ...props }, ref) => {
|
|
||||||
const Comp = asChild ? Slot : "a"
|
const Comp = asChild ? Slot : "a"
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Comp
|
<Comp
|
||||||
ref={ref}
|
data-slot="sidebar-menu-sub-button"
|
||||||
data-sidebar="menu-sub-button"
|
data-sidebar="menu-sub-button"
|
||||||
data-size={size}
|
data-size={size}
|
||||||
data-active={isActive}
|
data-active={isActive}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground outline-none ring-sidebar-ring 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 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground",
|
"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",
|
"data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
|
||||||
size === "sm" && "text-xs",
|
size === "sm" && "text-xs",
|
||||||
size === "md" && "text-sm",
|
size === "md" && "text-sm",
|
||||||
@@ -730,8 +696,7 @@ const SidebarMenuSubButton = React.forwardRef<
|
|||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
SidebarMenuSubButton.displayName = "SidebarMenuSubButton"
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Sidebar,
|
Sidebar,
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
function Skeleton({
|
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.HTMLAttributes<HTMLDivElement>) {
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn("animate-pulse rounded-md bg-primary/10", className)}
|
data-slot="skeleton"
|
||||||
|
className={cn("bg-accent animate-pulse rounded-md", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,26 +1,63 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as SliderPrimitive from "@radix-ui/react-slider"
|
import * as SliderPrimitive from "@radix-ui/react-slider"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const Slider = React.forwardRef<
|
function Slider({
|
||||||
React.ElementRef<typeof SliderPrimitive.Root>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
|
defaultValue,
|
||||||
>(({ className, ...props }, ref) => (
|
value,
|
||||||
|
min = 0,
|
||||||
|
max = 100,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SliderPrimitive.Root>) {
|
||||||
|
const _values = React.useMemo(
|
||||||
|
() =>
|
||||||
|
Array.isArray(value)
|
||||||
|
? value
|
||||||
|
: Array.isArray(defaultValue)
|
||||||
|
? defaultValue
|
||||||
|
: [min, max],
|
||||||
|
[value, defaultValue, min, max]
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
<SliderPrimitive.Root
|
<SliderPrimitive.Root
|
||||||
ref={ref}
|
data-slot="slider"
|
||||||
|
defaultValue={defaultValue}
|
||||||
|
value={value}
|
||||||
|
min={min}
|
||||||
|
max={max}
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative flex w-full touch-none select-none items-center",
|
"relative flex w-full touch-none items-center select-none data-[disabled]:opacity-50 data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-44 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<SliderPrimitive.Track className="relative h-1.5 w-full grow overflow-hidden rounded-full bg-primary/20">
|
<SliderPrimitive.Track
|
||||||
<SliderPrimitive.Range className="absolute h-full bg-primary" />
|
data-slot="slider-track"
|
||||||
|
className={cn(
|
||||||
|
"bg-muted relative grow overflow-hidden rounded-full data-[orientation=horizontal]:h-1.5 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-1.5"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<SliderPrimitive.Range
|
||||||
|
data-slot="slider-range"
|
||||||
|
className={cn(
|
||||||
|
"bg-primary absolute data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</SliderPrimitive.Track>
|
</SliderPrimitive.Track>
|
||||||
<SliderPrimitive.Thumb className="block h-4 w-4 rounded-full border border-primary/50 bg-background shadow transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50" />
|
{Array.from({ length: _values.length }, (_, index) => (
|
||||||
|
<SliderPrimitive.Thumb
|
||||||
|
data-slot="slider-thumb"
|
||||||
|
key={index}
|
||||||
|
className="border-primary bg-background ring-ring/50 block size-4 shrink-0 rounded-full border shadow-sm transition-[color,box-shadow] hover:ring-4 focus-visible:ring-4 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
</SliderPrimitive.Root>
|
</SliderPrimitive.Root>
|
||||||
))
|
)
|
||||||
Slider.displayName = SliderPrimitive.Root.displayName
|
}
|
||||||
|
|
||||||
export { Slider }
|
export { Slider }
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import { useTheme } from "next-themes"
|
import { useTheme } from "next-themes"
|
||||||
import { Toaster as Sonner } from "sonner"
|
import { Toaster as Sonner, ToasterProps } from "sonner"
|
||||||
|
|
||||||
type ToasterProps = React.ComponentProps<typeof Sonner>
|
|
||||||
|
|
||||||
const Toaster = ({ ...props }: ToasterProps) => {
|
const Toaster = ({ ...props }: ToasterProps) => {
|
||||||
const { theme = "system" } = useTheme()
|
const { theme = "system" } = useTheme()
|
||||||
@@ -12,17 +8,13 @@ const Toaster = ({ ...props }: ToasterProps) => {
|
|||||||
<Sonner
|
<Sonner
|
||||||
theme={theme as ToasterProps["theme"]}
|
theme={theme as ToasterProps["theme"]}
|
||||||
className="toaster group"
|
className="toaster group"
|
||||||
toastOptions={{
|
style={
|
||||||
classNames: {
|
{
|
||||||
toast:
|
"--normal-bg": "var(--popover)",
|
||||||
"group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
|
"--normal-text": "var(--popover-foreground)",
|
||||||
description: "group-[.toast]:text-muted-foreground",
|
"--normal-border": "var(--border)",
|
||||||
actionButton:
|
} as React.CSSProperties
|
||||||
"group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
|
}
|
||||||
cancelButton:
|
|
||||||
"group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,27 +1,31 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as SwitchPrimitives from "@radix-ui/react-switch"
|
import * as SwitchPrimitive from "@radix-ui/react-switch"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const Switch = React.forwardRef<
|
function Switch({
|
||||||
React.ElementRef<typeof SwitchPrimitives.Root>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.ComponentProps<typeof SwitchPrimitive.Root>) {
|
||||||
<SwitchPrimitives.Root
|
return (
|
||||||
|
<SwitchPrimitive.Root
|
||||||
|
data-slot="switch"
|
||||||
className={cn(
|
className={cn(
|
||||||
"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
|
"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
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
ref={ref}
|
|
||||||
>
|
>
|
||||||
<SwitchPrimitives.Thumb
|
<SwitchPrimitive.Thumb
|
||||||
|
data-slot="switch-thumb"
|
||||||
className={cn(
|
className={cn(
|
||||||
"pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0"
|
"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"
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</SwitchPrimitives.Root>
|
</SwitchPrimitive.Root>
|
||||||
))
|
)
|
||||||
Switch.displayName = SwitchPrimitives.Root.displayName
|
}
|
||||||
|
|
||||||
export { Switch }
|
export { Switch }
|
||||||
|
|||||||
@@ -2,111 +2,105 @@ import * as React from "react"
|
|||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const Table = React.forwardRef<
|
function Table({ className, ...props }: React.ComponentProps<"table">) {
|
||||||
HTMLTableElement,
|
return (
|
||||||
React.HTMLAttributes<HTMLTableElement>
|
<div
|
||||||
>(({ className, ...props }, ref) => (
|
data-slot="table-container"
|
||||||
<div className="relative w-full overflow-auto">
|
className="relative w-full overflow-x-auto"
|
||||||
|
>
|
||||||
<table
|
<table
|
||||||
ref={ref}
|
data-slot="table"
|
||||||
className={cn("w-full caption-bottom text-sm", className)}
|
className={cn("w-full caption-bottom text-sm", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))
|
)
|
||||||
Table.displayName = "Table"
|
}
|
||||||
|
|
||||||
const TableHeader = React.forwardRef<
|
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
|
||||||
HTMLTableSectionElement,
|
return (
|
||||||
React.HTMLAttributes<HTMLTableSectionElement>
|
<thead
|
||||||
>(({ className, ...props }, ref) => (
|
data-slot="table-header"
|
||||||
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
|
className={cn("[&_tr]:border-b", className)}
|
||||||
))
|
{...props}
|
||||||
TableHeader.displayName = "TableHeader"
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const TableBody = React.forwardRef<
|
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
|
||||||
HTMLTableSectionElement,
|
return (
|
||||||
React.HTMLAttributes<HTMLTableSectionElement>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<tbody
|
<tbody
|
||||||
ref={ref}
|
data-slot="table-body"
|
||||||
className={cn("[&_tr:last-child]:border-0", className)}
|
className={cn("[&_tr:last-child]:border-0", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
)
|
||||||
TableBody.displayName = "TableBody"
|
}
|
||||||
|
|
||||||
const TableFooter = React.forwardRef<
|
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
|
||||||
HTMLTableSectionElement,
|
return (
|
||||||
React.HTMLAttributes<HTMLTableSectionElement>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<tfoot
|
<tfoot
|
||||||
ref={ref}
|
data-slot="table-footer"
|
||||||
className={cn(
|
className={cn(
|
||||||
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
|
"bg-muted/50 border-t font-medium [&>tr]:last:border-b-0",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
)
|
||||||
TableFooter.displayName = "TableFooter"
|
}
|
||||||
|
|
||||||
const TableRow = React.forwardRef<
|
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
|
||||||
HTMLTableRowElement,
|
return (
|
||||||
React.HTMLAttributes<HTMLTableRowElement>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<tr
|
<tr
|
||||||
ref={ref}
|
data-slot="table-row"
|
||||||
className={cn(
|
className={cn(
|
||||||
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
|
"hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
)
|
||||||
TableRow.displayName = "TableRow"
|
}
|
||||||
|
|
||||||
const TableHead = React.forwardRef<
|
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
|
||||||
HTMLTableCellElement,
|
return (
|
||||||
React.ThHTMLAttributes<HTMLTableCellElement>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<th
|
<th
|
||||||
ref={ref}
|
data-slot="table-head"
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
"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
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
)
|
||||||
TableHead.displayName = "TableHead"
|
}
|
||||||
|
|
||||||
const TableCell = React.forwardRef<
|
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
|
||||||
HTMLTableCellElement,
|
return (
|
||||||
React.TdHTMLAttributes<HTMLTableCellElement>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<td
|
<td
|
||||||
ref={ref}
|
data-slot="table-cell"
|
||||||
className={cn(
|
className={cn(
|
||||||
"p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
)
|
||||||
TableCell.displayName = "TableCell"
|
}
|
||||||
|
|
||||||
const TableCaption = React.forwardRef<
|
function TableCaption({
|
||||||
HTMLTableCaptionElement,
|
className,
|
||||||
React.HTMLAttributes<HTMLTableCaptionElement>
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.ComponentProps<"caption">) {
|
||||||
|
return (
|
||||||
<caption
|
<caption
|
||||||
ref={ref}
|
data-slot="table-caption"
|
||||||
className={cn("mt-4 text-sm text-muted-foreground", className)}
|
className={cn("text-muted-foreground mt-4 text-sm", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
)
|
||||||
TableCaption.displayName = "TableCaption"
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Table,
|
Table,
|
||||||
|
|||||||
@@ -1,53 +1,66 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as TabsPrimitive from "@radix-ui/react-tabs"
|
import * as TabsPrimitive from "@radix-ui/react-tabs"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const Tabs = TabsPrimitive.Root
|
function Tabs({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<TabsPrimitive.Root
|
||||||
|
data-slot="tabs"
|
||||||
|
className={cn("flex flex-col gap-2", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const TabsList = React.forwardRef<
|
function TabsList({
|
||||||
React.ElementRef<typeof TabsPrimitive.List>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.ComponentProps<typeof TabsPrimitive.List>) {
|
||||||
|
return (
|
||||||
<TabsPrimitive.List
|
<TabsPrimitive.List
|
||||||
ref={ref}
|
data-slot="tabs-list"
|
||||||
className={cn(
|
className={cn(
|
||||||
"inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
|
"bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
)
|
||||||
TabsList.displayName = TabsPrimitive.List.displayName
|
}
|
||||||
|
|
||||||
const TabsTrigger = React.forwardRef<
|
function TabsTrigger({
|
||||||
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
|
||||||
|
return (
|
||||||
<TabsPrimitive.Trigger
|
<TabsPrimitive.Trigger
|
||||||
ref={ref}
|
data-slot="tabs-trigger"
|
||||||
className={cn(
|
className={cn(
|
||||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow",
|
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
)
|
||||||
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
|
}
|
||||||
|
|
||||||
const TabsContent = React.forwardRef<
|
function TabsContent({
|
||||||
React.ElementRef<typeof TabsPrimitive.Content>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
|
||||||
|
return (
|
||||||
<TabsPrimitive.Content
|
<TabsPrimitive.Content
|
||||||
ref={ref}
|
data-slot="tabs-content"
|
||||||
className={cn(
|
className={cn("flex-1 outline-none", className)}
|
||||||
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
)
|
||||||
TabsContent.displayName = TabsPrimitive.Content.displayName
|
}
|
||||||
|
|
||||||
export { Tabs, TabsList, TabsTrigger, TabsContent }
|
export { Tabs, TabsList, TabsTrigger, TabsContent }
|
||||||
|
|||||||
@@ -2,21 +2,17 @@ import * as React from "react"
|
|||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const Textarea = React.forwardRef<
|
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
||||||
HTMLTextAreaElement,
|
|
||||||
React.ComponentProps<"textarea">
|
|
||||||
>(({ className, ...props }, ref) => {
|
|
||||||
return (
|
return (
|
||||||
<textarea
|
<textarea
|
||||||
|
data-slot="textarea"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
"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
|
className
|
||||||
)}
|
)}
|
||||||
ref={ref}
|
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
Textarea.displayName = "Textarea"
|
|
||||||
|
|
||||||
export { Textarea }
|
export { Textarea }
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group"
|
import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group"
|
||||||
import { type VariantProps } from "class-variance-authority"
|
import { type VariantProps } from "class-variance-authority"
|
||||||
@@ -12,39 +14,53 @@ const ToggleGroupContext = React.createContext<
|
|||||||
variant: "default",
|
variant: "default",
|
||||||
})
|
})
|
||||||
|
|
||||||
const ToggleGroup = React.forwardRef<
|
function ToggleGroup({
|
||||||
React.ElementRef<typeof ToggleGroupPrimitive.Root>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Root> &
|
variant,
|
||||||
VariantProps<typeof toggleVariants>
|
size,
|
||||||
>(({ className, variant, size, children, ...props }, ref) => (
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ToggleGroupPrimitive.Root> &
|
||||||
|
VariantProps<typeof toggleVariants>) {
|
||||||
|
return (
|
||||||
<ToggleGroupPrimitive.Root
|
<ToggleGroupPrimitive.Root
|
||||||
ref={ref}
|
data-slot="toggle-group"
|
||||||
className={cn("flex items-center justify-center gap-1", className)}
|
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}
|
{...props}
|
||||||
>
|
>
|
||||||
<ToggleGroupContext.Provider value={{ variant, size }}>
|
<ToggleGroupContext.Provider value={{ variant, size }}>
|
||||||
{children}
|
{children}
|
||||||
</ToggleGroupContext.Provider>
|
</ToggleGroupContext.Provider>
|
||||||
</ToggleGroupPrimitive.Root>
|
</ToggleGroupPrimitive.Root>
|
||||||
))
|
)
|
||||||
|
}
|
||||||
|
|
||||||
ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName
|
function ToggleGroupItem({
|
||||||
|
className,
|
||||||
const ToggleGroupItem = React.forwardRef<
|
children,
|
||||||
React.ElementRef<typeof ToggleGroupPrimitive.Item>,
|
variant,
|
||||||
React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Item> &
|
size,
|
||||||
VariantProps<typeof toggleVariants>
|
...props
|
||||||
>(({ className, children, variant, size, ...props }, ref) => {
|
}: React.ComponentProps<typeof ToggleGroupPrimitive.Item> &
|
||||||
|
VariantProps<typeof toggleVariants>) {
|
||||||
const context = React.useContext(ToggleGroupContext)
|
const context = React.useContext(ToggleGroupContext)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ToggleGroupPrimitive.Item
|
<ToggleGroupPrimitive.Item
|
||||||
ref={ref}
|
data-slot="toggle-group-item"
|
||||||
|
data-variant={context.variant || variant}
|
||||||
|
data-size={context.size || size}
|
||||||
className={cn(
|
className={cn(
|
||||||
toggleVariants({
|
toggleVariants({
|
||||||
variant: context.variant || variant,
|
variant: context.variant || variant,
|
||||||
size: context.size || size,
|
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
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -52,8 +68,6 @@ const ToggleGroupItem = React.forwardRef<
|
|||||||
{children}
|
{children}
|
||||||
</ToggleGroupPrimitive.Item>
|
</ToggleGroupPrimitive.Item>
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
|
|
||||||
ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName
|
|
||||||
|
|
||||||
export { ToggleGroup, ToggleGroupItem }
|
export { ToggleGroup, ToggleGroupItem }
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as TogglePrimitive from "@radix-ui/react-toggle"
|
import * as TogglePrimitive from "@radix-ui/react-toggle"
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
@@ -7,13 +5,13 @@ import { cva, type VariantProps } from "class-variance-authority"
|
|||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const toggleVariants = cva(
|
const toggleVariants = cva(
|
||||||
"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
"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: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default: "bg-transparent",
|
default: "bg-transparent",
|
||||||
outline:
|
outline:
|
||||||
"border border-input bg-transparent shadow-sm hover:bg-accent hover:text-accent-foreground",
|
"border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground",
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
default: "h-9 px-2 min-w-9",
|
default: "h-9 px-2 min-w-9",
|
||||||
@@ -28,18 +26,20 @@ const toggleVariants = cva(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const Toggle = React.forwardRef<
|
function Toggle({
|
||||||
React.ElementRef<typeof TogglePrimitive.Root>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof TogglePrimitive.Root> &
|
variant,
|
||||||
VariantProps<typeof toggleVariants>
|
size,
|
||||||
>(({ className, variant, size, ...props }, ref) => (
|
...props
|
||||||
|
}: React.ComponentProps<typeof TogglePrimitive.Root> &
|
||||||
|
VariantProps<typeof toggleVariants>) {
|
||||||
|
return (
|
||||||
<TogglePrimitive.Root
|
<TogglePrimitive.Root
|
||||||
ref={ref}
|
data-slot="toggle"
|
||||||
className={cn(toggleVariants({ variant, size, className }))}
|
className={cn(toggleVariants({ variant, size, className }))}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
)
|
||||||
|
}
|
||||||
Toggle.displayName = TogglePrimitive.Root.displayName
|
|
||||||
|
|
||||||
export { Toggle, toggleVariants }
|
export { Toggle, toggleVariants }
|
||||||
|
|||||||
@@ -1,32 +1,59 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
|
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const TooltipProvider = TooltipPrimitive.Provider
|
function TooltipProvider({
|
||||||
|
delayDuration = 0,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
|
||||||
|
return (
|
||||||
|
<TooltipPrimitive.Provider
|
||||||
|
data-slot="tooltip-provider"
|
||||||
|
delayDuration={delayDuration}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const Tooltip = TooltipPrimitive.Root
|
function Tooltip({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<TooltipProvider>
|
||||||
|
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
|
||||||
|
</TooltipProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const TooltipTrigger = TooltipPrimitive.Trigger
|
function TooltipTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
|
||||||
|
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
const TooltipContent = React.forwardRef<
|
function TooltipContent({
|
||||||
React.ElementRef<typeof TooltipPrimitive.Content>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
sideOffset = 0,
|
||||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
|
||||||
|
return (
|
||||||
<TooltipPrimitive.Portal>
|
<TooltipPrimitive.Portal>
|
||||||
<TooltipPrimitive.Content
|
<TooltipPrimitive.Content
|
||||||
ref={ref}
|
data-slot="tooltip-content"
|
||||||
sideOffset={sideOffset}
|
sideOffset={sideOffset}
|
||||||
className={cn(
|
className={cn(
|
||||||
"z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs 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",
|
"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
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
>
|
||||||
|
{children}
|
||||||
|
<TooltipPrimitive.Arrow className="bg-primary fill-primary z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
|
||||||
|
</TooltipPrimitive.Content>
|
||||||
</TooltipPrimitive.Portal>
|
</TooltipPrimitive.Portal>
|
||||||
))
|
)
|
||||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName
|
}
|
||||||
|
|
||||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
||||||
|
|||||||
@@ -31,15 +31,16 @@ export default function useAppUpdater() {
|
|||||||
switch (event.event) {
|
switch (event.event) {
|
||||||
case 'Started':
|
case 'Started':
|
||||||
contentLength = event.data.contentLength;
|
contentLength = event.data.contentLength;
|
||||||
console.log(`started downloading ${event.data.contentLength} bytes`);
|
console.log(`started downloading app update of ${event.data.contentLength} bytes`);
|
||||||
break;
|
break;
|
||||||
case 'Progress':
|
case 'Progress':
|
||||||
downloaded += event.data.chunkLength;
|
downloaded += event.data.chunkLength;
|
||||||
setDownloadProgress(downloaded / (contentLength || 0));
|
const progress = (downloaded / (contentLength || 1)) * 100;
|
||||||
console.log(`downloaded ${downloaded} from ${contentLength}`);
|
setDownloadProgress(Math.round(progress * 10) / 10);
|
||||||
|
console.log(`downloaded ${downloaded} bytes from ${contentLength} bytes of app update`);
|
||||||
break;
|
break;
|
||||||
case 'Finished':
|
case 'Finished':
|
||||||
console.log('download finished');
|
console.log('app update download finished');
|
||||||
setIsUpdating(false);
|
setIsUpdating(false);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
204
src/index.css
204
src/index.css
@@ -1,80 +1,130 @@
|
|||||||
@tailwind base;
|
@import "tailwindcss";
|
||||||
@tailwind components;
|
@import "tw-animate-css";
|
||||||
@tailwind utilities;
|
|
||||||
@layer base {
|
@custom-variant dark (&:is(.dark *));
|
||||||
:root {
|
|
||||||
--background: 0 0% 100%;
|
@theme inline {
|
||||||
--foreground: 240 10% 3.9%;
|
--radius-sm: calc(var(--radius) - 4px);
|
||||||
--card: 0 0% 100%;
|
--radius-md: calc(var(--radius) - 2px);
|
||||||
--card-foreground: 240 10% 3.9%;
|
--radius-lg: var(--radius);
|
||||||
--popover: 0 0% 100%;
|
--radius-xl: calc(var(--radius) + 4px);
|
||||||
--popover-foreground: 240 10% 3.9%;
|
--color-background: var(--background);
|
||||||
--primary: 240 5.9% 10%;
|
--color-foreground: var(--foreground);
|
||||||
--primary-foreground: 0 0% 98%;
|
--color-card: var(--card);
|
||||||
--secondary: 240 4.8% 95.9%;
|
--color-card-foreground: var(--card-foreground);
|
||||||
--secondary-foreground: 240 5.9% 10%;
|
--color-popover: var(--popover);
|
||||||
--muted: 240 4.8% 95.9%;
|
--color-popover-foreground: var(--popover-foreground);
|
||||||
--muted-foreground: 240 3.8% 46.1%;
|
--color-primary: var(--primary);
|
||||||
--accent: 240 4.8% 95.9%;
|
--color-primary-foreground: var(--primary-foreground);
|
||||||
--accent-foreground: 240 5.9% 10%;
|
--color-secondary: var(--secondary);
|
||||||
--destructive: 0 84.2% 60.2%;
|
--color-secondary-foreground: var(--secondary-foreground);
|
||||||
--destructive-foreground: 0 0% 98%;
|
--color-muted: var(--muted);
|
||||||
--border: 240 5.9% 90%;
|
--color-muted-foreground: var(--muted-foreground);
|
||||||
--input: 240 5.9% 90%;
|
--color-accent: var(--accent);
|
||||||
--ring: 240 10% 3.9%;
|
--color-accent-foreground: var(--accent-foreground);
|
||||||
--chart-1: 12 76% 61%;
|
--color-destructive: var(--destructive);
|
||||||
--chart-2: 173 58% 39%;
|
--color-border: var(--border);
|
||||||
--chart-3: 197 37% 24%;
|
--color-input: var(--input);
|
||||||
--chart-4: 43 74% 66%;
|
--color-ring: var(--ring);
|
||||||
--chart-5: 27 87% 67%;
|
--color-chart-1: var(--chart-1);
|
||||||
--radius: 0.5rem
|
--color-chart-2: var(--chart-2);
|
||||||
;
|
--color-chart-3: var(--chart-3);
|
||||||
--sidebar-background: 0 0% 98%;
|
--color-chart-4: var(--chart-4);
|
||||||
--sidebar-foreground: 240 5.3% 26.1%;
|
--color-chart-5: var(--chart-5);
|
||||||
--sidebar-primary: 240 5.9% 10%;
|
--color-sidebar: var(--sidebar);
|
||||||
--sidebar-primary-foreground: 0 0% 98%;
|
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||||
--sidebar-accent: 240 4.8% 95.9%;
|
--color-sidebar-primary: var(--sidebar-primary);
|
||||||
--sidebar-accent-foreground: 240 5.9% 10%;
|
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||||
--sidebar-border: 220 13% 91%;
|
--color-sidebar-accent: var(--sidebar-accent);
|
||||||
--sidebar-ring: 217.2 91.2% 59.8%}
|
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||||
.dark {
|
--color-sidebar-border: var(--sidebar-border);
|
||||||
--background: 240 10% 3.9%;
|
--color-sidebar-ring: var(--sidebar-ring);
|
||||||
--foreground: 0 0% 98%;
|
|
||||||
--card: 240 10% 3.9%;
|
@keyframes indeterminate-progress {
|
||||||
--card-foreground: 0 0% 98%;
|
0% {
|
||||||
--popover: 240 10% 3.9%;
|
transform: translateX(0) scaleX(0);
|
||||||
--popover-foreground: 0 0% 98%;
|
|
||||||
--primary: 0 0% 98%;
|
|
||||||
--primary-foreground: 240 5.9% 10%;
|
|
||||||
--secondary: 240 3.7% 15.9%;
|
|
||||||
--secondary-foreground: 0 0% 98%;
|
|
||||||
--muted: 240 3.7% 15.9%;
|
|
||||||
--muted-foreground: 240 5% 64.9%;
|
|
||||||
--accent: 240 3.7% 15.9%;
|
|
||||||
--accent-foreground: 0 0% 98%;
|
|
||||||
--destructive: 0 62.8% 30.6%;
|
|
||||||
--destructive-foreground: 0 0% 98%;
|
|
||||||
--border: 240 3.7% 15.9%;
|
|
||||||
--input: 240 3.7% 15.9%;
|
|
||||||
--ring: 240 4.9% 83.9%;
|
|
||||||
--chart-1: 220 70% 50%;
|
|
||||||
--chart-2: 160 60% 45%;
|
|
||||||
--chart-3: 30 80% 55%;
|
|
||||||
--chart-4: 280 65% 60%;
|
|
||||||
--chart-5: 340 75% 55%
|
|
||||||
;
|
|
||||||
--sidebar-background: 240 5.9% 10%;
|
|
||||||
--sidebar-foreground: 240 4.8% 95.9%;
|
|
||||||
--sidebar-primary: 224.3 76.3% 48%;
|
|
||||||
--sidebar-primary-foreground: 0 0% 100%;
|
|
||||||
--sidebar-accent: 240 3.7% 15.9%;
|
|
||||||
--sidebar-accent-foreground: 240 4.8% 95.9%;
|
|
||||||
--sidebar-border: 240 3.7% 15.9%;
|
|
||||||
--sidebar-ring: 217.2 91.2% 59.8%}
|
|
||||||
}
|
}
|
||||||
|
40% {
|
||||||
|
transform: translateX(0) scaleX(0.4);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateX(100%) scaleX(0.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--radius: 0.625rem;
|
||||||
|
--background: oklch(1 0 0);
|
||||||
|
--foreground: oklch(0.141 0.005 285.823);
|
||||||
|
--card: oklch(1 0 0);
|
||||||
|
--card-foreground: oklch(0.141 0.005 285.823);
|
||||||
|
--popover: oklch(1 0 0);
|
||||||
|
--popover-foreground: oklch(0.141 0.005 285.823);
|
||||||
|
--primary: oklch(0.21 0.006 285.885);
|
||||||
|
--primary-foreground: oklch(0.985 0 0);
|
||||||
|
--secondary: oklch(0.967 0.001 286.375);
|
||||||
|
--secondary-foreground: oklch(0.21 0.006 285.885);
|
||||||
|
--muted: oklch(0.967 0.001 286.375);
|
||||||
|
--muted-foreground: oklch(0.552 0.016 285.938);
|
||||||
|
--accent: oklch(0.967 0.001 286.375);
|
||||||
|
--accent-foreground: oklch(0.21 0.006 285.885);
|
||||||
|
--destructive: oklch(0.577 0.245 27.325);
|
||||||
|
--border: oklch(0.92 0.004 286.32);
|
||||||
|
--input: oklch(0.92 0.004 286.32);
|
||||||
|
--ring: oklch(0.705 0.015 286.067);
|
||||||
|
--chart-1: oklch(0.646 0.222 41.116);
|
||||||
|
--chart-2: oklch(0.6 0.118 184.704);
|
||||||
|
--chart-3: oklch(0.398 0.07 227.392);
|
||||||
|
--chart-4: oklch(0.828 0.189 84.429);
|
||||||
|
--chart-5: oklch(0.769 0.188 70.08);
|
||||||
|
--sidebar: oklch(0.985 0 0);
|
||||||
|
--sidebar-foreground: oklch(0.141 0.005 285.823);
|
||||||
|
--sidebar-primary: oklch(0.21 0.006 285.885);
|
||||||
|
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-accent: oklch(0.967 0.001 286.375);
|
||||||
|
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
|
||||||
|
--sidebar-border: oklch(0.92 0.004 286.32);
|
||||||
|
--sidebar-ring: oklch(0.705 0.015 286.067);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--background: oklch(0.141 0.005 285.823);
|
||||||
|
--foreground: oklch(0.985 0 0);
|
||||||
|
--card: oklch(0.21 0.006 285.885);
|
||||||
|
--card-foreground: oklch(0.985 0 0);
|
||||||
|
--popover: oklch(0.21 0.006 285.885);
|
||||||
|
--popover-foreground: oklch(0.985 0 0);
|
||||||
|
--primary: oklch(0.92 0.004 286.32);
|
||||||
|
--primary-foreground: oklch(0.21 0.006 285.885);
|
||||||
|
--secondary: oklch(0.274 0.006 286.033);
|
||||||
|
--secondary-foreground: oklch(0.985 0 0);
|
||||||
|
--muted: oklch(0.274 0.006 286.033);
|
||||||
|
--muted-foreground: oklch(0.705 0.015 286.067);
|
||||||
|
--accent: oklch(0.274 0.006 286.033);
|
||||||
|
--accent-foreground: oklch(0.985 0 0);
|
||||||
|
--destructive: oklch(0.704 0.191 22.216);
|
||||||
|
--border: oklch(1 0 0 / 10%);
|
||||||
|
--input: oklch(1 0 0 / 15%);
|
||||||
|
--ring: oklch(0.552 0.016 285.938);
|
||||||
|
--chart-1: oklch(0.488 0.243 264.376);
|
||||||
|
--chart-2: oklch(0.696 0.17 162.48);
|
||||||
|
--chart-3: oklch(0.769 0.188 70.08);
|
||||||
|
--chart-4: oklch(0.627 0.265 303.9);
|
||||||
|
--chart-5: oklch(0.645 0.246 16.439);
|
||||||
|
--sidebar: oklch(0.21 0.006 285.885);
|
||||||
|
--sidebar-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||||
|
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-accent: oklch(0.274 0.006 286.033);
|
||||||
|
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-border: oklch(1 0 0 / 10%);
|
||||||
|
--sidebar-ring: oklch(0.552 0.016 285.938);
|
||||||
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
* {
|
* {
|
||||||
@apply border-border;
|
@apply border-border outline-ring/50;
|
||||||
}
|
}
|
||||||
body {
|
body {
|
||||||
@apply bg-background text-foreground;
|
@apply bg-background text-foreground;
|
||||||
@@ -92,3 +142,9 @@
|
|||||||
@apply bg-foreground;
|
@apply bg-foreground;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@utility no-scrollbar {
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,12 +7,12 @@ import { Input } from "@/components/ui/input";
|
|||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import { useAppContext } from "@/providers/appContextProvider";
|
import { useAppContext } from "@/providers/appContextProvider";
|
||||||
import { useCurrentVideoMetadataStore, useDownloaderPageStatesStore } from "@/services/store";
|
import { useCurrentVideoMetadataStore, useDownloaderPageStatesStore, useSettingsPageStatesStore } from "@/services/store";
|
||||||
import { determineFileType, fileFormatFilter, formatBitrate, formatDurationString, formatFileSize, formatReleaseDate, formatYtStyleCount, isObjEmpty, sortByBitrate } from "@/utils";
|
import { determineFileType, fileFormatFilter, formatBitrate, formatDurationString, formatFileSize, formatReleaseDate, formatYtStyleCount, isObjEmpty, sortByBitrate } from "@/utils";
|
||||||
import { Calendar, Clock, DownloadCloud, Eye, Info, Loader2, Music, ThumbsUp, Video, File, ListVideo } from "lucide-react";
|
import { Calendar, Clock, DownloadCloud, Eye, Info, Loader2, Music, ThumbsUp, Video, File, ListVideo, PackageSearch, AlertCircleIcon, X } from "lucide-react";
|
||||||
import { FormatSelectionGroup, FormatSelectionGroupItem } from "@/components/custom/formatSelectionGroup";
|
import { FormatSelectionGroup, FormatSelectionGroupItem } from "@/components/custom/formatSelectionGroup";
|
||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
|
import { ToggleGroup, ToggleGroupItem } from "@/components/custom/legacyToggleGroup";
|
||||||
import { VideoFormat } from "@/types/video";
|
import { VideoFormat } from "@/types/video";
|
||||||
// import { PlaylistToggleGroup, PlaylistToggleGroupItem } from "@/components/custom/playlistToggleGroup";
|
// import { PlaylistToggleGroup, PlaylistToggleGroupItem } from "@/components/custom/playlistToggleGroup";
|
||||||
import { PlaylistSelectionGroup, PlaylistSelectionGroupItem } from "@/components/custom/playlistSelectionGroup";
|
import { PlaylistSelectionGroup, PlaylistSelectionGroupItem } from "@/components/custom/playlistSelectionGroup";
|
||||||
@@ -21,10 +21,16 @@ import { useForm } from "react-hook-form";
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
import { Form, FormControl, FormField, FormItem, FormMessage } from "@/components/ui/form";
|
import { Form, FormControl, FormField, FormItem, FormMessage } from "@/components/ui/form";
|
||||||
import { config } from "@/config";
|
import { config } from "@/config";
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||||
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
|
||||||
const searchFormSchema = z.object({
|
const searchFormSchema = z.object({
|
||||||
url: z.string().min(1, { message: "URL is required" })
|
url: z.url({
|
||||||
.url({message: "Invalid URL format." }),
|
error: (issue) => issue.input === undefined || issue.input === null || issue.input === ""
|
||||||
|
? "URL is required"
|
||||||
|
: "Invalid URL format"
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default function DownloaderPage() {
|
export default function DownloaderPage() {
|
||||||
@@ -36,21 +42,33 @@ export default function DownloaderPage() {
|
|||||||
const isMetadataLoading = useCurrentVideoMetadataStore((state) => state.isMetadataLoading);
|
const isMetadataLoading = useCurrentVideoMetadataStore((state) => state.isMetadataLoading);
|
||||||
const requestedUrl = useCurrentVideoMetadataStore((state) => state.requestedUrl);
|
const requestedUrl = useCurrentVideoMetadataStore((state) => state.requestedUrl);
|
||||||
const autoSubmitSearch = useCurrentVideoMetadataStore((state) => state.autoSubmitSearch);
|
const autoSubmitSearch = useCurrentVideoMetadataStore((state) => state.autoSubmitSearch);
|
||||||
|
const searchPid = useCurrentVideoMetadataStore((state) => state.searchPid);
|
||||||
const setVideoUrl = useCurrentVideoMetadataStore((state) => state.setVideoUrl);
|
const setVideoUrl = useCurrentVideoMetadataStore((state) => state.setVideoUrl);
|
||||||
const setVideoMetadata = useCurrentVideoMetadataStore((state) => state.setVideoMetadata);
|
const setVideoMetadata = useCurrentVideoMetadataStore((state) => state.setVideoMetadata);
|
||||||
const setIsMetadataLoading = useCurrentVideoMetadataStore((state) => state.setIsMetadataLoading);
|
const setIsMetadataLoading = useCurrentVideoMetadataStore((state) => state.setIsMetadataLoading);
|
||||||
const setRequestedUrl = useCurrentVideoMetadataStore((state) => state.setRequestedUrl);
|
const setRequestedUrl = useCurrentVideoMetadataStore((state) => state.setRequestedUrl);
|
||||||
const setAutoSubmitSearch = useCurrentVideoMetadataStore((state) => state.setAutoSubmitSearch);
|
const setAutoSubmitSearch = useCurrentVideoMetadataStore((state) => state.setAutoSubmitSearch);
|
||||||
|
const setSearchPid = useCurrentVideoMetadataStore((state) => state.setSearchPid);
|
||||||
|
const setShowSearchError = useCurrentVideoMetadataStore((state) => state.setShowSearchError);
|
||||||
|
|
||||||
|
const activeDownloadModeTab = useDownloaderPageStatesStore((state) => state.activeDownloadModeTab);
|
||||||
const isStartingDownload = useDownloaderPageStatesStore((state) => state.isStartingDownload);
|
const isStartingDownload = useDownloaderPageStatesStore((state) => state.isStartingDownload);
|
||||||
const selctedDownloadFormat = useDownloaderPageStatesStore((state) => state.selctedDownloadFormat);
|
const selectedDownloadFormat = useDownloaderPageStatesStore((state) => state.selectedDownloadFormat);
|
||||||
|
const selectedCombinableVideoFormat = useDownloaderPageStatesStore((state) => state.selectedCombinableVideoFormat);
|
||||||
|
const selectedCombinableAudioFormat = useDownloaderPageStatesStore((state) => state.selectedCombinableAudioFormat);
|
||||||
const selectedSubtitles = useDownloaderPageStatesStore((state) => state.selectedSubtitles);
|
const selectedSubtitles = useDownloaderPageStatesStore((state) => state.selectedSubtitles);
|
||||||
const selectedPlaylistVideoIndex = useDownloaderPageStatesStore((state) => state.selectedPlaylistVideoIndex);
|
const selectedPlaylistVideoIndex = useDownloaderPageStatesStore((state) => state.selectedPlaylistVideoIndex);
|
||||||
|
const setActiveDownloadModeTab = useDownloaderPageStatesStore((state) => state.setActiveDownloadModeTab);
|
||||||
const setIsStartingDownload = useDownloaderPageStatesStore((state) => state.setIsStartingDownload);
|
const setIsStartingDownload = useDownloaderPageStatesStore((state) => state.setIsStartingDownload);
|
||||||
const setSelctedDownloadFormat = useDownloaderPageStatesStore((state) => state.setSelctedDownloadFormat);
|
const setSelectedDownloadFormat = useDownloaderPageStatesStore((state) => state.setSelectedDownloadFormat);
|
||||||
|
const setSelectedCombinableVideoFormat = useDownloaderPageStatesStore((state) => state.setSelectedCombinableVideoFormat);
|
||||||
|
const setSelectedCombinableAudioFormat = useDownloaderPageStatesStore((state) => state.setSelectedCombinableAudioFormat);
|
||||||
const setSelectedSubtitles = useDownloaderPageStatesStore((state) => state.setSelectedSubtitles);
|
const setSelectedSubtitles = useDownloaderPageStatesStore((state) => state.setSelectedSubtitles);
|
||||||
const setSelectedPlaylistVideoIndex = useDownloaderPageStatesStore((state) => state.setSelectedPlaylistVideoIndex);
|
const setSelectedPlaylistVideoIndex = useDownloaderPageStatesStore((state) => state.setSelectedPlaylistVideoIndex);
|
||||||
|
|
||||||
|
const videoFormat = useSettingsPageStatesStore(state => state.settings.video_format);
|
||||||
|
const audioFormat = useSettingsPageStatesStore(state => state.settings.audio_format);
|
||||||
|
|
||||||
const audioOnlyFormats = videoMetadata?._type === 'video' ? sortByBitrate(videoMetadata?.formats.filter(fileFormatFilter('audio'))) : videoMetadata?._type === 'playlist' ? sortByBitrate(videoMetadata?.entries[Number(selectedPlaylistVideoIndex) - 1].formats.filter(fileFormatFilter('audio'))) : [];
|
const audioOnlyFormats = videoMetadata?._type === 'video' ? sortByBitrate(videoMetadata?.formats.filter(fileFormatFilter('audio'))) : videoMetadata?._type === 'playlist' ? sortByBitrate(videoMetadata?.entries[Number(selectedPlaylistVideoIndex) - 1].formats.filter(fileFormatFilter('audio'))) : [];
|
||||||
const videoOnlyFormats = videoMetadata?._type === 'video' ? sortByBitrate(videoMetadata?.formats.filter(fileFormatFilter('video'))) : videoMetadata?._type === 'playlist' ? sortByBitrate(videoMetadata?.entries[Number(selectedPlaylistVideoIndex) - 1].formats.filter(fileFormatFilter('video'))) : [];
|
const videoOnlyFormats = videoMetadata?._type === 'video' ? sortByBitrate(videoMetadata?.formats.filter(fileFormatFilter('video'))) : videoMetadata?._type === 'playlist' ? sortByBitrate(videoMetadata?.entries[Number(selectedPlaylistVideoIndex) - 1].formats.filter(fileFormatFilter('video'))) : [];
|
||||||
const combinedFormats = videoMetadata?._type === 'video' ? sortByBitrate(videoMetadata?.formats.filter(fileFormatFilter('video+audio'))) : videoMetadata?._type === 'playlist' ? sortByBitrate(videoMetadata?.entries[Number(selectedPlaylistVideoIndex) - 1].formats.filter(fileFormatFilter('video+audio'))) : [];
|
const combinedFormats = videoMetadata?._type === 'video' ? sortByBitrate(videoMetadata?.formats.filter(fileFormatFilter('video+audio'))) : videoMetadata?._type === 'playlist' ? sortByBitrate(videoMetadata?.entries[Number(selectedPlaylistVideoIndex) - 1].formats.filter(fileFormatFilter('video+audio'))) : [];
|
||||||
@@ -79,22 +97,44 @@ export default function DownloaderPage() {
|
|||||||
const allFilteredFormats = [...(audioOnlyFormats || []), ...(videoOnlyFormats || []), ...(combinedFormats || []), ...(qualityPresetFormats || [])];
|
const allFilteredFormats = [...(audioOnlyFormats || []), ...(videoOnlyFormats || []), ...(combinedFormats || []), ...(qualityPresetFormats || [])];
|
||||||
const selectedFormat = (() => {
|
const selectedFormat = (() => {
|
||||||
if (videoMetadata?._type === 'video') {
|
if (videoMetadata?._type === 'video') {
|
||||||
if (selctedDownloadFormat === 'best') {
|
if (selectedDownloadFormat === 'best') {
|
||||||
return videoMetadata?.requested_downloads[0];
|
return videoMetadata?.requested_downloads[0];
|
||||||
}
|
}
|
||||||
return allFilteredFormats.find(
|
return allFilteredFormats.find(
|
||||||
(format) => format.format_id === selctedDownloadFormat
|
(format) => format.format_id === selectedDownloadFormat
|
||||||
);
|
);
|
||||||
} else if (videoMetadata?._type === 'playlist') {
|
} else if (videoMetadata?._type === 'playlist') {
|
||||||
if (selctedDownloadFormat === 'best') {
|
if (selectedDownloadFormat === 'best') {
|
||||||
return videoMetadata?.entries[Number(selectedPlaylistVideoIndex) - 1].requested_downloads[0];
|
return videoMetadata?.entries[Number(selectedPlaylistVideoIndex) - 1].requested_downloads[0];
|
||||||
}
|
}
|
||||||
return allFilteredFormats.find(
|
return allFilteredFormats.find(
|
||||||
(format) => format.format_id === selctedDownloadFormat
|
(format) => format.format_id === selectedDownloadFormat
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
const selectedFormatFileType = determineFileType(selectedFormat?.vcodec, selectedFormat?.acodec);
|
const selectedFormatFileType = determineFileType(selectedFormat?.vcodec, selectedFormat?.acodec);
|
||||||
|
const selectedVideoFormat = (() => {
|
||||||
|
if (videoMetadata?._type === 'video') {
|
||||||
|
return allFilteredFormats.find(
|
||||||
|
(format) => format.format_id === selectedCombinableVideoFormat
|
||||||
|
);
|
||||||
|
} else if (videoMetadata?._type === 'playlist') {
|
||||||
|
return allFilteredFormats.find(
|
||||||
|
(format) => format.format_id === selectedCombinableVideoFormat
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
const selectedAudioFormat = (() => {
|
||||||
|
if (videoMetadata?._type === 'video') {
|
||||||
|
return allFilteredFormats.find(
|
||||||
|
(format) => format.format_id === selectedCombinableAudioFormat
|
||||||
|
);
|
||||||
|
} else if (videoMetadata?._type === 'playlist') {
|
||||||
|
return allFilteredFormats.find(
|
||||||
|
(format) => format.format_id === selectedCombinableAudioFormat
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
const subtitles = videoMetadata?._type === 'video' ? (videoMetadata?.subtitles || {}) : videoMetadata?._type === 'playlist' ? (videoMetadata?.entries[Number(selectedPlaylistVideoIndex) - 1].subtitles || {}) : {};
|
const subtitles = videoMetadata?._type === 'video' ? (videoMetadata?.subtitles || {}) : videoMetadata?._type === 'playlist' ? (videoMetadata?.entries[Number(selectedPlaylistVideoIndex) - 1].subtitles || {}) : {};
|
||||||
const subtitleLanguages = Object.keys(subtitles).map(langCode => ({
|
const subtitleLanguages = Object.keys(subtitles).map(langCode => ({
|
||||||
@@ -105,6 +145,62 @@ export default function DownloaderPage() {
|
|||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const bottomBarRef = useRef<HTMLDivElement>(null);
|
const bottomBarRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
let selectedFormatExtensionMsg = 'Auto - unknown';
|
||||||
|
if (activeDownloadModeTab === 'combine') {
|
||||||
|
if (videoFormat !== 'auto') {
|
||||||
|
selectedFormatExtensionMsg = `Combined - ${videoFormat.toUpperCase()}`;
|
||||||
|
}
|
||||||
|
else if (selectedAudioFormat?.ext && selectedVideoFormat?.ext) {
|
||||||
|
selectedFormatExtensionMsg = `Combined - ${selectedVideoFormat.ext.toUpperCase()} + ${selectedAudioFormat.ext.toUpperCase()}`;
|
||||||
|
} else {
|
||||||
|
selectedFormatExtensionMsg = `Combined - unknown`;
|
||||||
|
}
|
||||||
|
} else if (selectedFormat?.ext) {
|
||||||
|
if ((selectedFormatFileType === 'video+audio' || selectedFormatFileType === 'video') && videoFormat !== 'auto') {
|
||||||
|
selectedFormatExtensionMsg = `Forced - ${videoFormat.toUpperCase()}`;
|
||||||
|
} else if (selectedFormatFileType === 'audio' && audioFormat !== 'auto') {
|
||||||
|
selectedFormatExtensionMsg = `Forced - ${audioFormat.toUpperCase()}`;
|
||||||
|
} else {
|
||||||
|
selectedFormatExtensionMsg = `Auto - ${selectedFormat.ext.toUpperCase()}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let selectedFormatResolutionMsg = 'unknown';
|
||||||
|
if (activeDownloadModeTab === 'combine') {
|
||||||
|
selectedFormatResolutionMsg = `${selectedVideoFormat?.resolution ?? 'unknown'} + ${selectedAudioFormat?.tbr ? formatBitrate(selectedAudioFormat.tbr) : 'unknown'}`;
|
||||||
|
} else if (selectedFormat?.resolution) {
|
||||||
|
selectedFormatResolutionMsg = selectedFormat.resolution;
|
||||||
|
}
|
||||||
|
|
||||||
|
let selectedFormatDynamicRangeMsg = '';
|
||||||
|
if (activeDownloadModeTab === 'combine') {
|
||||||
|
selectedFormatDynamicRangeMsg = selectedVideoFormat?.dynamic_range && selectedVideoFormat.dynamic_range !== 'SDR' ? selectedVideoFormat.dynamic_range : '';
|
||||||
|
} else if (selectedFormat?.dynamic_range && selectedFormat.dynamic_range !== 'SDR') {
|
||||||
|
selectedFormatDynamicRangeMsg = selectedFormat.dynamic_range;
|
||||||
|
}
|
||||||
|
|
||||||
|
let selectedFormatFileSizeMsg = 'unknown filesize';
|
||||||
|
if (activeDownloadModeTab === 'combine') {
|
||||||
|
selectedFormatFileSizeMsg = selectedVideoFormat?.filesize_approx && selectedAudioFormat?.filesize_approx ? formatFileSize(selectedVideoFormat.filesize_approx + selectedAudioFormat.filesize_approx) : 'unknown filesize';
|
||||||
|
} else if (selectedFormat?.filesize_approx) {
|
||||||
|
selectedFormatFileSizeMsg = formatFileSize(selectedFormat.filesize_approx);
|
||||||
|
}
|
||||||
|
|
||||||
|
let selectedFormatFinalMsg = '';
|
||||||
|
if (activeDownloadModeTab === 'combine') {
|
||||||
|
if (selectedCombinableVideoFormat && selectedCombinableAudioFormat) {
|
||||||
|
selectedFormatFinalMsg = `${selectedFormatExtensionMsg} (${selectedFormatResolutionMsg}) ${selectedFormatDynamicRangeMsg} ${selectedSubtitles.length > 0 ? `• ESUB` : ''} • ${selectedFormatFileSizeMsg}`;
|
||||||
|
} else {
|
||||||
|
selectedFormatFinalMsg = `Choose a video and audio stream to combine`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (selectedFormat) {
|
||||||
|
selectedFormatFinalMsg = `${selectedFormatExtensionMsg} (${selectedFormatResolutionMsg}) ${selectedFormatDynamicRangeMsg} ${selectedSubtitles.length > 0 ? `• ESUB` : ''} • ${selectedFormatFileSizeMsg}`;
|
||||||
|
} else {
|
||||||
|
selectedFormatFinalMsg = `Choose a stream to download`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const searchForm = useForm<z.infer<typeof searchFormSchema>>({
|
const searchForm = useForm<z.infer<typeof searchFormSchema>>({
|
||||||
resolver: zodResolver(searchFormSchema),
|
resolver: zodResolver(searchFormSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -113,27 +209,46 @@ export default function DownloaderPage() {
|
|||||||
mode: "onChange",
|
mode: "onChange",
|
||||||
})
|
})
|
||||||
const watchedUrl = searchForm.watch("url");
|
const watchedUrl = searchForm.watch("url");
|
||||||
|
const { errors: searchFormErrors } = searchForm.formState;
|
||||||
|
|
||||||
function handleSearchSubmit(values: z.infer<typeof searchFormSchema>) {
|
function handleSearchSubmit(values: z.infer<typeof searchFormSchema>) {
|
||||||
setVideoMetadata(null);
|
setVideoMetadata(null);
|
||||||
|
setSearchPid(null);
|
||||||
|
setShowSearchError(true);
|
||||||
setIsMetadataLoading(true);
|
setIsMetadataLoading(true);
|
||||||
setSelctedDownloadFormat('best');
|
setSelectedDownloadFormat('best');
|
||||||
|
setSelectedCombinableVideoFormat('');
|
||||||
|
setSelectedCombinableAudioFormat('');
|
||||||
setSelectedSubtitles([]);
|
setSelectedSubtitles([]);
|
||||||
setSelectedPlaylistVideoIndex('1');
|
setSelectedPlaylistVideoIndex('1');
|
||||||
|
|
||||||
fetchVideoMetadata(values.url).then((metadata) => {
|
fetchVideoMetadata(values.url).then((metadata) => {
|
||||||
if (!metadata || (metadata._type !== 'video' && metadata._type !== 'playlist') || (metadata && metadata._type === 'video' && metadata.formats.length <= 0) || (metadata && metadata._type === 'playlist' && metadata.entries.length <= 0)) {
|
if (!metadata || (metadata._type !== 'video' && metadata._type !== 'playlist') || (metadata && metadata._type === 'video' && metadata.formats.length <= 0) || (metadata && metadata._type === 'playlist' && metadata.entries.length <= 0)) {
|
||||||
|
const showSearchError = useCurrentVideoMetadataStore.getState().showSearchError;
|
||||||
|
if (showSearchError) {
|
||||||
toast({
|
toast({
|
||||||
title: 'Opps! No results found',
|
title: 'Oops! No results found',
|
||||||
description: 'The provided URL does not contain any downloadable content. Please check the URL and try again.',
|
description: 'The provided URL does not contain any downloadable content or you are not connected to the internet. Please check the URL, your network connection and try again.',
|
||||||
variant: "destructive"
|
variant: "destructive"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (metadata && (metadata._type === 'video' || metadata._type === 'playlist') && ((metadata._type === 'video' && metadata.formats.length > 0) || (metadata._type === 'playlist' && metadata.entries.length > 0))) setVideoMetadata(metadata);
|
if (metadata && (metadata._type === 'video' || metadata._type === 'playlist') && ((metadata._type === 'video' && metadata.formats.length > 0) || (metadata._type === 'playlist' && metadata.entries.length > 0))) setVideoMetadata(metadata);
|
||||||
if (metadata) console.log(metadata);
|
if (metadata) console.log(metadata);
|
||||||
setIsMetadataLoading(false);
|
setIsMetadataLoading(false);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const cancelSearch = async (pid: number | null) => {
|
||||||
|
setShowSearchError(false);
|
||||||
|
if (pid) {
|
||||||
|
console.log("Killing process with PID:", pid);
|
||||||
|
await invoke('kill_all_process', { pid: pid });
|
||||||
|
}
|
||||||
|
setVideoMetadata(null);
|
||||||
|
setIsMetadataLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const updateBottomBarWidth = (): void => {
|
const updateBottomBarWidth = (): void => {
|
||||||
if (containerRef.current && bottomBarRef.current) {
|
if (containerRef.current && bottomBarRef.current) {
|
||||||
@@ -223,9 +338,9 @@ export default function DownloaderPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto p-4 space-y-4 relative" ref={containerRef}>
|
<div className="container mx-auto p-4 space-y-4 relative" ref={containerRef}>
|
||||||
<Card>
|
<Card className="gap-4">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>{config.appName} Search</CardTitle>
|
<CardTitle className="flex items-center"><PackageSearch className="size-5 mr-3" />{config.appName} Search</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
<Form {...searchForm}>
|
<Form {...searchForm}>
|
||||||
@@ -239,7 +354,7 @@ export default function DownloaderPage() {
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
className="focus-visible:ring-0"
|
className="focus-visible:ring-0"
|
||||||
placeholder="Enter URL to search..."
|
placeholder="Enter Video URL to Search"
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
@@ -247,9 +362,20 @@ export default function DownloaderPage() {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
{isMetadataLoading && (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="destructive"
|
||||||
|
size="icon"
|
||||||
|
disabled={!isMetadataLoading}
|
||||||
|
onClick={() => cancelSearch(searchPid)}
|
||||||
|
>
|
||||||
|
<X className="size-4" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={!videoUrl || isMetadataLoading}
|
disabled={!videoUrl || Object.keys(searchFormErrors).length > 0 || isMetadataLoading}
|
||||||
>
|
>
|
||||||
{isMetadataLoading ? (
|
{isMetadataLoading ? (
|
||||||
<>
|
<>
|
||||||
@@ -267,11 +393,11 @@ export default function DownloaderPage() {
|
|||||||
{!isMetadataLoading && videoMetadata && videoMetadata._type === 'video' && ( // === Single Video ===
|
{!isMetadataLoading && videoMetadata && videoMetadata._type === 'video' && ( // === Single Video ===
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<div className="flex flex-col w-[55%] border-r border-border pr-4">
|
<div className="flex flex-col w-[55%] border-r border-border pr-4">
|
||||||
<h3 className="text-sm mb-4 flex items-center gap-2">
|
<h3 className="text-sm mb-4 mt-2 flex items-center gap-2">
|
||||||
<Info className="w-4 h-4" />
|
<Info className="w-4 h-4" />
|
||||||
<span>Metadata</span>
|
<span>Metadata</span>
|
||||||
</h3>
|
</h3>
|
||||||
<div className="flex flex-col overflow-y-scroll max-h-[53vh] no-scrollbar">
|
<div className="flex flex-col overflow-y-scroll max-h-[50vh] no-scrollbar">
|
||||||
<AspectRatio ratio={16 / 9} className={clsx("w-full rounded-lg overflow-hidden mb-2 border border-border", videoMetadata.aspect_ratio && videoMetadata.aspect_ratio === 0.56 && "relative")}>
|
<AspectRatio ratio={16 / 9} className={clsx("w-full rounded-lg overflow-hidden mb-2 border border-border", videoMetadata.aspect_ratio && videoMetadata.aspect_ratio === 0.56 && "relative")}>
|
||||||
<ProxyImage src={videoMetadata.thumbnail} alt="thumbnail" className={clsx(videoMetadata.aspect_ratio && videoMetadata.aspect_ratio === 0.56 && "absolute h-full w-auto top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2")} />
|
<ProxyImage src={videoMetadata.thumbnail} alt="thumbnail" className={clsx(videoMetadata.aspect_ratio && videoMetadata.aspect_ratio === 0.56 && "absolute h-full w-auto top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2")} />
|
||||||
</AspectRatio>
|
</AspectRatio>
|
||||||
@@ -309,15 +435,27 @@ export default function DownloaderPage() {
|
|||||||
<Info className="w-3 h-3 mr-2" />
|
<Info className="w-3 h-3 mr-2" />
|
||||||
<span className="text-xs">Extracted from {videoMetadata.extractor ? videoMetadata.extractor.charAt(0).toUpperCase() + videoMetadata.extractor.slice(1) : 'Unknown'}</span>
|
<span className="text-xs">Extracted from {videoMetadata.extractor ? videoMetadata.extractor.charAt(0).toUpperCase() + videoMetadata.extractor.slice(1) : 'Unknown'}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="spacer mb-14"></div>
|
<div className="spacer mb-10"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col w-full pl-4">
|
<div className="flex flex-col w-full pl-4">
|
||||||
<h3 className="text-sm mb-4 flex items-center gap-2">
|
<Tabs
|
||||||
|
className=""
|
||||||
|
value={activeDownloadModeTab}
|
||||||
|
onValueChange={(tab) => setActiveDownloadModeTab(tab)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h3 className="text-sm flex items-center gap-2">
|
||||||
<DownloadCloud className="w-4 h-4" />
|
<DownloadCloud className="w-4 h-4" />
|
||||||
<span>Download Options</span>
|
<span>Download Options</span>
|
||||||
</h3>
|
</h3>
|
||||||
<div className="flex flex-col overflow-y-scroll max-h-[53vh] no-scrollbar">
|
<TabsList>
|
||||||
|
<TabsTrigger value="selective">Selective</TabsTrigger>
|
||||||
|
<TabsTrigger value="combine">Combine</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
</div>
|
||||||
|
<TabsContent value="selective">
|
||||||
|
<div className="flex flex-col overflow-y-scroll max-h-[50vh] no-scrollbar">
|
||||||
{subtitles && !isObjEmpty(subtitles) && (
|
{subtitles && !isObjEmpty(subtitles) && (
|
||||||
<ToggleGroup
|
<ToggleGroup
|
||||||
type="multiple"
|
type="multiple"
|
||||||
@@ -325,7 +463,7 @@ export default function DownloaderPage() {
|
|||||||
className="flex flex-col items-start gap-2 mb-2"
|
className="flex flex-col items-start gap-2 mb-2"
|
||||||
value={selectedSubtitles}
|
value={selectedSubtitles}
|
||||||
onValueChange={(value) => setSelectedSubtitles(value)}
|
onValueChange={(value) => setSelectedSubtitles(value)}
|
||||||
disabled={selectedFormat?.ext !== 'mp4' && selectedFormat?.ext !== 'mkv' && selectedFormat?.ext !== 'webm'}
|
// disabled={selectedFormat?.ext !== 'mp4' && selectedFormat?.ext !== 'mkv' && selectedFormat?.ext !== 'webm'}
|
||||||
>
|
>
|
||||||
<p className="text-xs">Subtitle Languages</p>
|
<p className="text-xs">Subtitle Languages</p>
|
||||||
<div className="flex gap-2 flex-wrap items-center">
|
<div className="flex gap-2 flex-wrap items-center">
|
||||||
@@ -343,16 +481,16 @@ export default function DownloaderPage() {
|
|||||||
</ToggleGroup>
|
</ToggleGroup>
|
||||||
)}
|
)}
|
||||||
<FormatSelectionGroup
|
<FormatSelectionGroup
|
||||||
value={selctedDownloadFormat}
|
value={selectedDownloadFormat}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
setSelctedDownloadFormat(value);
|
setSelectedDownloadFormat(value);
|
||||||
const currentlySelectedFormat = value === 'best' ? videoMetadata?.requested_downloads[0] : allFilteredFormats.find((format) => format.format_id === value);
|
// const currentlySelectedFormat = value === 'best' ? videoMetadata?.requested_downloads[0] : allFilteredFormats.find((format) => format.format_id === value);
|
||||||
if (currentlySelectedFormat?.ext !== 'mp4' && currentlySelectedFormat?.ext !== 'mkv' && currentlySelectedFormat?.ext !== 'webm') {
|
// if (currentlySelectedFormat?.ext !== 'mp4' && currentlySelectedFormat?.ext !== 'mkv' && currentlySelectedFormat?.ext !== 'webm') {
|
||||||
setSelectedSubtitles([]);
|
// setSelectedSubtitles([]);
|
||||||
}
|
// }
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<p className="text-xs">Suggested (Best)</p>
|
<p className="text-xs">Suggested</p>
|
||||||
<div className="">
|
<div className="">
|
||||||
<FormatSelectionGroupItem
|
<FormatSelectionGroupItem
|
||||||
key="best"
|
key="best"
|
||||||
@@ -417,19 +555,101 @@ export default function DownloaderPage() {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</FormatSelectionGroup>
|
</FormatSelectionGroup>
|
||||||
<div className="spacer mb-14"></div>
|
<div className="spacer mb-10"></div>
|
||||||
</div>
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="combine">
|
||||||
|
<div className="flex flex-col overflow-y-scroll max-h-[50vh] no-scrollbar">
|
||||||
|
{audioOnlyFormats && audioOnlyFormats.length > 0 && videoOnlyFormats && videoOnlyFormats.length > 0 && subtitles && !isObjEmpty(subtitles) && (
|
||||||
|
<ToggleGroup
|
||||||
|
type="multiple"
|
||||||
|
variant="outline"
|
||||||
|
className="flex flex-col items-start gap-2 mb-2"
|
||||||
|
value={selectedSubtitles}
|
||||||
|
onValueChange={(value) => setSelectedSubtitles(value)}
|
||||||
|
>
|
||||||
|
<p className="text-xs">Subtitle Languages</p>
|
||||||
|
<div className="flex gap-2 flex-wrap items-center">
|
||||||
|
{subtitleLanguages.map((lang) => (
|
||||||
|
<ToggleGroupItem
|
||||||
|
className="text-xs text-nowrap border-2 data-[state=on]:border-2 data-[state=on]:border-primary data-[state=on]:bg-muted/70 hover:bg-muted/70"
|
||||||
|
value={lang.code}
|
||||||
|
size="sm"
|
||||||
|
aria-label={lang.lang}
|
||||||
|
key={lang.code}>
|
||||||
|
{lang.lang}
|
||||||
|
</ToggleGroupItem>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</ToggleGroup>
|
||||||
|
)}
|
||||||
|
<FormatSelectionGroup
|
||||||
|
className="mb-2"
|
||||||
|
value={selectedCombinableAudioFormat}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
setSelectedCombinableAudioFormat(value);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{videoOnlyFormats && videoOnlyFormats.length > 0 && audioOnlyFormats && audioOnlyFormats.length > 0 && (
|
||||||
|
<>
|
||||||
|
<p className="text-xs">Audio</p>
|
||||||
|
<div className="grid grid-cols-2 xl:grid-cols-3 gap-2">
|
||||||
|
{audioOnlyFormats.map((format) => (
|
||||||
|
<FormatSelectionGroupItem
|
||||||
|
key={format.format_id}
|
||||||
|
value={format.format_id}
|
||||||
|
format={format}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</FormatSelectionGroup>
|
||||||
|
<FormatSelectionGroup
|
||||||
|
value={selectedCombinableVideoFormat}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
setSelectedCombinableVideoFormat(value);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{audioOnlyFormats && audioOnlyFormats.length > 0 && videoOnlyFormats && videoOnlyFormats.length > 0 && (
|
||||||
|
<>
|
||||||
|
<p className="text-xs">Video</p>
|
||||||
|
<div className="grid grid-cols-2 xl:grid-cols-3 gap-2">
|
||||||
|
{videoOnlyFormats.map((format) => (
|
||||||
|
<FormatSelectionGroupItem
|
||||||
|
key={format.format_id}
|
||||||
|
value={format.format_id}
|
||||||
|
format={format}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</FormatSelectionGroup>
|
||||||
|
{(!videoOnlyFormats || videoOnlyFormats.length === 0 || !audioOnlyFormats || audioOnlyFormats.length === 0) && (
|
||||||
|
<Alert>
|
||||||
|
<AlertCircleIcon />
|
||||||
|
<AlertTitle>Unable to use Combine Mode!</AlertTitle>
|
||||||
|
<AlertDescription>
|
||||||
|
Cannot use combine mode for this video as it does not have both audio and video formats available. Use Selective Mode or try another video.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
<div className="spacer mb-10"></div>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!isMetadataLoading && videoMetadata && videoMetadata._type === 'playlist' && ( // === Playlists ===
|
{!isMetadataLoading && videoMetadata && videoMetadata._type === 'playlist' && ( // === Playlists ===
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<div className="flex flex-col w-[55%] border-r border-border pr-4">
|
<div className="flex flex-col w-[55%] border-r border-border pr-4">
|
||||||
<h3 className="text-sm mb-4 flex items-center gap-2">
|
<h3 className="text-sm mb-4 mt-2 flex items-center gap-2">
|
||||||
<ListVideo className="w-4 h-4" />
|
<ListVideo className="w-4 h-4" />
|
||||||
<span>Playlist ({videoMetadata.entries[0].n_entries})</span>
|
<span>Playlist ({videoMetadata.entries[0].n_entries})</span>
|
||||||
</h3>
|
</h3>
|
||||||
<div className="flex flex-col overflow-y-scroll max-h-[53vh] no-scrollbar">
|
<div className="flex flex-col overflow-y-scroll max-h-[50vh] no-scrollbar">
|
||||||
<h2 className="mb-1">{videoMetadata.entries[0].playlist_title ? videoMetadata.entries[0].playlist_title : 'UNTITLED'}</h2>
|
<h2 className="mb-1">{videoMetadata.entries[0].playlist_title ? videoMetadata.entries[0].playlist_title : 'UNTITLED'}</h2>
|
||||||
<p className="text-muted-foreground text-xs mb-4">{videoMetadata.entries[0].playlist_channel || videoMetadata.entries[0].playlist_uploader || 'unknown'}</p>
|
<p className="text-muted-foreground text-xs mb-4">{videoMetadata.entries[0].playlist_channel || videoMetadata.entries[0].playlist_uploader || 'unknown'}</p>
|
||||||
{/* <PlaylistToggleGroup
|
{/* <PlaylistToggleGroup
|
||||||
@@ -451,8 +671,10 @@ export default function DownloaderPage() {
|
|||||||
value={selectedPlaylistVideoIndex}
|
value={selectedPlaylistVideoIndex}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
setSelectedPlaylistVideoIndex(value);
|
setSelectedPlaylistVideoIndex(value);
|
||||||
setSelctedDownloadFormat('best');
|
setSelectedDownloadFormat('best');
|
||||||
setSelectedSubtitles([]);
|
setSelectedSubtitles([]);
|
||||||
|
setSelectedCombinableVideoFormat('');
|
||||||
|
setSelectedCombinableAudioFormat('');
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{videoMetadata.entries.map((entry) => entry ? (
|
{videoMetadata.entries.map((entry) => entry ? (
|
||||||
@@ -467,15 +689,27 @@ export default function DownloaderPage() {
|
|||||||
<Info className="w-3 h-3 mr-2" />
|
<Info className="w-3 h-3 mr-2" />
|
||||||
<span className="text-xs">Extracted from {videoMetadata.entries[0].extractor ? videoMetadata.entries[0].extractor.charAt(0).toUpperCase() + videoMetadata.entries[0].extractor.slice(1) : 'Unknown'}</span>
|
<span className="text-xs">Extracted from {videoMetadata.entries[0].extractor ? videoMetadata.entries[0].extractor.charAt(0).toUpperCase() + videoMetadata.entries[0].extractor.slice(1) : 'Unknown'}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="spacer mb-14"></div>
|
<div className="spacer mb-10"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col w-full pl-4">
|
<div className="flex flex-col w-full pl-4">
|
||||||
<h3 className="text-sm mb-4 flex items-center gap-2">
|
<Tabs
|
||||||
|
className=""
|
||||||
|
value={activeDownloadModeTab}
|
||||||
|
onValueChange={(tab) => setActiveDownloadModeTab(tab)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h3 className="text-sm flex items-center gap-2">
|
||||||
<DownloadCloud className="w-4 h-4" />
|
<DownloadCloud className="w-4 h-4" />
|
||||||
<span>Download Options</span>
|
<span>Download Options</span>
|
||||||
</h3>
|
</h3>
|
||||||
<div className="flex flex-col overflow-y-scroll max-h-[53vh] no-scrollbar">
|
<TabsList>
|
||||||
|
<TabsTrigger value="selective">Selective</TabsTrigger>
|
||||||
|
<TabsTrigger value="combine">Combine</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
</div>
|
||||||
|
<TabsContent value="selective">
|
||||||
|
<div className="flex flex-col overflow-y-scroll max-h-[50vh] no-scrollbar">
|
||||||
{subtitles && !isObjEmpty(subtitles) && (
|
{subtitles && !isObjEmpty(subtitles) && (
|
||||||
<ToggleGroup
|
<ToggleGroup
|
||||||
type="multiple"
|
type="multiple"
|
||||||
@@ -501,16 +735,16 @@ export default function DownloaderPage() {
|
|||||||
</ToggleGroup>
|
</ToggleGroup>
|
||||||
)}
|
)}
|
||||||
<FormatSelectionGroup
|
<FormatSelectionGroup
|
||||||
value={selctedDownloadFormat}
|
value={selectedDownloadFormat}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
setSelctedDownloadFormat(value);
|
setSelectedDownloadFormat(value);
|
||||||
const currentlySelectedFormat = value === 'best' ? videoMetadata?.entries[Number(value) - 1].requested_downloads[0] : allFilteredFormats.find((format) => format.format_id === value);
|
const currentlySelectedFormat = value === 'best' ? videoMetadata?.entries[Number(value) - 1].requested_downloads[0] : allFilteredFormats.find((format) => format.format_id === value);
|
||||||
if (currentlySelectedFormat?.ext !== 'mp4' && currentlySelectedFormat?.ext !== 'mkv' && currentlySelectedFormat?.ext !== 'webm') {
|
if (currentlySelectedFormat?.ext !== 'mp4' && currentlySelectedFormat?.ext !== 'mkv' && currentlySelectedFormat?.ext !== 'webm') {
|
||||||
setSelectedSubtitles([]);
|
setSelectedSubtitles([]);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<p className="text-xs">Suggested (Best)</p>
|
<p className="text-xs">Suggested</p>
|
||||||
<div className="">
|
<div className="">
|
||||||
<FormatSelectionGroupItem
|
<FormatSelectionGroupItem
|
||||||
key="best"
|
key="best"
|
||||||
@@ -575,12 +809,95 @@ export default function DownloaderPage() {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</FormatSelectionGroup>
|
</FormatSelectionGroup>
|
||||||
<div className="spacer mb-14"></div>
|
<div className="spacer mb-10"></div>
|
||||||
</div>
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="combine">
|
||||||
|
<div className="flex flex-col overflow-y-scroll max-h-[50vh] no-scrollbar">
|
||||||
|
{audioOnlyFormats && audioOnlyFormats.length > 0 && videoOnlyFormats && videoOnlyFormats.length > 0 && subtitles && !isObjEmpty(subtitles) && (
|
||||||
|
<ToggleGroup
|
||||||
|
type="multiple"
|
||||||
|
variant="outline"
|
||||||
|
className="flex flex-col items-start gap-2 mb-2"
|
||||||
|
value={selectedSubtitles}
|
||||||
|
onValueChange={(value) => setSelectedSubtitles(value)}
|
||||||
|
disabled={selectedFormat?.ext !== 'mp4' && selectedFormat?.ext !== 'mkv' && selectedFormat?.ext !== 'webm'}
|
||||||
|
>
|
||||||
|
<p className="text-xs">Subtitle Languages</p>
|
||||||
|
<div className="flex gap-2 flex-wrap items-center">
|
||||||
|
{subtitleLanguages.map((lang) => (
|
||||||
|
<ToggleGroupItem
|
||||||
|
className="text-xs text-nowrap border-2 data-[state=on]:border-2 data-[state=on]:border-primary data-[state=on]:bg-muted/70 hover:bg-muted/70"
|
||||||
|
value={lang.code}
|
||||||
|
size="sm"
|
||||||
|
aria-label={lang.lang}
|
||||||
|
key={lang.code}>
|
||||||
|
{lang.lang}
|
||||||
|
</ToggleGroupItem>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</ToggleGroup>
|
||||||
|
)}
|
||||||
|
<FormatSelectionGroup
|
||||||
|
className="mb-2"
|
||||||
|
value={selectedCombinableAudioFormat}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
setSelectedCombinableAudioFormat(value);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{videoOnlyFormats && videoOnlyFormats.length > 0 && audioOnlyFormats && audioOnlyFormats.length > 0 && (
|
||||||
|
<>
|
||||||
|
<p className="text-xs">Audio</p>
|
||||||
|
<div className="grid grid-cols-2 xl:grid-cols-3 gap-2">
|
||||||
|
{audioOnlyFormats.map((format) => (
|
||||||
|
<FormatSelectionGroupItem
|
||||||
|
key={format.format_id}
|
||||||
|
value={format.format_id}
|
||||||
|
format={format}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</FormatSelectionGroup>
|
||||||
|
<FormatSelectionGroup
|
||||||
|
value={selectedCombinableVideoFormat}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
setSelectedCombinableVideoFormat(value);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{audioOnlyFormats && audioOnlyFormats.length > 0 && videoOnlyFormats && videoOnlyFormats.length > 0 && (
|
||||||
|
<>
|
||||||
|
<p className="text-xs">Video</p>
|
||||||
|
<div className="grid grid-cols-2 xl:grid-cols-3 gap-2">
|
||||||
|
{videoOnlyFormats.map((format) => (
|
||||||
|
<FormatSelectionGroupItem
|
||||||
|
key={format.format_id}
|
||||||
|
value={format.format_id}
|
||||||
|
format={format}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</FormatSelectionGroup>
|
||||||
|
{(!videoOnlyFormats || videoOnlyFormats.length === 0 || !audioOnlyFormats || audioOnlyFormats.length === 0) && (
|
||||||
|
<Alert>
|
||||||
|
<AlertCircleIcon />
|
||||||
|
<AlertTitle>Unable to use Combine Mode!</AlertTitle>
|
||||||
|
<AlertDescription>
|
||||||
|
Cannot use combine mode for this video as it does not have both audio and video formats available. Use Selective Mode or try another video.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
<div className="spacer mb-10"></div>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!isMetadataLoading && videoMetadata && selctedDownloadFormat && ( // === Bottom Bar ===
|
{!isMetadataLoading && videoMetadata && selectedDownloadFormat && ( // === Bottom Bar ===
|
||||||
<div className="flex justify-between items-center gap-2 fixed bottom-0 right-0 p-4 w-full bg-background rounded-t-lg border-t border-border z-20" ref={bottomBarRef}>
|
<div className="flex justify-between items-center gap-2 fixed bottom-0 right-0 p-4 w-full bg-background rounded-t-lg border-t border-border z-20" ref={bottomBarRef}>
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<div className="flex justify-center items-center p-3 rounded-md border border-border">
|
<div className="flex justify-center items-center p-3 rounded-md border border-border">
|
||||||
@@ -596,7 +913,7 @@ export default function DownloaderPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<span className="text-sm text-nowrap max-w-[30rem] xl:max-w-[50rem] overflow-hidden text-ellipsis">{videoMetadata._type === 'video' ? videoMetadata.title : videoMetadata._type === 'playlist' ? videoMetadata.entries[Number(selectedPlaylistVideoIndex) - 1].title : 'Unknown' }</span>
|
<span className="text-sm text-nowrap max-w-[30rem] xl:max-w-[50rem] overflow-hidden text-ellipsis">{videoMetadata._type === 'video' ? videoMetadata.title : videoMetadata._type === 'playlist' ? videoMetadata.entries[Number(selectedPlaylistVideoIndex) - 1].title : 'Unknown' }</span>
|
||||||
<span className="text-xs text-muted-foreground">{selectedFormat?.ext ? selectedFormat.ext.toUpperCase() : 'unknown'} ({selectedFormat?.resolution ? selectedFormat.resolution : 'unknown'}) {selectedFormat?.dynamic_range && selectedFormat.dynamic_range !== 'SDR' ? selectedFormat.dynamic_range : null } {selectedSubtitles.length > 0 ? `• ESUB` : null} • {selectedFormat?.filesize_approx ? formatFileSize(selectedFormat?.filesize_approx) : 'unknown filesize'}</span>
|
<span className="text-xs text-muted-foreground">{selectedFormatFinalMsg}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
@@ -606,7 +923,7 @@ export default function DownloaderPage() {
|
|||||||
if (videoMetadata._type === 'playlist') {
|
if (videoMetadata._type === 'playlist') {
|
||||||
await startDownload(
|
await startDownload(
|
||||||
videoMetadata.original_url,
|
videoMetadata.original_url,
|
||||||
selctedDownloadFormat === 'best' ? videoMetadata.entries[Number(selectedPlaylistVideoIndex) - 1].requested_downloads[0].format_id : selctedDownloadFormat,
|
activeDownloadModeTab === 'combine' ? `${selectedCombinableVideoFormat}+${selectedCombinableAudioFormat}` : selectedDownloadFormat === 'best' ? videoMetadata.entries[Number(selectedPlaylistVideoIndex) - 1].requested_downloads[0].format_id : selectedDownloadFormat,
|
||||||
selectedSubtitles.length > 0 ? selectedSubtitles.join(',') : null,
|
selectedSubtitles.length > 0 ? selectedSubtitles.join(',') : null,
|
||||||
undefined,
|
undefined,
|
||||||
selectedPlaylistVideoIndex
|
selectedPlaylistVideoIndex
|
||||||
@@ -614,7 +931,7 @@ export default function DownloaderPage() {
|
|||||||
} else if (videoMetadata._type === 'video') {
|
} else if (videoMetadata._type === 'video') {
|
||||||
await startDownload(
|
await startDownload(
|
||||||
videoMetadata.webpage_url,
|
videoMetadata.webpage_url,
|
||||||
selctedDownloadFormat === 'best' ? videoMetadata.requested_downloads[0].format_id : selctedDownloadFormat,
|
activeDownloadModeTab === 'combine' ? `${selectedCombinableVideoFormat}+${selectedCombinableAudioFormat}` : selectedDownloadFormat === 'best' ? videoMetadata.requested_downloads[0].format_id : selectedDownloadFormat,
|
||||||
selectedSubtitles.length > 0 ? selectedSubtitles.join(',') : null
|
selectedSubtitles.length > 0 ? selectedSubtitles.join(',') : null
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -633,12 +950,12 @@ export default function DownloaderPage() {
|
|||||||
setIsStartingDownload(false);
|
setIsStartingDownload(false);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
disabled={isStartingDownload}
|
disabled={isStartingDownload || !selectedDownloadFormat || (activeDownloadModeTab === 'combine' && (!selectedCombinableVideoFormat || !selectedCombinableAudioFormat))}
|
||||||
>
|
>
|
||||||
{isStartingDownload ? (
|
{isStartingDownload ? (
|
||||||
<>
|
<>
|
||||||
<Loader2 className="h-4 w-4 animate-spin" />
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
Starting
|
Starting Download
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
'Start Download'
|
'Start Download'
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ import { Progress } from "@/components/ui/progress";
|
|||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import { useAppContext } from "@/providers/appContextProvider";
|
import { useAppContext } from "@/providers/appContextProvider";
|
||||||
import { useDownloadActionStatesStore, useDownloadStatesStore } from "@/services/store";
|
import { useDownloadActionStatesStore, useDownloadStatesStore, useLibraryPageStatesStore } from "@/services/store";
|
||||||
import { formatBitrate, formatCodec, formatDurationString, formatFileSize, formatSecToTimeString, formatSpeed } from "@/utils";
|
import { formatBitrate, formatCodec, formatDurationString, formatFileSize, formatSecToTimeString, formatSpeed } from "@/utils";
|
||||||
import { AudioLines, CircleArrowDown, CircleCheck, Clock, File, FileAudio2, FileQuestion, FileVideo2, FolderInput, ListVideo, Loader2, Music, Pause, Play, Trash2, Video, X } from "lucide-react";
|
import { AudioLines, Clock, File, FileAudio2, FileQuestion, FileVideo2, FolderInput, ListVideo, Loader2, Music, Pause, Play, Square, Trash2, Video, X } from "lucide-react";
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
import * as fs from "@tauri-apps/plugin-fs";
|
import * as fs from "@tauri-apps/plugin-fs";
|
||||||
import { DownloadState } from "@/types/download";
|
import { DownloadState } from "@/types/download";
|
||||||
@@ -18,9 +18,14 @@ import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent,
|
|||||||
import { Checkbox } from "@/components/ui/checkbox";
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import Heading from "@/components/heading";
|
import Heading from "@/components/heading";
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
|
||||||
|
|
||||||
export default function LibraryPage() {
|
export default function LibraryPage() {
|
||||||
|
const activeTab = useLibraryPageStatesStore(state => state.activeTab);
|
||||||
|
const setActiveTab = useLibraryPageStatesStore(state => state.setActiveTab);
|
||||||
|
|
||||||
const downloadStates = useDownloadStatesStore(state => state.downloadStates);
|
const downloadStates = useDownloadStatesStore(state => state.downloadStates);
|
||||||
const downloadActions = useDownloadActionStatesStore(state => state.downloadActions);
|
const downloadActions = useDownloadActionStatesStore(state => state.downloadActions);
|
||||||
const setIsResumingDownload = useDownloadActionStatesStore(state => state.setIsResumingDownload);
|
const setIsResumingDownload = useDownloadActionStatesStore(state => state.setIsResumingDownload);
|
||||||
@@ -36,6 +41,9 @@ export default function LibraryPage() {
|
|||||||
|
|
||||||
const incompleteDownloads = downloadStates.filter(state => state.download_status !== 'completed');
|
const incompleteDownloads = downloadStates.filter(state => state.download_status !== 'completed');
|
||||||
const completedDownloads = downloadStates.filter(state => state.download_status === 'completed');
|
const completedDownloads = downloadStates.filter(state => state.download_status === 'completed');
|
||||||
|
const ongoingDownloads = downloadStates.filter(state =>
|
||||||
|
['starting', 'downloading', 'queued'].includes(state.download_status)
|
||||||
|
);
|
||||||
|
|
||||||
const openFile = async (filePath: string | null, app: string | null) => {
|
const openFile = async (filePath: string | null, app: string | null) => {
|
||||||
if (filePath && await fs.exists(filePath)) {
|
if (filePath && await fs.exists(filePath)) {
|
||||||
@@ -96,16 +104,216 @@ export default function LibraryPage() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const stopOngoingDownloads = async () => {
|
||||||
|
if (ongoingDownloads.length > 0) {
|
||||||
|
for (const state of ongoingDownloads) {
|
||||||
|
setIsPausingDownload(state.download_id, true);
|
||||||
|
try {
|
||||||
|
await pauseDownload(state);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
toast({
|
||||||
|
title: 'Failed to stop download',
|
||||||
|
description: `An error occurred while trying to stop the download for ${state.title}.`,
|
||||||
|
variant: "destructive"
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsPausingDownload(state.download_id, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ongoingDownloads.length === 0) {
|
||||||
|
toast({
|
||||||
|
title: 'Stopped ongoing downloads',
|
||||||
|
description: 'All ongoing downloads have been stopped successfully.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
toast({
|
||||||
|
title: 'No ongoing downloads',
|
||||||
|
description: 'There are no ongoing downloads to stop.',
|
||||||
|
variant: "destructive"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto p-4 space-y-4">
|
<div className="container mx-auto p-4 space-y-4">
|
||||||
<Heading title="Library" description="Manage all your downloads in one place" />
|
<Heading title="Library" description="Manage all your downloads in one place" />
|
||||||
<div className="w-full fle flex-col">
|
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||||
<div className="flex w-full items-center gap-2 mb-2">
|
<div className="w-full flex items-center justify-between mb-4">
|
||||||
<CircleArrowDown className="size-4" />
|
<TabsList>
|
||||||
<h3 className="text-nowrap font-semibold">Incomplete Downloads</h3>
|
<TabsTrigger value="completed">Completed {completedDownloads.length > 0 && (`(${completedDownloads.length})`)}</TabsTrigger>
|
||||||
|
<TabsTrigger value="incomplete">Incomplete {(incompleteDownloads.length > 0 && ongoingDownloads.length <= 0) && (`(${incompleteDownloads.length})`)} {ongoingDownloads.length > 0 && (<Badge className="h-4 min-w-4 rounded-full px-1 font-mono tabular-nums ml-1">{ongoingDownloads.length}</Badge>)}</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
<AlertDialog>
|
||||||
|
<AlertDialogTrigger asChild>
|
||||||
|
<Button
|
||||||
|
className="w-fit"
|
||||||
|
variant="destructive"
|
||||||
|
size="sm"
|
||||||
|
disabled={ongoingDownloads.length <= 0}
|
||||||
|
>
|
||||||
|
<Square className="h-4 w-4" />
|
||||||
|
Stop
|
||||||
|
</Button>
|
||||||
|
</AlertDialogTrigger>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>Stop all ongoing downloads?</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
Are you sure you want to stop all ongoing downloads? This will pause all downloads including the download queue.
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
|
<AlertDialogAction
|
||||||
|
onClick={() => stopOngoingDownloads()}
|
||||||
|
>Stop</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
</div>
|
</div>
|
||||||
<Separator orientation="horizontal" className="" />
|
<TabsContent value="completed">
|
||||||
|
<div className="w-full flex flex-col gap-2">
|
||||||
|
{completedDownloads.length > 0 ? (
|
||||||
|
completedDownloads.map((state) => {
|
||||||
|
const itemActionStates = downloadActions[state.download_id] || {
|
||||||
|
isResuming: false,
|
||||||
|
isPausing: false,
|
||||||
|
isCanceling: false,
|
||||||
|
isDeleteFileChecked: false,
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div className="p-4 border border-border rounded-lg flex gap-4" key={state.download_id}>
|
||||||
|
<div className="w-[30%] flex flex-col justify-between gap-2">
|
||||||
|
<AspectRatio ratio={16 / 9} className="w-full rounded-lg overflow-hidden border border-border mb-2">
|
||||||
|
<ProxyImage src={state.thumbnail || ""} alt="thumbnail" className="" />
|
||||||
|
</AspectRatio>
|
||||||
|
<span className="w-full flex items-center justify-center text-xs border border-border py-1 px-2 rounded">
|
||||||
|
{state.filetype && (state.filetype === 'video' || state.filetype === 'video+audio') && (
|
||||||
|
<Video className="w-4 h-4 mr-2" />
|
||||||
|
)}
|
||||||
|
{state.filetype && state.filetype === 'audio' && (
|
||||||
|
<Music className="w-4 h-4 mr-2" />
|
||||||
|
)}
|
||||||
|
{(!state.filetype) || (state.filetype && state.filetype !== 'video' && state.filetype !== 'audio' && state.filetype !== 'video+audio') && (
|
||||||
|
<File className="w-4 h-4 mr-2" />
|
||||||
|
)}
|
||||||
|
{state.ext?.toUpperCase()} {state.resolution ? `(${state.resolution})` : null}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="w-full flex flex-col justify-between gap-2">
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<h4 className="">{state.title}</h4>
|
||||||
|
<p className="text-xs text-muted-foreground">{state.channel ? state.channel : 'unknown'} {state.host ? `• ${state.host}` : 'unknown'}</p>
|
||||||
|
<div className="flex items-center mt-1">
|
||||||
|
<span className="text-xs text-muted-foreground flex items-center pr-3"><Clock className="w-4 h-4 mr-2"/> {state.duration_string ? formatDurationString(state.duration_string) : 'unknown'}</span>
|
||||||
|
<Separator orientation="vertical" />
|
||||||
|
<span className="text-xs text-muted-foreground flex items-center px-3">
|
||||||
|
{state.filetype && (state.filetype === 'video' || state.filetype === 'video+audio') && (
|
||||||
|
<FileVideo2 className="w-4 h-4 mr-2"/>
|
||||||
|
)}
|
||||||
|
{state.filetype && state.filetype === 'audio' && (
|
||||||
|
<FileAudio2 className="w-4 h-4 mr-2" />
|
||||||
|
)}
|
||||||
|
{(!state.filetype) || (state.filetype && state.filetype !== 'video' && state.filetype !== 'audio' && state.filetype !== 'video+audio') && (
|
||||||
|
<FileQuestion className="w-4 h-4 mr-2" />
|
||||||
|
)}
|
||||||
|
{state.filesize ? formatFileSize(state.filesize) : 'unknown'}
|
||||||
|
</span>
|
||||||
|
<Separator orientation="vertical" />
|
||||||
|
<span className="text-xs text-muted-foreground flex items-center pl-3"><AudioLines className="w-4 h-4 mr-2"/>
|
||||||
|
{state.vbr && state.abr ? (
|
||||||
|
formatBitrate(state.vbr + state.abr)
|
||||||
|
) : state.vbr ? (
|
||||||
|
formatBitrate(state.vbr)
|
||||||
|
) : state.abr ? (
|
||||||
|
formatBitrate(state.abr)
|
||||||
|
) : (
|
||||||
|
'unknown'
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="hidden xl:flex items-center mt-1 gap-2 flex-wrap text-xs">
|
||||||
|
{state.playlist_id && state.playlist_index && (
|
||||||
|
<span
|
||||||
|
className="border border-border py-1 px-2 rounded flex items-center cursor-pointer"
|
||||||
|
title={`${state.playlist_title ?? 'UNKNOWN PLAYLIST'}` + ' by ' + `${state.playlist_channel ?? 'UNKNOWN CHANNEL'}`}
|
||||||
|
>
|
||||||
|
<ListVideo className="w-4 h-4 mr-2" /> Playlist ({state.playlist_index} of {state.playlist_n_entries})
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{state.vcodec && (
|
||||||
|
<span className="border border-border py-1 px-2 rounded">{formatCodec(state.vcodec)}</span>
|
||||||
|
)}
|
||||||
|
{state.acodec && (
|
||||||
|
<span className="border border-border py-1 px-2 rounded">{formatCodec(state.acodec)}</span>
|
||||||
|
)}
|
||||||
|
{state.dynamic_range && state.dynamic_range !== 'SDR' && (
|
||||||
|
<span className="border border-border py-1 px-2 rounded">{state.dynamic_range}</span>
|
||||||
|
)}
|
||||||
|
{state.subtitle_id && (
|
||||||
|
<span
|
||||||
|
className="border border-border py-1 px-2 rounded cursor-pointer"
|
||||||
|
title={`EMBEDED SUBTITLE (${state.subtitle_id})`}
|
||||||
|
>
|
||||||
|
ESUB
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="w-full flex items-center gap-2">
|
||||||
|
<Button size="sm" onClick={() => openFile(state.filepath, null)}>
|
||||||
|
<Play className="w-4 h-4" />
|
||||||
|
Open
|
||||||
|
</Button>
|
||||||
|
<Button size="sm" variant="outline" onClick={() => openFile(state.filepath, 'explorer')}>
|
||||||
|
<FolderInput className="w-4 h-4" />
|
||||||
|
Open in Explorer
|
||||||
|
</Button>
|
||||||
|
<AlertDialog>
|
||||||
|
<AlertDialogTrigger asChild>
|
||||||
|
<Button size="sm" variant="destructive">
|
||||||
|
<Trash2 className="w-4 h-4" />
|
||||||
|
Remove
|
||||||
|
</Button>
|
||||||
|
</AlertDialogTrigger>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>Remove from library?</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
Are you sure you want to remove this download from the library? You can also delete the downloaded file by cheking the box below. This action cannot be undone.
|
||||||
|
</AlertDialogDescription>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Checkbox id="delete-file" checked={itemActionStates.isDeleteFileChecked} onCheckedChange={() => {setIsDeleteFileChecked(state.download_id, !itemActionStates.isDeleteFileChecked)}} />
|
||||||
|
<Label htmlFor="delete-file">Delete the downloaded file</Label>
|
||||||
|
</div>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
|
<AlertDialogAction onClick={
|
||||||
|
() => removeFromDownloads(state, itemActionStates.isDeleteFileChecked).then(() => {
|
||||||
|
setIsDeleteFileChecked(state.download_id, false);
|
||||||
|
})
|
||||||
|
}>Remove</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
<div className="w-full flex flex-col items-center gap-2 justify-center mt-27">
|
||||||
|
<h4 className="text-4xl font-bold text-muted-foreground/80 dark:text-muted">Nothing!</h4>
|
||||||
|
<p className="text-lg font-semibold text-muted-foreground/50">No Completed Downloads</p>
|
||||||
|
<p className="max-w-[50%] text-center text-xs text-muted-foreground/70">You have not completed any downloads yet. Complete downloading something to see here :)</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="incomplete">
|
||||||
<div className="w-full flex flex-col gap-2">
|
<div className="w-full flex flex-col gap-2">
|
||||||
{incompleteDownloads.length > 0 ? (
|
{incompleteDownloads.length > 0 ? (
|
||||||
incompleteDownloads.map((state) => {
|
incompleteDownloads.map((state) => {
|
||||||
@@ -275,150 +483,15 @@ export default function LibraryPage() {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
) : (
|
) : (
|
||||||
<div className="w-full flex items-center justify-center text-muted-foreground text-sm">No Incomplete downloads!</div>
|
<div className="w-full flex flex-col items-center gap-2 justify-center mt-27">
|
||||||
)}
|
<h4 className="text-4xl font-bold text-muted-foreground/80 dark:text-muted">Nothing!</h4>
|
||||||
</div>
|
<p className="text-lg font-semibold text-muted-foreground/50">No Incomplete Downloads</p>
|
||||||
<div className="w-full fle flex-col">
|
<p className="max-w-[50%] text-center text-xs text-muted-foreground/70">You have all caught up! Sit back and relax or just spin up a new download to see here :)</p>
|
||||||
<div className="flex w-full items-center gap-2 mb-2">
|
</div>
|
||||||
<CircleCheck className="size-4" />
|
|
||||||
<h3 className="text-nowrap font-semibold">Completed Downloads</h3>
|
|
||||||
</div>
|
|
||||||
<Separator orientation="horizontal" className="" />
|
|
||||||
</div>
|
|
||||||
<div className="w-full flex flex-col gap-2">
|
|
||||||
{completedDownloads.length > 0 ? (
|
|
||||||
completedDownloads.map((state) => {
|
|
||||||
const itemActionStates = downloadActions[state.download_id] || {
|
|
||||||
isResuming: false,
|
|
||||||
isPausing: false,
|
|
||||||
isCanceling: false,
|
|
||||||
isDeleteFileChecked: false,
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<div className="p-4 border border-border rounded-lg flex gap-4" key={state.download_id}>
|
|
||||||
<div className="w-[30%] flex flex-col justify-between gap-2">
|
|
||||||
<AspectRatio ratio={16 / 9} className="w-full rounded-lg overflow-hidden border border-border mb-2">
|
|
||||||
<ProxyImage src={state.thumbnail || ""} alt="thumbnail" className="" />
|
|
||||||
</AspectRatio>
|
|
||||||
<span className="w-full flex items-center justify-center text-xs border border-border py-1 px-2 rounded">
|
|
||||||
{state.filetype && (state.filetype === 'video' || state.filetype === 'video+audio') && (
|
|
||||||
<Video className="w-4 h-4 mr-2" />
|
|
||||||
)}
|
|
||||||
{state.filetype && state.filetype === 'audio' && (
|
|
||||||
<Music className="w-4 h-4 mr-2" />
|
|
||||||
)}
|
|
||||||
{(!state.filetype) || (state.filetype && state.filetype !== 'video' && state.filetype !== 'audio' && state.filetype !== 'video+audio') && (
|
|
||||||
<File className="w-4 h-4 mr-2" />
|
|
||||||
)}
|
|
||||||
{state.ext?.toUpperCase()} {state.resolution ? `(${state.resolution})` : null}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="w-full flex flex-col justify-between gap-2">
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<h4 className="">{state.title}</h4>
|
|
||||||
<p className="text-xs text-muted-foreground">{state.channel ? state.channel : 'unknown'} {state.host ? `• ${state.host}` : 'unknown'}</p>
|
|
||||||
<div className="flex items-center mt-1">
|
|
||||||
<span className="text-xs text-muted-foreground flex items-center pr-3"><Clock className="w-4 h-4 mr-2"/> {state.duration_string ? formatDurationString(state.duration_string) : 'unknown'}</span>
|
|
||||||
<Separator orientation="vertical" />
|
|
||||||
<span className="text-xs text-muted-foreground flex items-center px-3">
|
|
||||||
{state.filetype && (state.filetype === 'video' || state.filetype === 'video+audio') && (
|
|
||||||
<FileVideo2 className="w-4 h-4 mr-2"/>
|
|
||||||
)}
|
|
||||||
{state.filetype && state.filetype === 'audio' && (
|
|
||||||
<FileAudio2 className="w-4 h-4 mr-2" />
|
|
||||||
)}
|
|
||||||
{(!state.filetype) || (state.filetype && state.filetype !== 'video' && state.filetype !== 'audio' && state.filetype !== 'video+audio') && (
|
|
||||||
<FileQuestion className="w-4 h-4 mr-2" />
|
|
||||||
)}
|
|
||||||
{state.filesize ? formatFileSize(state.filesize) : 'unknown'}
|
|
||||||
</span>
|
|
||||||
<Separator orientation="vertical" />
|
|
||||||
<span className="text-xs text-muted-foreground flex items-center pl-3"><AudioLines className="w-4 h-4 mr-2"/>
|
|
||||||
{state.vbr && state.abr ? (
|
|
||||||
formatBitrate(state.vbr + state.abr)
|
|
||||||
) : state.vbr ? (
|
|
||||||
formatBitrate(state.vbr)
|
|
||||||
) : state.abr ? (
|
|
||||||
formatBitrate(state.abr)
|
|
||||||
) : (
|
|
||||||
'unknown'
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="hidden xl:flex items-center mt-1 gap-2 flex-wrap text-xs">
|
|
||||||
{state.playlist_id && state.playlist_index && (
|
|
||||||
<span
|
|
||||||
className="border border-border py-1 px-2 rounded flex items-center cursor-pointer"
|
|
||||||
title={`${state.playlist_title ?? 'UNKNOWN PLAYLIST'}` + ' by ' + `${state.playlist_channel ?? 'UNKNOWN CHANNEL'}`}
|
|
||||||
>
|
|
||||||
<ListVideo className="w-4 h-4 mr-2" /> Playlist ({state.playlist_index} of {state.playlist_n_entries})
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{state.vcodec && (
|
|
||||||
<span className="border border-border py-1 px-2 rounded">{formatCodec(state.vcodec)}</span>
|
|
||||||
)}
|
|
||||||
{state.acodec && (
|
|
||||||
<span className="border border-border py-1 px-2 rounded">{formatCodec(state.acodec)}</span>
|
|
||||||
)}
|
|
||||||
{state.dynamic_range && state.dynamic_range !== 'SDR' && (
|
|
||||||
<span className="border border-border py-1 px-2 rounded">{state.dynamic_range}</span>
|
|
||||||
)}
|
|
||||||
{state.subtitle_id && (
|
|
||||||
<span
|
|
||||||
className="border border-border py-1 px-2 rounded cursor-pointer"
|
|
||||||
title={`EMBEDED SUBTITLE (${state.subtitle_id})`}
|
|
||||||
>
|
|
||||||
ESUB
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="w-full flex items-center gap-2">
|
|
||||||
<Button size="sm" onClick={() => openFile(state.filepath, null)}>
|
|
||||||
<Play className="w-4 h-4" />
|
|
||||||
Open
|
|
||||||
</Button>
|
|
||||||
<Button size="sm" variant="outline" onClick={() => openFile(state.filepath, 'explorer')}>
|
|
||||||
<FolderInput className="w-4 h-4" />
|
|
||||||
Open in Explorer
|
|
||||||
</Button>
|
|
||||||
<AlertDialog>
|
|
||||||
<AlertDialogTrigger asChild>
|
|
||||||
<Button size="sm" variant="destructive">
|
|
||||||
<Trash2 className="w-4 h-4" />
|
|
||||||
Remove
|
|
||||||
</Button>
|
|
||||||
</AlertDialogTrigger>
|
|
||||||
<AlertDialogContent>
|
|
||||||
<AlertDialogHeader>
|
|
||||||
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
|
||||||
<AlertDialogDescription>
|
|
||||||
This action cannot be undone! it will permanently remove this from downloads.
|
|
||||||
</AlertDialogDescription>
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<Checkbox id="delete-file" checked={itemActionStates.isDeleteFileChecked} onCheckedChange={() => {setIsDeleteFileChecked(state.download_id, !itemActionStates.isDeleteFileChecked)}} />
|
|
||||||
<Label htmlFor="delete-file">Delete the downloaded file</Label>
|
|
||||||
</div>
|
|
||||||
</AlertDialogHeader>
|
|
||||||
<AlertDialogFooter>
|
|
||||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
||||||
<AlertDialogAction onClick={
|
|
||||||
() => removeFromDownloads(state, itemActionStates.isDeleteFileChecked).then(() => {
|
|
||||||
setIsDeleteFileChecked(state.download_id, false);
|
|
||||||
})
|
|
||||||
}>Remove</AlertDialogAction>
|
|
||||||
</AlertDialogFooter>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialog>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
) : (
|
|
||||||
<div className="w-full flex items-center justify-center text-muted-foreground text-sm">No Completed downloads!</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -7,7 +7,7 @@ import { Switch } from "@/components/ui/switch";
|
|||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import { ExternalLink, FolderOpen, Loader2, LucideIcon, Monitor, Moon, Radio, RotateCcw, RotateCw, Sun, Terminal } from "lucide-react";
|
import { ArrowDownToLine, ArrowRight, BrushCleaning, EthernetPort, ExternalLink, FileVideo, Folder, FolderOpen, Info, Loader2, LucideIcon, Monitor, Moon, Radio, RotateCcw, RotateCw, Sun, Terminal, WandSparkles, Wifi, Wrench } from "lucide-react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useTheme } from "@/providers/themeProvider";
|
import { useTheme } from "@/providers/themeProvider";
|
||||||
@@ -22,18 +22,46 @@ import { zodResolver } from "@hookform/resolvers/zod"
|
|||||||
import { Form, FormControl, FormField, FormItem, FormMessage } from "@/components/ui/form";
|
import { Form, FormControl, FormField, FormItem, FormMessage } from "@/components/ui/form";
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from "@/components/ui/alert-dialog";
|
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from "@/components/ui/alert-dialog";
|
||||||
|
import { SlidingButton } from "@/components/custom/slidingButton";
|
||||||
|
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||||
|
import * as fs from "@tauri-apps/plugin-fs";
|
||||||
|
import { join } from "@tauri-apps/api/path";
|
||||||
|
import { formatSpeed } from "@/utils";
|
||||||
|
|
||||||
const websocketPortSchema = z.object({
|
const websocketPortSchema = z.object({
|
||||||
port: z.string().min(1, { message: "Websocket port is required" })
|
port: z.coerce.number<number>({
|
||||||
.regex(/^\d+$/, { message: "Websocket port must be a number" })
|
error: (issue) => issue.input === undefined || issue.input === null || issue.input === ""
|
||||||
.transform((val) => parseInt(val, 10))
|
? "Websocket Port is required"
|
||||||
.refine((port) => port >= 50000 && port <= 60000, {
|
: "Websocket Port must be a valid number"
|
||||||
message: "Websocket port must be between 50000 and 60000",
|
}).int({
|
||||||
|
message: "Websocket Port must be an integer"
|
||||||
|
}).min(50000, {
|
||||||
|
message: "Websocket Port must be at least 50000"
|
||||||
|
}).max(60000, {
|
||||||
|
message: "Websocket Port must be at most 60000"
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
const proxyUrlSchema = z.object({
|
||||||
|
url: z.url({
|
||||||
|
error: (issue) => issue.input === undefined || issue.input === null || issue.input === ""
|
||||||
|
? "Proxy URL is required"
|
||||||
|
: "Invalid URL format"
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
const proxyUrlSchema = z.object({
|
const rateLimitSchema = z.object({
|
||||||
url: z.string().min(1, { message: "Proxy URL is required" }).url({ message: "Invalid URL format" })
|
rate_limit: z.coerce.number<number>({
|
||||||
|
error: (issue) => issue.input === undefined || issue.input === null || issue.input === ""
|
||||||
|
? "Rate Limit is required"
|
||||||
|
: "Rate Limit must be a valid number"
|
||||||
|
}).int({
|
||||||
|
message: "Rate Limit must be an integer"
|
||||||
|
}).min(1024, {
|
||||||
|
message: "Rate Limit must be at least 1024 bytes/s (1 KB/s)"
|
||||||
|
}).max(104857600, {
|
||||||
|
message: "Rate Limit must be at most 104857600 bytes/s (100 MB/s)"
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default function SettingsPage() {
|
export default function SettingsPage() {
|
||||||
@@ -41,7 +69,11 @@ export default function SettingsPage() {
|
|||||||
const { setTheme } = useTheme();
|
const { setTheme } = useTheme();
|
||||||
|
|
||||||
const activeTab = useSettingsPageStatesStore(state => state.activeTab);
|
const activeTab = useSettingsPageStatesStore(state => state.activeTab);
|
||||||
|
const activeSubAppTab = useSettingsPageStatesStore(state => state.activeSubAppTab);
|
||||||
|
const activeSubExtTab = useSettingsPageStatesStore(state => state.activeSubExtTab);
|
||||||
const setActiveTab = useSettingsPageStatesStore(state => state.setActiveTab);
|
const setActiveTab = useSettingsPageStatesStore(state => state.setActiveTab);
|
||||||
|
const setActiveSubAppTab = useSettingsPageStatesStore(state => state.setActiveSubAppTab);
|
||||||
|
const setActiveSubExtTab = useSettingsPageStatesStore(state => state.setActiveSubExtTab);
|
||||||
|
|
||||||
const isUsingDefaultSettings = useSettingsPageStatesStore(state => state.isUsingDefaultSettings);
|
const isUsingDefaultSettings = useSettingsPageStatesStore(state => state.isUsingDefaultSettings);
|
||||||
const ytDlpVersion = useSettingsPageStatesStore(state => state.ytDlpVersion);
|
const ytDlpVersion = useSettingsPageStatesStore(state => state.ytDlpVersion);
|
||||||
@@ -51,9 +83,19 @@ export default function SettingsPage() {
|
|||||||
const ytDlpAutoUpdate = useSettingsPageStatesStore(state => state.settings.ytdlp_auto_update);
|
const ytDlpAutoUpdate = useSettingsPageStatesStore(state => state.settings.ytdlp_auto_update);
|
||||||
const appTheme = useSettingsPageStatesStore(state => state.settings.theme);
|
const appTheme = useSettingsPageStatesStore(state => state.settings.theme);
|
||||||
const maxParallelDownloads = useSettingsPageStatesStore(state => state.settings.max_parallel_downloads);
|
const maxParallelDownloads = useSettingsPageStatesStore(state => state.settings.max_parallel_downloads);
|
||||||
|
const maxRetries = useSettingsPageStatesStore(state => state.settings.max_retries);
|
||||||
const preferVideoOverPlaylist = useSettingsPageStatesStore(state => state.settings.prefer_video_over_playlist);
|
const preferVideoOverPlaylist = useSettingsPageStatesStore(state => state.settings.prefer_video_over_playlist);
|
||||||
|
const strictDownloadabilityCheck = useSettingsPageStatesStore(state => state.settings.strict_downloadablity_check);
|
||||||
const useProxy = useSettingsPageStatesStore(state => state.settings.use_proxy);
|
const useProxy = useSettingsPageStatesStore(state => state.settings.use_proxy);
|
||||||
const proxyUrl = useSettingsPageStatesStore(state => state.settings.proxy_url);
|
const proxyUrl = useSettingsPageStatesStore(state => state.settings.proxy_url);
|
||||||
|
const useRateLimit = useSettingsPageStatesStore(state => state.settings.use_rate_limit);
|
||||||
|
const rateLimit = useSettingsPageStatesStore(state => state.settings.rate_limit);
|
||||||
|
const videoFormat = useSettingsPageStatesStore(state => state.settings.video_format);
|
||||||
|
const audioFormat = useSettingsPageStatesStore(state => state.settings.audio_format);
|
||||||
|
const alwaysReencodeVideo = useSettingsPageStatesStore(state => state.settings.always_reencode_video);
|
||||||
|
const embedVideoMetadata = useSettingsPageStatesStore(state => state.settings.embed_video_metadata);
|
||||||
|
const embedAudioMetadata = useSettingsPageStatesStore(state => state.settings.embed_audio_metadata);
|
||||||
|
const embedAudioThumbnail = useSettingsPageStatesStore(state => state.settings.embed_audio_thumbnail);
|
||||||
const websocketPort = useSettingsPageStatesStore(state => state.settings.websocket_port);
|
const websocketPort = useSettingsPageStatesStore(state => state.settings.websocket_port);
|
||||||
const isChangingWebSocketPort = useSettingsPageStatesStore(state => state.isChangingWebSocketPort);
|
const isChangingWebSocketPort = useSettingsPageStatesStore(state => state.isChangingWebSocketPort);
|
||||||
const setIsChangingWebSocketPort = useSettingsPageStatesStore(state => state.setIsChangingWebSocketPort);
|
const setIsChangingWebSocketPort = useSettingsPageStatesStore(state => state.setIsChangingWebSocketPort);
|
||||||
@@ -66,6 +108,7 @@ export default function SettingsPage() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const downloadDirPath = useBasePathsStore((state) => state.downloadDirPath);
|
const downloadDirPath = useBasePathsStore((state) => state.downloadDirPath);
|
||||||
|
const tempDownloadDirPath = useBasePathsStore((state) => state.tempDownloadDirPath);
|
||||||
const setPath = useBasePathsStore((state) => state.setPath);
|
const setPath = useBasePathsStore((state) => state.setPath);
|
||||||
const { saveSettingsKey, resetSettings } = useSettings();
|
const { saveSettingsKey, resetSettings } = useSettings();
|
||||||
const { updateYtDlp } = useYtDlpUpdater();
|
const { updateYtDlp } = useYtDlpUpdater();
|
||||||
@@ -77,6 +120,53 @@ export default function SettingsPage() {
|
|||||||
{ value: 'system', icon: Monitor, label: 'System' },
|
{ value: 'system', icon: Monitor, label: 'System' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const openLink = async (url: string, app: string | null) => {
|
||||||
|
try {
|
||||||
|
await invoke('open_file_with_app', { filePath: url, appName: app }).then(() => {
|
||||||
|
toast({
|
||||||
|
title: 'Opening Link',
|
||||||
|
description: `Opening link with ${app ? app : 'default app'}.`,
|
||||||
|
})
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
toast({
|
||||||
|
title: 'Failed to open link',
|
||||||
|
description: 'An error occurred while trying to open the link.',
|
||||||
|
variant: "destructive"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const cleanTemporaryDownloads = async () => {
|
||||||
|
const tempFiles = await fs.readDir(tempDownloadDirPath ?? '');
|
||||||
|
if (tempFiles.length > 0) {
|
||||||
|
try {
|
||||||
|
for (const file of tempFiles) {
|
||||||
|
if (file.isFile) {
|
||||||
|
const filePath = await join(tempDownloadDirPath ?? '', file.name);
|
||||||
|
await fs.remove(filePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
toast({
|
||||||
|
title: "Temporary Downloads Cleaned",
|
||||||
|
description: "All temporary downloads have been successfully cleaned up.",
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
toast({
|
||||||
|
title: "Temporary Downloads Cleanup Failed",
|
||||||
|
description: "An error occurred while trying to clean up temporary downloads. Please try again.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
toast({
|
||||||
|
title: "No Temporary Downloads",
|
||||||
|
description: "There are no temporary downloads to clean up.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const proxyUrlForm = useForm<z.infer<typeof proxyUrlSchema>>({
|
const proxyUrlForm = useForm<z.infer<typeof proxyUrlSchema>>({
|
||||||
resolver: zodResolver(proxyUrlSchema),
|
resolver: zodResolver(proxyUrlSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -104,6 +194,33 @@ export default function SettingsPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const rateLimitForm = useForm<z.infer<typeof rateLimitSchema>>({
|
||||||
|
resolver: zodResolver(rateLimitSchema),
|
||||||
|
defaultValues: {
|
||||||
|
rate_limit: rateLimit,
|
||||||
|
},
|
||||||
|
mode: "onChange",
|
||||||
|
});
|
||||||
|
const watchedRateLimit = rateLimitForm.watch("rate_limit");
|
||||||
|
const { errors: rateLimitFormErrors } = rateLimitForm.formState;
|
||||||
|
|
||||||
|
function handleRateLimitSubmit(values: z.infer<typeof rateLimitSchema>) {
|
||||||
|
try {
|
||||||
|
saveSettingsKey('rate_limit', values.rate_limit);
|
||||||
|
toast({
|
||||||
|
title: "Rate Limit updated",
|
||||||
|
description: `Rate Limit changed to ${values.rate_limit} bytes/s`,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error changing rate limit:", error);
|
||||||
|
toast({
|
||||||
|
title: "Failed to change rate limit",
|
||||||
|
description: "Please try again.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
interface Config {
|
interface Config {
|
||||||
port: number;
|
port: number;
|
||||||
}
|
}
|
||||||
@@ -155,15 +272,44 @@ export default function SettingsPage() {
|
|||||||
<div className="container mx-auto p-4 space-y-4 min-h-screen">
|
<div className="container mx-auto p-4 space-y-4 min-h-screen">
|
||||||
<Heading title="Settings" description="Manage your preferences and app settings" />
|
<Heading title="Settings" description="Manage your preferences and app settings" />
|
||||||
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||||
|
<div className="w-full flex items-center justify-between">
|
||||||
<TabsList>
|
<TabsList>
|
||||||
<TabsTrigger value="general">General</TabsTrigger>
|
<TabsTrigger value="app">Application</TabsTrigger>
|
||||||
<TabsTrigger value="extension">Extension</TabsTrigger>
|
<TabsTrigger value="extension">Extension</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
<TabsContent value="general">
|
<AlertDialog>
|
||||||
|
<AlertDialogTrigger asChild>
|
||||||
|
<Button
|
||||||
|
className="w-fit"
|
||||||
|
variant="destructive"
|
||||||
|
size="sm"
|
||||||
|
disabled={isUsingDefaultSettings}
|
||||||
|
>
|
||||||
|
<RotateCcw className="h-4 w-4" />
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
</AlertDialogTrigger>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>Reset settings to default?</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
Are you sure you want to reset all settings to their default values? This action cannot be undone!
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
|
<AlertDialogAction onClick={
|
||||||
|
() => resetSettings()
|
||||||
|
}>Reset</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
</div>
|
||||||
|
<TabsContent value="app">
|
||||||
<Card className="p-4 space-y-4 my-4">
|
<Card className="p-4 space-y-4 my-4">
|
||||||
<div className="w-full flex gap-4 items-center justify-between">
|
<div className="w-full flex gap-4 items-center justify-between">
|
||||||
<div className="flex gap-4 items-center">
|
<div className="flex gap-4 items-center">
|
||||||
<div className="imgwrapper w-10 h-10 flex items-center justify-center bg-gradient-to-r from-[#4444FF] to-[#FF43D0] rounded-md overflow-hidden border border-border">
|
<div className="imgwrapper w-10 h-10 flex items-center justify-center bg-linear-65 from-[#FF43D0] to-[#4444FF] rounded-md overflow-hidden border border-border">
|
||||||
<Terminal className="size-5 text-white" />
|
<Terminal className="size-5 text-white" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
@@ -218,10 +364,95 @@ export default function SettingsPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
<div className="flex flex-col w-[50%] gap-4">
|
<Tabs
|
||||||
|
className="w-full flex flex-row items-start gap-4 mt-7"
|
||||||
|
orientation="vertical"
|
||||||
|
value={activeSubAppTab}
|
||||||
|
onValueChange={setActiveSubAppTab}
|
||||||
|
>
|
||||||
|
<TabsList className="shrink-0 grid grid-cols-1 gap-1 p-0 bg-background min-w-45">
|
||||||
|
<TabsTrigger
|
||||||
|
key="general"
|
||||||
|
value="general"
|
||||||
|
className="data-[state=active]:bg-primary data-[state=active]:text-primary-foreground justify-start px-3 py-1.5 gap-2"
|
||||||
|
><Wrench className="size-4" /> General</TabsTrigger>
|
||||||
|
<TabsTrigger
|
||||||
|
key="appearance"
|
||||||
|
value="appearance"
|
||||||
|
className="data-[state=active]:bg-primary data-[state=active]:text-primary-foreground justify-start px-3 py-1.5 gap-2"
|
||||||
|
><WandSparkles className="size-4" /> Appearance</TabsTrigger>
|
||||||
|
<TabsTrigger
|
||||||
|
key="folders"
|
||||||
|
value="folders"
|
||||||
|
className="data-[state=active]:bg-primary data-[state=active]:text-primary-foreground justify-start px-3 py-1.5 gap-2"
|
||||||
|
><Folder className="size-4" /> Folders</TabsTrigger>
|
||||||
|
<TabsTrigger
|
||||||
|
key="formats"
|
||||||
|
value="formats"
|
||||||
|
className="data-[state=active]:bg-primary data-[state=active]:text-primary-foreground justify-start px-3 py-1.5 gap-2"
|
||||||
|
><FileVideo className="size-4" /> Formats</TabsTrigger>
|
||||||
|
<TabsTrigger
|
||||||
|
key="metadata"
|
||||||
|
value="metadata"
|
||||||
|
className="data-[state=active]:bg-primary data-[state=active]:text-primary-foreground justify-start px-3 py-1.5 gap-2"
|
||||||
|
><Info className="size-4" /> Metadata</TabsTrigger>
|
||||||
|
<TabsTrigger
|
||||||
|
key="network"
|
||||||
|
value="network"
|
||||||
|
className="data-[state=active]:bg-primary data-[state=active]:text-primary-foreground justify-start px-3 py-1.5 gap-2"
|
||||||
|
><Wifi className="size-4" /> Network</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
<div className="min-h-full flex flex-col max-w-[55%] w-full border-l border-border pl-4">
|
||||||
|
<TabsContent key="general" value="general" className="flex flex-col gap-4 min-h-[235px]">
|
||||||
|
<div className="max-parallel-downloads">
|
||||||
|
<h3 className="font-semibold">Max Parallel Downloads</h3>
|
||||||
|
<p className="text-xs text-muted-foreground mb-3">Set maximum number of allowed parallel downloads</p>
|
||||||
|
<Slider
|
||||||
|
id="max-parallel-downloads"
|
||||||
|
className="w-[350px] mb-2"
|
||||||
|
value={[maxParallelDownloads]}
|
||||||
|
min={1}
|
||||||
|
max={5}
|
||||||
|
onValueChange={(value) => saveSettingsKey('max_parallel_downloads', value[0])}
|
||||||
|
/>
|
||||||
|
<Label htmlFor="max-parallel-downloads" className="text-xs text-muted-foreground">(Current: {maxParallelDownloads}) (Default: 2, Maximum: 5)</Label>
|
||||||
|
</div>
|
||||||
|
<div className="prefer-video-over-playlist">
|
||||||
|
<h3 className="font-semibold">Prefer Video Over Playlist</h3>
|
||||||
|
<p className="text-xs text-muted-foreground mb-3">Prefer only the video, if the URL refers to a video and a playlist</p>
|
||||||
|
<Switch
|
||||||
|
id="prefer-video-over-playlist"
|
||||||
|
checked={preferVideoOverPlaylist}
|
||||||
|
onCheckedChange={(checked) => saveSettingsKey('prefer_video_over_playlist', checked)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="strict-downloadability-check">
|
||||||
|
<h3 className="font-semibold">Strict Downloadablity Check</h3>
|
||||||
|
<p className="text-xs text-muted-foreground mb-3">Only show streams that are actualy downloadable, also check formats before downloading (high quality results, takes longer time to search)</p>
|
||||||
|
<Switch
|
||||||
|
id="strict-downloadablity-check"
|
||||||
|
checked={strictDownloadabilityCheck}
|
||||||
|
onCheckedChange={(checked) => saveSettingsKey('strict_downloadablity_check', checked)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="max-retries">
|
||||||
|
<h3 className="font-semibold">Max Retries</h3>
|
||||||
|
<p className="text-xs text-muted-foreground mb-3">Set maximum number of retries for a download before giving up</p>
|
||||||
|
<Slider
|
||||||
|
id="max-retries"
|
||||||
|
className="w-[350px] mb-2"
|
||||||
|
value={[maxRetries]}
|
||||||
|
min={1}
|
||||||
|
max={100}
|
||||||
|
onValueChange={(value) => saveSettingsKey('max_retries', value[0])}
|
||||||
|
/>
|
||||||
|
<Label htmlFor="max-retries" className="text-xs text-muted-foreground">(Current: {maxRetries}) (Default: 5, Maximum: 100)</Label>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent key="appearance" value="appearance" className="flex flex-col gap-4 min-h-[235px]">
|
||||||
<div className="app-theme">
|
<div className="app-theme">
|
||||||
<h3 className="font-semibold">Theme</h3>
|
<h3 className="font-semibold">Theme</h3>
|
||||||
<p className="text-sm text-muted-foreground mb-3">Choose app interface theme</p>
|
<p className="text-xs text-muted-foreground mb-3">Choose app interface theme</p>
|
||||||
<div className={cn('inline-flex gap-1 rounded-lg p-1 bg-muted')}>
|
<div className={cn('inline-flex gap-1 rounded-lg p-1 bg-muted')}>
|
||||||
{themeOptions.map(({ value, icon: Icon, label }) => (
|
{themeOptions.map(({ value, icon: Icon, label }) => (
|
||||||
<button
|
<button
|
||||||
@@ -240,9 +471,11 @@ export default function SettingsPage() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent key="folders" value="folders" className="flex flex-col gap-4 min-h-[235px]">
|
||||||
<div className="download-dir">
|
<div className="download-dir">
|
||||||
<h3 className="font-semibold">Download Directory</h3>
|
<h3 className="font-semibold">Download Folder</h3>
|
||||||
<p className="text-sm text-muted-foreground mb-3">Set default download directory</p>
|
<p className="text-xs text-muted-foreground mb-3">Set default download folder (directory)</p>
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<Input className="focus-visible:ring-0" type="text" placeholder="Select download directory" value={downloadDirPath ?? 'Unknown'} readOnly/>
|
<Input className="focus-visible:ring-0" type="text" placeholder="Select download directory" value={downloadDirPath ?? 'Unknown'} readOnly/>
|
||||||
<Button
|
<Button
|
||||||
@@ -271,31 +504,136 @@ export default function SettingsPage() {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="max-parallel-downloads">
|
<div className="temporary-download-dir">
|
||||||
<h3 className="font-semibold">Max Parallel Downloads</h3>
|
<h3 className="font-semibold">Temporary Download Folder</h3>
|
||||||
<p className="text-sm text-muted-foreground mb-3">Set maximum number of allowed parallel downloads</p>
|
<p className="text-xs text-muted-foreground mb-3">Clean up temporary downloads (broken, cancelled, paused downloads)</p>
|
||||||
<Slider
|
<div className="flex items-center gap-4">
|
||||||
id="max-parallel-downloads"
|
<Input className="focus-visible:ring-0" type="text" placeholder="Temporary download directory" value={tempDownloadDirPath ?? 'Unknown'} readOnly/>
|
||||||
className="w-[350px]"
|
<AlertDialog>
|
||||||
value={[maxParallelDownloads]}
|
<AlertDialogTrigger asChild>
|
||||||
min={1}
|
<Button
|
||||||
max={5}
|
variant="destructive"
|
||||||
onValueChange={(value) => saveSettingsKey('max_parallel_downloads', value[0])}
|
disabled={ongoingDownloads.length > 0}
|
||||||
/>
|
>
|
||||||
<Label htmlFor="max-parallel-downloads" className="text-xs text-muted-foreground">(Current: {maxParallelDownloads}) (Default: 2, Maximum: 5)</Label>
|
<BrushCleaning className="size-4" /> Clean
|
||||||
|
</Button>
|
||||||
|
</AlertDialogTrigger>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>Clean up all temporary downloads?</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>Are you sure you want to clean up all temporary downloads? This will remove all broken, cancelled and paused downloads from the temporary folder. Paused downloads will re-start from the begining. This action cannot be undone!</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
|
<AlertDialogAction
|
||||||
|
onClick={() => cleanTemporaryDownloads()}
|
||||||
|
>Clean</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
</div>
|
</div>
|
||||||
<div className="prefer-video-over-playlist">
|
</div>
|
||||||
<h3 className="font-semibold">Prefer Video Over Playlist</h3>
|
</TabsContent>
|
||||||
<p className="text-sm text-muted-foreground mb-3">Prefer only the video, if the URL refers to a video and a playlist</p>
|
<TabsContent key="formats" value="formats" className="flex flex-col gap-4 min-h-[235px]">
|
||||||
|
<div className="video-format">
|
||||||
|
<h3 className="font-semibold">Video Format</h3>
|
||||||
|
<p className="text-xs text-muted-foreground mb-3">Choose in which format the final video file will be saved</p>
|
||||||
|
<RadioGroup
|
||||||
|
orientation="horizontal"
|
||||||
|
className="flex items-center gap-4"
|
||||||
|
value={videoFormat}
|
||||||
|
onValueChange={(value) => saveSettingsKey('video_format', value)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<RadioGroupItem value="auto" id="v-auto" />
|
||||||
|
<Label htmlFor="v-auto">Auto (Default)</Label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<RadioGroupItem value="mp4" id="v-mp4" />
|
||||||
|
<Label htmlFor="v-mp4">MP4</Label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<RadioGroupItem value="webm" id="v-webm" />
|
||||||
|
<Label htmlFor="v-webm">WEBM</Label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<RadioGroupItem value="mkv" id="v-mkv" />
|
||||||
|
<Label htmlFor="v-mkv">MKV</Label>
|
||||||
|
</div>
|
||||||
|
</RadioGroup>
|
||||||
|
</div>
|
||||||
|
<div className="audio-format">
|
||||||
|
<h3 className="font-semibold">Audio Format</h3>
|
||||||
|
<p className="text-xs text-muted-foreground mb-3">Choose in which format the final audio file will be saved</p>
|
||||||
|
<RadioGroup
|
||||||
|
orientation="horizontal"
|
||||||
|
className="flex items-center gap-4"
|
||||||
|
value={audioFormat}
|
||||||
|
onValueChange={(value) => saveSettingsKey('audio_format', value)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<RadioGroupItem value="auto" id="a-auto" />
|
||||||
|
<Label htmlFor="a-auto">Auto (Default)</Label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<RadioGroupItem value="m4a" id="a-m4a" />
|
||||||
|
<Label htmlFor="a-m4a">M4A</Label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<RadioGroupItem value="opus" id="a-opus" />
|
||||||
|
<Label htmlFor="a-opus">OPUS</Label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<RadioGroupItem value="mp3" id="a-mp3" />
|
||||||
|
<Label htmlFor="a-mp3">MP3</Label>
|
||||||
|
</div>
|
||||||
|
</RadioGroup>
|
||||||
|
</div>
|
||||||
|
<div className="always-reencode-video">
|
||||||
|
<h3 className="font-semibold">Always Re-Encode Video</h3>
|
||||||
|
<p className="text-xs text-muted-foreground mb-3">Instead of remuxing (simple container change) always re-encode the video to the target format with best compatible codecs (better compatibility, takes longer processing time)</p>
|
||||||
<Switch
|
<Switch
|
||||||
id="prefer-video-over-playlist"
|
id="always-reencode-video"
|
||||||
checked={preferVideoOverPlaylist}
|
checked={alwaysReencodeVideo}
|
||||||
onCheckedChange={(checked) => saveSettingsKey('prefer_video_over_playlist', checked)}
|
onCheckedChange={(checked) => saveSettingsKey('always_reencode_video', checked)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent key="metadata" value="metadata" className="flex flex-col gap-4 min-h-[235px]">
|
||||||
|
<div className="embed-video-metadata">
|
||||||
|
<h3 className="font-semibold">Embed Metadata</h3>
|
||||||
|
<p className="text-xs text-muted-foreground mb-3">Wheather to embed metadata in video/audio files (info, chapters)</p>
|
||||||
|
<div className="flex items-center space-x-2 mb-3">
|
||||||
|
<Switch
|
||||||
|
id="embed-video-metadata"
|
||||||
|
checked={embedVideoMetadata}
|
||||||
|
onCheckedChange={(checked) => saveSettingsKey('embed_video_metadata', checked)}
|
||||||
|
/>
|
||||||
|
<Label htmlFor="embed-video-metadata">Video</Label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Switch
|
||||||
|
id="embed-audio-metadata"
|
||||||
|
checked={embedAudioMetadata}
|
||||||
|
onCheckedChange={(checked) => saveSettingsKey('embed_audio_metadata', checked)}
|
||||||
|
/>
|
||||||
|
<Label htmlFor="embed-audio-metadata">Audio</Label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="embed-audio-thumbnail">
|
||||||
|
<h3 className="font-semibold">Embed Thumbnail in Audio</h3>
|
||||||
|
<p className="text-xs text-muted-foreground mb-3">Wheather to embed thumbnail in audio files (as cover art)</p>
|
||||||
|
<Switch
|
||||||
|
id="embed-audio-thumbnail"
|
||||||
|
checked={embedAudioThumbnail}
|
||||||
|
onCheckedChange={(checked) => saveSettingsKey('embed_audio_thumbnail', checked)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent key="network" value="network" className="flex flex-col gap-4 min-h-[235px]">
|
||||||
<div className="proxy">
|
<div className="proxy">
|
||||||
<h3 className="font-semibold">Proxy</h3>
|
<h3 className="font-semibold">Proxy</h3>
|
||||||
<p className="text-sm text-muted-foreground mb-3">Use proxy for downloads, Unblocks blocked sites in your region (Download speed may affect, Some sites may not work)</p>
|
<p className="text-xs text-muted-foreground mb-3">Use proxy for downloads, Unblocks blocked sites in your region (download speed may affect, some sites may not work)</p>
|
||||||
<div className="flex items-center space-x-2 mb-4">
|
<div className="flex items-center space-x-2 mb-4">
|
||||||
<Switch
|
<Switch
|
||||||
id="use-proxy"
|
id="use-proxy"
|
||||||
@@ -304,7 +642,6 @@ export default function SettingsPage() {
|
|||||||
/>
|
/>
|
||||||
<Label htmlFor="use-proxy">Use Proxy</Label>
|
<Label htmlFor="use-proxy">Use Proxy</Label>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<Form {...proxyUrlForm}>
|
<Form {...proxyUrlForm}>
|
||||||
<form onSubmit={proxyUrlForm.handleSubmit(handleProxyUrlSubmit)} className="flex gap-4 w-full" autoComplete="off">
|
<form onSubmit={proxyUrlForm.handleSubmit(handleProxyUrlSubmit)} className="flex gap-4 w-full" autoComplete="off">
|
||||||
<FormField
|
<FormField
|
||||||
@@ -334,19 +671,66 @@ export default function SettingsPage() {
|
|||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="rate-limit">
|
||||||
|
<h3 className="font-semibold">Rate Limit</h3>
|
||||||
|
<p className="text-xs text-muted-foreground mb-3">Limit download speed to prevent network congestion. Rate limit is applied per-download basis (not in the whole app)</p>
|
||||||
|
<div className="flex items-center space-x-2 mb-4">
|
||||||
|
<Switch
|
||||||
|
id="use-rate-limit"
|
||||||
|
checked={useRateLimit}
|
||||||
|
onCheckedChange={(checked) => saveSettingsKey('use_rate_limit', checked)}
|
||||||
|
/>
|
||||||
|
<Label htmlFor="use-rate-limit">Use Rate Limit</Label>
|
||||||
</div>
|
</div>
|
||||||
|
<Form {...rateLimitForm}>
|
||||||
|
<form onSubmit={rateLimitForm.handleSubmit(handleRateLimitSubmit)} className="flex gap-4 w-full" autoComplete="off">
|
||||||
|
<FormField
|
||||||
|
control={rateLimitForm.control}
|
||||||
|
name="rate_limit"
|
||||||
|
disabled={!useRateLimit}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="w-full">
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
className="focus-visible:ring-0"
|
||||||
|
placeholder="Enter rate limit in bytes/s"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<Label htmlFor="rate_limit" className="text-xs text-muted-foreground">(Configured: {rateLimit ? `${rateLimit} = ${formatSpeed(rateLimit)}` : 'No'}, Status: {useRateLimit ? 'Enabled' : 'Disabled'}) (Default: 1048576, Range: 1024-104857600)</Label>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
disabled={!watchedRateLimit || Number(watchedRateLimit) === rateLimit || Object.keys(rateLimitFormErrors).length > 0 || !useRateLimit}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
</div>
|
||||||
|
</Tabs>
|
||||||
|
</TabsContent>
|
||||||
<TabsContent value="extension">
|
<TabsContent value="extension">
|
||||||
<Card className="p-4 space-y-4 my-4">
|
<Card className="p-4 space-y-4 my-4">
|
||||||
<div className="w-full flex gap-4 items-center justify-between">
|
<div className="w-full flex gap-4 items-center justify-between">
|
||||||
<div className="flex gap-4 items-center">
|
<div className="flex gap-4 items-center">
|
||||||
<div className="imgwrapper w-10 h-10 flex items-center justify-center bg-gradient-to-r from-[#4444FF] to-[#FF43D0] rounded-md overflow-hidden border border-border">
|
<div className="imgwrapper w-10 h-10 flex items-center justify-center bg-linear-65 from-[#FF43D0] to-[#4444FF] rounded-md overflow-hidden border border-border">
|
||||||
<Radio className="size-5 text-white" />
|
<Radio className="size-5 text-white" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<h3 className="">Extension Websocket Server</h3>
|
<h3 className="">Extension Websocket Server</h3>
|
||||||
<p className="text-xs text-muted-foreground">{isChangingWebSocketPort || isRestartingWebSocketServer ? 'Restarting...' : 'Running' }</p>
|
<div className="text-xs flex items-center">
|
||||||
|
{isChangingWebSocketPort || isRestartingWebSocketServer ? (
|
||||||
|
<><div className="h-1.5 w-1.5 rounded-full bg-amber-600 dark:bg-amber-500 mr-1.5 mt-0.5" /><span className="text-amber-600 dark:text-amber-500">Restarting...</span></>
|
||||||
|
) : (
|
||||||
|
<><div className="h-1.5 w-1.5 rounded-full bg-emerald-600 dark:bg-emerald-500 mr-1.5 mt-0.5" /><span className="text-emerald-600 dark:text-emerald-500">Running</span></>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-4 items-center">
|
<div className="flex gap-4 items-center">
|
||||||
@@ -387,10 +771,79 @@ export default function SettingsPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
<div className="flex flex-col w-[50%] gap-4">
|
<Tabs
|
||||||
|
className="w-full flex flex-row items-start gap-4 mt-7"
|
||||||
|
orientation="vertical"
|
||||||
|
value={activeSubExtTab}
|
||||||
|
onValueChange={setActiveSubExtTab}
|
||||||
|
>
|
||||||
|
<TabsList className="shrink-0 grid grid-cols-1 gap-1 p-0 bg-background min-w-45">
|
||||||
|
<TabsTrigger
|
||||||
|
key="install"
|
||||||
|
value="install"
|
||||||
|
className="data-[state=active]:bg-primary data-[state=active]:text-primary-foreground justify-start px-3 py-1.5 gap-2"
|
||||||
|
><ArrowDownToLine className="size-4" /> Install</TabsTrigger>
|
||||||
|
<TabsTrigger
|
||||||
|
key="port"
|
||||||
|
value="port"
|
||||||
|
className="data-[state=active]:bg-primary data-[state=active]:text-primary-foreground justify-start px-3 py-1.5 gap-2"
|
||||||
|
><EthernetPort className="size-4" /> Port</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
<div className="min-h-full flex flex-col w-full border-l border-border pl-4">
|
||||||
|
<TabsContent key="install" value="install" className="flex flex-col gap-4 min-h-[150px] max-w-[90%]">
|
||||||
|
<div className="install-neodlp-extension">
|
||||||
|
<h3 className="font-semibold">NeoDLP Extension</h3>
|
||||||
|
<p className="text-xs text-muted-foreground mb-4">Integrate NeoDLP with your favourite browser</p>
|
||||||
|
<div className="flex items-center gap-4 mb-4">
|
||||||
|
<SlidingButton
|
||||||
|
slidingContent={
|
||||||
|
<div className="flex items-center justify-center gap-2 text-white dark:text-black">
|
||||||
|
<ArrowRight className="size-4" />
|
||||||
|
<span>Get Now</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
onClick={() => openLink('https://chromewebstore.google.com/detail/neo-downloader-plus/mehopeailfjmiloiiohgicphlcgpompf', 'chrome')}
|
||||||
|
>
|
||||||
|
<span className="font-semibold flex items-center gap-2">
|
||||||
|
<svg className="size-4 fill-white dark:fill-black" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||||
|
<path d="M0 256C0 209.4 12.5 165.6 34.3 127.1L144.1 318.3C166 357.5 207.9 384 256 384C270.3 384 283.1 381.7 296.8 377.4L220.5 509.6C95.9 492.3 0 385.3 0 256zM365.1 321.6C377.4 302.4 384 279.1 384 256C384 217.8 367.2 183.5 340.7 160H493.4C505.4 189.6 512 222.1 512 256C512 397.4 397.4 511.1 256 512L365.1 321.6zM477.8 128H256C193.1 128 142.3 172.1 130.5 230.7L54.2 98.5C101 38.5 174 0 256 0C350.8 0 433.5 51.5 477.8 128V128zM168 256C168 207.4 207.4 168 256 168C304.6 168 344 207.4 344 256C344 304.6 304.6 344 256 344C207.4 344 168 304.6 168 256z"/>
|
||||||
|
</svg>
|
||||||
|
Get Chrome Extension
|
||||||
|
</span>
|
||||||
|
<span className="text-xs">from Chrome Web Store</span>
|
||||||
|
</SlidingButton>
|
||||||
|
<SlidingButton
|
||||||
|
slidingContent={
|
||||||
|
<div className="flex items-center justify-center gap-2 text-white dark:text-black">
|
||||||
|
<ArrowRight className="size-4" />
|
||||||
|
<span>Get Now</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
onClick={() => openLink('https://addons.mozilla.org/en-US/firefox/addon/neo-downloader-plus', 'firefox')}
|
||||||
|
>
|
||||||
|
<span className="font-semibold flex items-center gap-2">
|
||||||
|
<svg className="size-4 fill-white dark:fill-black" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||||
|
<path d="M130.2 127.5C130.4 127.6 130.3 127.6 130.2 127.5V127.5zM481.6 172.9C471 147.4 449.6 119.9 432.7 111.2C446.4 138.1 454.4 165 457.4 185.2C457.4 185.3 457.4 185.4 457.5 185.6C429.9 116.8 383.1 89.1 344.9 28.7C329.9 5.1 334 3.5 331.8 4.1L331.7 4.2C285 30.1 256.4 82.5 249.1 126.9C232.5 127.8 216.2 131.9 201.2 139C199.8 139.6 198.7 140.7 198.1 142C197.4 143.4 197.2 144.9 197.5 146.3C197.7 147.2 198.1 148 198.6 148.6C199.1 149.3 199.8 149.9 200.5 150.3C201.3 150.7 202.1 151 203 151.1C203.8 151.1 204.7 151 205.5 150.8L206 150.6C221.5 143.3 238.4 139.4 255.5 139.2C318.4 138.7 352.7 183.3 363.2 201.5C350.2 192.4 326.8 183.3 304.3 187.2C392.1 231.1 368.5 381.8 247 376.4C187.5 373.8 149.9 325.5 146.4 285.6C146.4 285.6 157.7 243.7 227 243.7C234.5 243.7 256 222.8 256.4 216.7C256.3 214.7 213.8 197.8 197.3 181.5C188.4 172.8 184.2 168.6 180.5 165.5C178.5 163.8 176.4 162.2 174.2 160.7C168.6 141.2 168.4 120.6 173.5 101.1C148.5 112.5 129 130.5 114.8 146.4H114.7C105 134.2 105.7 93.8 106.3 85.3C106.1 84.8 99 89 98.1 89.7C89.5 95.7 81.6 102.6 74.3 110.1C58 126.7 30.1 160.2 18.8 211.3C14.2 231.7 12 255.7 12 263.6C12 398.3 121.2 507.5 255.9 507.5C376.6 507.5 478.9 420.3 496.4 304.9C507.9 228.2 481.6 173.8 481.6 172.9z"/>
|
||||||
|
</svg>
|
||||||
|
Get Firefox Extension
|
||||||
|
</span>
|
||||||
|
<span className="text-xs">from Mozilla Addons Store</span>
|
||||||
|
</SlidingButton>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2 mb-4">
|
||||||
|
<Button variant="outline" onClick={() => openLink('https://chromewebstore.google.com/detail/neo-downloader-plus/mehopeailfjmiloiiohgicphlcgpompf', 'msedge')}>Edge</Button>
|
||||||
|
<Button variant="outline" onClick={() => openLink('https://chromewebstore.google.com/detail/neo-downloader-plus/mehopeailfjmiloiiohgicphlcgpompf', 'opera')}>Opera</Button>
|
||||||
|
<Button variant="outline" onClick={() => openLink('https://chromewebstore.google.com/detail/neo-downloader-plus/mehopeailfjmiloiiohgicphlcgpompf', 'brave')}>Brave</Button>
|
||||||
|
<Button variant="outline" onClick={() => openLink('https://chromewebstore.google.com/detail/neo-downloader-plus/mehopeailfjmiloiiohgicphlcgpompf', 'arc')}>Arc</Button>
|
||||||
|
<Button variant="outline" onClick={() => openLink('https://addons.mozilla.org/en-US/firefox/addon/neo-downloader-plus', 'zen')}>Zen</Button>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground mb-2">* These links opens with coresponding browsers only. Make sure the browser is installed before clicking the link</p>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent key="port" value="port" className="flex flex-col gap-4 min-h-[150px] max-w-[70%]">
|
||||||
<div className="websocket-port">
|
<div className="websocket-port">
|
||||||
<h3 className="font-semibold">Websocket Port</h3>
|
<h3 className="font-semibold">Websocket Port</h3>
|
||||||
<p className="text-sm text-muted-foreground mb-3">Change extension websocket server port</p>
|
<p className="text-xs text-muted-foreground mb-3">Change extension websocket server port</p>
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<Form {...websocketPortForm}>
|
<Form {...websocketPortForm}>
|
||||||
<form onSubmit={websocketPortForm.handleSubmit(handleWebsocketPortSubmit)} className="flex gap-4 w-full" autoComplete="off">
|
<form onSubmit={websocketPortForm.handleSubmit(handleWebsocketPortSubmit)} className="flex gap-4 w-full" autoComplete="off">
|
||||||
@@ -429,39 +882,11 @@ export default function SettingsPage() {
|
|||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</TabsContent>
|
||||||
</div>
|
</div>
|
||||||
|
</Tabs>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
<div className="flex flex-col">
|
|
||||||
<h3 className="font-semibold">Reset Settings</h3>
|
|
||||||
<p className="text-sm text-muted-foreground mb-3">Reset all setting to default</p>
|
|
||||||
<AlertDialog>
|
|
||||||
<AlertDialogTrigger asChild>
|
|
||||||
<Button
|
|
||||||
className="w-fit"
|
|
||||||
variant="destructive"
|
|
||||||
disabled={isUsingDefaultSettings}
|
|
||||||
>
|
|
||||||
<RotateCcw className="h-4 w-4" />
|
|
||||||
Reset Default
|
|
||||||
</Button>
|
|
||||||
</AlertDialogTrigger>
|
|
||||||
<AlertDialogContent>
|
|
||||||
<AlertDialogHeader>
|
|
||||||
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
|
||||||
<AlertDialogDescription>
|
|
||||||
This action cannot be undone! it will permanently reset all settings to default.
|
|
||||||
</AlertDialogDescription>
|
|
||||||
</AlertDialogHeader>
|
|
||||||
<AlertDialogFooter>
|
|
||||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
||||||
<AlertDialogAction onClick={
|
|
||||||
() => resetSettings()
|
|
||||||
}>Reset</AlertDialogAction>
|
|
||||||
</AlertDialogFooter>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialog>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Bell, Download, Settings, SquarePlay } from "lucide-react";
|
import { Download, Settings, SquarePlay } from "lucide-react";
|
||||||
import { RoutesObj } from "@/types/route";
|
import { RoutesObj } from "@/types/route";
|
||||||
|
|
||||||
export const AllRoutes: Array<RoutesObj> = [
|
export const AllRoutes: Array<RoutesObj> = [
|
||||||
@@ -16,10 +16,5 @@ export const AllRoutes: Array<RoutesObj> = [
|
|||||||
title: "Settings",
|
title: "Settings",
|
||||||
url: "/settings",
|
url: "/settings",
|
||||||
icon: Settings,
|
icon: Settings,
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Notifications",
|
|
||||||
url: "/notifications",
|
|
||||||
icon: Bell,
|
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { BasePathsStore, CurrentVideoMetadataStore, DownloadActionStatesStore, DownloaderPageStatesStore, DownloadStatesStore, KvPairsStatesStore, SettingsPageStatesStore } from '@/types/store';
|
import { BasePathsStore, CurrentVideoMetadataStore, DownloadActionStatesStore, DownloaderPageStatesStore, DownloadStatesStore, KvPairsStatesStore, LibraryPageStatesStore, SettingsPageStatesStore } from '@/types/store';
|
||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
|
|
||||||
export const useBasePathsStore = create<BasePathsStore>((set) => ({
|
export const useBasePathsStore = create<BasePathsStore>((set) => ({
|
||||||
@@ -34,22 +34,43 @@ export const useCurrentVideoMetadataStore = create<CurrentVideoMetadataStore>((s
|
|||||||
isMetadataLoading: false,
|
isMetadataLoading: false,
|
||||||
requestedUrl: '',
|
requestedUrl: '',
|
||||||
autoSubmitSearch: false,
|
autoSubmitSearch: false,
|
||||||
|
searchPid: null,
|
||||||
|
showSearchError: true,
|
||||||
setVideoUrl: (url) => set(() => ({ videoUrl: url })),
|
setVideoUrl: (url) => set(() => ({ videoUrl: url })),
|
||||||
setVideoMetadata: (metadata) => set(() => ({ videoMetadata: metadata })),
|
setVideoMetadata: (metadata) => set(() => ({ videoMetadata: metadata })),
|
||||||
setIsMetadataLoading: (isLoading) => set(() => ({ isMetadataLoading: isLoading })),
|
setIsMetadataLoading: (isLoading) => set(() => ({ isMetadataLoading: isLoading })),
|
||||||
setRequestedUrl: (url) => set(() => ({ requestedUrl: url })),
|
setRequestedUrl: (url) => set(() => ({ requestedUrl: url })),
|
||||||
setAutoSubmitSearch: (autoSubmit) => set(() => ({ autoSubmitSearch: autoSubmit })),
|
setAutoSubmitSearch: (autoSubmit) => set(() => ({ autoSubmitSearch: autoSubmit })),
|
||||||
|
setSearchPid: (pid) => set(() => ({ searchPid: pid })),
|
||||||
|
setShowSearchError: (showError) => set(() => ({ showSearchError: showError }))
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const useDownloaderPageStatesStore = create<DownloaderPageStatesStore>((set) => ({
|
export const useDownloaderPageStatesStore = create<DownloaderPageStatesStore>((set) => ({
|
||||||
|
activeDownloadModeTab: 'selective',
|
||||||
isStartingDownload: false,
|
isStartingDownload: false,
|
||||||
selctedDownloadFormat: 'best',
|
selectedDownloadFormat: 'best',
|
||||||
|
selectedCombinableVideoFormat: '',
|
||||||
|
selectedCombinableAudioFormat: '',
|
||||||
selectedSubtitles: [],
|
selectedSubtitles: [],
|
||||||
selectedPlaylistVideoIndex: '1',
|
selectedPlaylistVideoIndex: '1',
|
||||||
|
isErrored: false,
|
||||||
|
isErrorExpected: false,
|
||||||
|
erroredDownloadId: null,
|
||||||
|
setActiveDownloadModeTab: (tab) => set(() => ({ activeDownloadModeTab: tab })),
|
||||||
setIsStartingDownload: (isStarting) => set(() => ({ isStartingDownload: isStarting })),
|
setIsStartingDownload: (isStarting) => set(() => ({ isStartingDownload: isStarting })),
|
||||||
setSelctedDownloadFormat: (format) => set(() => ({ selctedDownloadFormat: format })),
|
setSelectedDownloadFormat: (format) => set(() => ({ selectedDownloadFormat: format })),
|
||||||
|
setSelectedCombinableVideoFormat: (format) => set(() => ({ selectedCombinableVideoFormat: format })),
|
||||||
|
setSelectedCombinableAudioFormat: (format) => set(() => ({ selectedCombinableAudioFormat: format })),
|
||||||
setSelectedSubtitles: (subtitles) => set(() => ({ selectedSubtitles: subtitles })),
|
setSelectedSubtitles: (subtitles) => set(() => ({ selectedSubtitles: subtitles })),
|
||||||
setSelectedPlaylistVideoIndex: (index) => set(() => ({ selectedPlaylistVideoIndex: index }))
|
setSelectedPlaylistVideoIndex: (index) => set(() => ({ selectedPlaylistVideoIndex: index })),
|
||||||
|
setIsErrored: (isErrored) => set(() => ({ isErrored: isErrored })),
|
||||||
|
setIsErrorExpected: (isErrorExpected) => set(() => ({ isErrorExpected: isErrorExpected })),
|
||||||
|
setErroredDownloadId: (downloadId) => set(() => ({ erroredDownloadId: downloadId })),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const useLibraryPageStatesStore = create<LibraryPageStatesStore>((set) => ({
|
||||||
|
activeTab: 'completed',
|
||||||
|
setActiveTab: (tab) => set(() => ({ activeTab: tab }))
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const useDownloadActionStatesStore = create<DownloadActionStatesStore>((set) => ({
|
export const useDownloadActionStatesStore = create<DownloadActionStatesStore>((set) => ({
|
||||||
@@ -93,7 +114,9 @@ export const useDownloadActionStatesStore = create<DownloadActionStatesStore>((s
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
export const useSettingsPageStatesStore = create<SettingsPageStatesStore>((set) => ({
|
export const useSettingsPageStatesStore = create<SettingsPageStatesStore>((set) => ({
|
||||||
activeTab: 'general',
|
activeTab: 'app',
|
||||||
|
activeSubAppTab: 'general',
|
||||||
|
activeSubExtTab: 'install',
|
||||||
appVersion: null,
|
appVersion: null,
|
||||||
isFetchingAppVersion: false,
|
isFetchingAppVersion: false,
|
||||||
ytDlpVersion: null,
|
ytDlpVersion: null,
|
||||||
@@ -105,9 +128,19 @@ export const useSettingsPageStatesStore = create<SettingsPageStatesStore>((set)
|
|||||||
theme: 'system',
|
theme: 'system',
|
||||||
download_dir: '',
|
download_dir: '',
|
||||||
prefer_video_over_playlist: true,
|
prefer_video_over_playlist: true,
|
||||||
|
strict_downloadablity_check: false,
|
||||||
max_parallel_downloads: 2,
|
max_parallel_downloads: 2,
|
||||||
|
max_retries: 5,
|
||||||
use_proxy: false,
|
use_proxy: false,
|
||||||
proxy_url: '',
|
proxy_url: '',
|
||||||
|
use_rate_limit: false,
|
||||||
|
rate_limit: 1048576, // 1 MB/s
|
||||||
|
video_format: 'auto',
|
||||||
|
audio_format: 'auto',
|
||||||
|
always_reencode_video: false,
|
||||||
|
embed_video_metadata: false,
|
||||||
|
embed_audio_metadata: true,
|
||||||
|
embed_audio_thumbnail: true,
|
||||||
websocket_port: 53511
|
websocket_port: 53511
|
||||||
},
|
},
|
||||||
isUsingDefaultSettings: true,
|
isUsingDefaultSettings: true,
|
||||||
@@ -118,6 +151,8 @@ export const useSettingsPageStatesStore = create<SettingsPageStatesStore>((set)
|
|||||||
isUpdatingApp: false,
|
isUpdatingApp: false,
|
||||||
appUpdateDownloadProgress: 0,
|
appUpdateDownloadProgress: 0,
|
||||||
setActiveTab: (tab) => set(() => ({ activeTab: tab })),
|
setActiveTab: (tab) => set(() => ({ activeTab: tab })),
|
||||||
|
setActiveSubAppTab: (tab) => set(() => ({ activeSubAppTab: tab })),
|
||||||
|
setActiveSubExtTab: (tab) => set(() => ({ activeSubExtTab: tab })),
|
||||||
setAppVersion: (version) => set(() => ({ appVersion: version })),
|
setAppVersion: (version) => set(() => ({ appVersion: version })),
|
||||||
setIsFetchingAppVersion: (isFetching) => set(() => ({ isFetchingAppVersion: isFetching })),
|
setIsFetchingAppVersion: (isFetching) => set(() => ({ isFetchingAppVersion: isFetching })),
|
||||||
setYtDlpVersion: (version) => set(() => ({ ytDlpVersion: version })),
|
setYtDlpVersion: (version) => set(() => ({ ytDlpVersion: version })),
|
||||||
@@ -137,9 +172,19 @@ export const useSettingsPageStatesStore = create<SettingsPageStatesStore>((set)
|
|||||||
theme: 'system',
|
theme: 'system',
|
||||||
download_dir: '',
|
download_dir: '',
|
||||||
prefer_video_over_playlist: true,
|
prefer_video_over_playlist: true,
|
||||||
|
strict_downloadablity_check: false,
|
||||||
max_parallel_downloads: 2,
|
max_parallel_downloads: 2,
|
||||||
|
max_retries: 5,
|
||||||
use_proxy: false,
|
use_proxy: false,
|
||||||
proxy_url: '',
|
proxy_url: '',
|
||||||
|
use_rate_limit: false,
|
||||||
|
rate_limit: 1048576, // 1 MB/s
|
||||||
|
video_format: 'auto',
|
||||||
|
audio_format: 'auto',
|
||||||
|
always_reencode_video: false,
|
||||||
|
embed_video_metadata: false,
|
||||||
|
embed_audio_metadata: true,
|
||||||
|
embed_audio_thumbnail: true,
|
||||||
websocket_port: 53511
|
websocket_port: 53511
|
||||||
},
|
},
|
||||||
isUsingDefaultSettings: true
|
isUsingDefaultSettings: true
|
||||||
|
|||||||
@@ -9,8 +9,19 @@ export interface Settings {
|
|||||||
theme: 'dark' | 'light' | 'system';
|
theme: 'dark' | 'light' | 'system';
|
||||||
download_dir: string;
|
download_dir: string;
|
||||||
max_parallel_downloads: number;
|
max_parallel_downloads: number;
|
||||||
|
max_retries: number;
|
||||||
prefer_video_over_playlist: boolean;
|
prefer_video_over_playlist: boolean;
|
||||||
|
strict_downloadablity_check: boolean;
|
||||||
use_proxy: boolean;
|
use_proxy: boolean;
|
||||||
proxy_url: string;
|
proxy_url: string;
|
||||||
|
use_rate_limit: boolean;
|
||||||
|
rate_limit: number;
|
||||||
|
video_format: string;
|
||||||
|
audio_format: string;
|
||||||
|
always_reencode_video: boolean;
|
||||||
|
embed_video_metadata: boolean;
|
||||||
|
embed_audio_metadata: boolean;
|
||||||
|
embed_audio_thumbnail: boolean;
|
||||||
|
// extension settings
|
||||||
websocket_port: number;
|
websocket_port: number;
|
||||||
}
|
}
|
||||||
@@ -23,22 +23,43 @@ export interface CurrentVideoMetadataStore {
|
|||||||
isMetadataLoading: boolean;
|
isMetadataLoading: boolean;
|
||||||
requestedUrl: string;
|
requestedUrl: string;
|
||||||
autoSubmitSearch: boolean;
|
autoSubmitSearch: boolean;
|
||||||
|
searchPid: number | null;
|
||||||
|
showSearchError: boolean;
|
||||||
setVideoUrl: (url: string) => void;
|
setVideoUrl: (url: string) => void;
|
||||||
setVideoMetadata: (metadata: RawVideoInfo | null) => void;
|
setVideoMetadata: (metadata: RawVideoInfo | null) => void;
|
||||||
setIsMetadataLoading: (isLoading: boolean) => void;
|
setIsMetadataLoading: (isLoading: boolean) => void;
|
||||||
setRequestedUrl: (url: string) => void;
|
setRequestedUrl: (url: string) => void;
|
||||||
setAutoSubmitSearch: (autoSubmit: boolean) => void;
|
setAutoSubmitSearch: (autoSubmit: boolean) => void;
|
||||||
|
setSearchPid: (pid: number | null) => void;
|
||||||
|
setShowSearchError: (showError: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DownloaderPageStatesStore {
|
export interface DownloaderPageStatesStore {
|
||||||
|
activeDownloadModeTab: string;
|
||||||
isStartingDownload: boolean;
|
isStartingDownload: boolean;
|
||||||
selctedDownloadFormat: string;
|
selectedDownloadFormat: string;
|
||||||
|
selectedCombinableVideoFormat: string;
|
||||||
|
selectedCombinableAudioFormat: string;
|
||||||
selectedSubtitles: string[];
|
selectedSubtitles: string[];
|
||||||
selectedPlaylistVideoIndex: string;
|
selectedPlaylistVideoIndex: string;
|
||||||
|
isErrored: boolean;
|
||||||
|
isErrorExpected: boolean;
|
||||||
|
erroredDownloadId: string | null;
|
||||||
|
setActiveDownloadModeTab: (tab: string) => void;
|
||||||
setIsStartingDownload: (isStarting: boolean) => void;
|
setIsStartingDownload: (isStarting: boolean) => void;
|
||||||
setSelctedDownloadFormat: (format: string) => void;
|
setSelectedDownloadFormat: (format: string) => void;
|
||||||
|
setSelectedCombinableVideoFormat: (format: string) => void;
|
||||||
|
setSelectedCombinableAudioFormat: (format: string) => void;
|
||||||
setSelectedSubtitles: (subtitles: string[]) => void;
|
setSelectedSubtitles: (subtitles: string[]) => void;
|
||||||
setSelectedPlaylistVideoIndex: (index: string) => void;
|
setSelectedPlaylistVideoIndex: (index: string) => void;
|
||||||
|
setIsErrored: (isErrored: boolean) => void;
|
||||||
|
setIsErrorExpected: (isErrorExpected: boolean) => void;
|
||||||
|
setErroredDownloadId: (downloadId: string | null) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LibraryPageStatesStore {
|
||||||
|
activeTab: string;
|
||||||
|
setActiveTab: (tab: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DownloadActionStatesStore {
|
export interface DownloadActionStatesStore {
|
||||||
@@ -58,6 +79,8 @@ export interface DownloadActionStatesStore {
|
|||||||
|
|
||||||
export interface SettingsPageStatesStore {
|
export interface SettingsPageStatesStore {
|
||||||
activeTab: string;
|
activeTab: string;
|
||||||
|
activeSubAppTab: string;
|
||||||
|
activeSubExtTab: string;
|
||||||
appVersion: string | null;
|
appVersion: string | null;
|
||||||
isFetchingAppVersion: boolean;
|
isFetchingAppVersion: boolean;
|
||||||
ytDlpVersion: string | null;
|
ytDlpVersion: string | null;
|
||||||
@@ -72,6 +95,8 @@ export interface SettingsPageStatesStore {
|
|||||||
isUpdatingApp: boolean;
|
isUpdatingApp: boolean;
|
||||||
appUpdateDownloadProgress: number;
|
appUpdateDownloadProgress: number;
|
||||||
setActiveTab: (tab: string) => void;
|
setActiveTab: (tab: string) => void;
|
||||||
|
setActiveSubAppTab: (tab: string) => void;
|
||||||
|
setActiveSubExtTab: (tab: string) => void;
|
||||||
setAppVersion: (version: string | null) => void;
|
setAppVersion: (version: string | null) => void;
|
||||||
setIsFetchingAppVersion: (isFetching: boolean) => void;
|
setIsFetchingAppVersion: (isFetching: boolean) => void;
|
||||||
setYtDlpVersion: (version: string | null) => void;
|
setYtDlpVersion: (version: string | null) => void;
|
||||||
|
|||||||
@@ -1,110 +0,0 @@
|
|||||||
/** @type {import('tailwindcss').Config} */
|
|
||||||
const plugin = require('tailwindcss/plugin');
|
|
||||||
export default {
|
|
||||||
darkMode: ["class"],
|
|
||||||
content: ["./src/**/*.{ts,tsx}"],
|
|
||||||
theme: {
|
|
||||||
extend: {
|
|
||||||
borderRadius: {
|
|
||||||
lg: 'var(--radius)',
|
|
||||||
md: 'calc(var(--radius) - 2px)',
|
|
||||||
sm: 'calc(var(--radius) - 4px)'
|
|
||||||
},
|
|
||||||
colors: {
|
|
||||||
background: 'hsl(var(--background))',
|
|
||||||
foreground: 'hsl(var(--foreground))',
|
|
||||||
card: {
|
|
||||||
DEFAULT: 'hsl(var(--card))',
|
|
||||||
foreground: 'hsl(var(--card-foreground))'
|
|
||||||
},
|
|
||||||
popover: {
|
|
||||||
DEFAULT: 'hsl(var(--popover))',
|
|
||||||
foreground: 'hsl(var(--popover-foreground))'
|
|
||||||
},
|
|
||||||
primary: {
|
|
||||||
DEFAULT: 'hsl(var(--primary))',
|
|
||||||
foreground: 'hsl(var(--primary-foreground))'
|
|
||||||
},
|
|
||||||
secondary: {
|
|
||||||
DEFAULT: 'hsl(var(--secondary))',
|
|
||||||
foreground: 'hsl(var(--secondary-foreground))'
|
|
||||||
},
|
|
||||||
muted: {
|
|
||||||
DEFAULT: 'hsl(var(--muted))',
|
|
||||||
foreground: 'hsl(var(--muted-foreground))'
|
|
||||||
},
|
|
||||||
accent: {
|
|
||||||
DEFAULT: 'hsl(var(--accent))',
|
|
||||||
foreground: 'hsl(var(--accent-foreground))'
|
|
||||||
},
|
|
||||||
destructive: {
|
|
||||||
DEFAULT: 'hsl(var(--destructive))',
|
|
||||||
foreground: 'hsl(var(--destructive-foreground))'
|
|
||||||
},
|
|
||||||
border: 'hsl(var(--border))',
|
|
||||||
input: 'hsl(var(--input))',
|
|
||||||
ring: 'hsl(var(--ring))',
|
|
||||||
chart: {
|
|
||||||
'1': 'hsl(var(--chart-1))',
|
|
||||||
'2': 'hsl(var(--chart-2))',
|
|
||||||
'3': 'hsl(var(--chart-3))',
|
|
||||||
'4': 'hsl(var(--chart-4))',
|
|
||||||
'5': 'hsl(var(--chart-5))'
|
|
||||||
},
|
|
||||||
sidebar: {
|
|
||||||
DEFAULT: 'hsl(var(--sidebar-background))',
|
|
||||||
foreground: 'hsl(var(--sidebar-foreground))',
|
|
||||||
primary: 'hsl(var(--sidebar-primary))',
|
|
||||||
'primary-foreground': 'hsl(var(--sidebar-primary-foreground))',
|
|
||||||
accent: 'hsl(var(--sidebar-accent))',
|
|
||||||
'accent-foreground': 'hsl(var(--sidebar-accent-foreground))',
|
|
||||||
border: 'hsl(var(--sidebar-border))',
|
|
||||||
ring: 'hsl(var(--sidebar-ring))'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
keyframes: {
|
|
||||||
'accordion-down': {
|
|
||||||
from: {
|
|
||||||
height: '0'
|
|
||||||
},
|
|
||||||
to: {
|
|
||||||
height: 'var(--radix-accordion-content-height)'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'accordion-up': {
|
|
||||||
from: {
|
|
||||||
height: 'var(--radix-accordion-content-height)'
|
|
||||||
},
|
|
||||||
to: {
|
|
||||||
height: '0'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'indeterminate-progress': {
|
|
||||||
'0%': { transform: "translateX(0) scaleX(0)" },
|
|
||||||
'40%': { transform: "translateX(0) scaleX(0.4)" },
|
|
||||||
'100%': { transform: "translateX(100%) scaleX(0.5)" },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
animation: {
|
|
||||||
'accordion-down': 'accordion-down 0.2s ease-out',
|
|
||||||
'accordion-up': 'accordion-up 0.2s ease-out',
|
|
||||||
'indeterminate-progress': "indeterminate-progress 1s infinite linear",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
require("tailwindcss-animate"),
|
|
||||||
plugin(function({ addUtilities }) {
|
|
||||||
addUtilities({
|
|
||||||
'.no-scrollbar::-webkit-scrollbar': {
|
|
||||||
'display': 'none',
|
|
||||||
},
|
|
||||||
'.no-scrollbar': {
|
|
||||||
'-ms-overflow-style': 'none',
|
|
||||||
'scrollbar-width': 'none',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
})
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,14 +1,17 @@
|
|||||||
import path from "path"
|
import path from "path"
|
||||||
import { defineConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
import react from "@vitejs/plugin-react";
|
import react from "@vitejs/plugin-react";
|
||||||
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
|
|
||||||
// @ts-expect-error process is a nodejs global
|
// @ts-expect-error process is a nodejs global
|
||||||
const host = process.env.TAURI_DEV_HOST;
|
const host = process.env.TAURI_DEV_HOST;
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig(async () => ({
|
export default defineConfig(async () => ({
|
||||||
plugins: [react()],
|
plugins: [react(), tailwindcss()],
|
||||||
|
build: {
|
||||||
|
chunkSizeWarningLimit: 1024, // 1MB
|
||||||
|
},
|
||||||
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
|
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
|
||||||
//
|
//
|
||||||
// 1. prevent vite from obscuring rust errors
|
// 1. prevent vite from obscuring rust errors
|
||||||
|
|||||||
Reference in New Issue
Block a user