mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-04-11 10:23:27 +00:00
chore: initial Untitled UI Vite scaffold with FontAwesome Pro
This commit is contained in:
1
.github/last-sync-commit
vendored
Normal file
1
.github/last-sync-commit
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
3cdabc0b462049086ebc0f954fc661dc4ca2a594
|
||||||
73
.github/workflows/README.md
vendored
Normal file
73
.github/workflows/README.md
vendored
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
# Component sync workflow
|
||||||
|
|
||||||
|
This workflow synchronizes components from the main [untitleduico/react](https://github.com/untitleduico/react) repository with intelligent commit tracking.
|
||||||
|
|
||||||
|
## How to use
|
||||||
|
|
||||||
|
1. Go to the "Actions" tab in your GitHub repository
|
||||||
|
2. Select "Sync Components from Main Repository"
|
||||||
|
3. Click "Run workflow"
|
||||||
|
4. Configure options:
|
||||||
|
- **Directories**: Which directories to sync (default: `components,hooks,utils,styles`)
|
||||||
|
- **Sync mode**: Choose between `all` or `existing-only`
|
||||||
|
5. Click "Run workflow" to start the sync
|
||||||
|
|
||||||
|
## What it does
|
||||||
|
|
||||||
|
1. **Fetches** the latest commit information from the main repository
|
||||||
|
2. **Tracks** previously synced commits to show what's new
|
||||||
|
3. **Creates** a branch named `sync/YYYY-MM-DD-[commit-hash]`
|
||||||
|
4. **Processes** files by removing `"use client";` directives
|
||||||
|
5. **Generates** a PR with commit history and detailed diff summaries
|
||||||
|
6. **Stores** the sync state for future runs
|
||||||
|
|
||||||
|
## Sync modes
|
||||||
|
|
||||||
|
### `all` (default)
|
||||||
|
- Syncs all files from the main repository
|
||||||
|
- Adds new files and updates existing ones
|
||||||
|
- Creates complete mirror of main repo structure
|
||||||
|
|
||||||
|
### `existing-only`
|
||||||
|
- Only updates files that already exist in your repository
|
||||||
|
- Skips new files from the main repository
|
||||||
|
- Useful for maintaining a subset of components
|
||||||
|
|
||||||
|
## Smart commit tracking
|
||||||
|
|
||||||
|
- **First run**: Shows "Initial sync" with latest commit details
|
||||||
|
- **Subsequent runs**: Shows commit range since last sync
|
||||||
|
- **Up-to-date**: Indicates when no new commits are available
|
||||||
|
- **Commit history**: Collapsible list of all commits since last sync
|
||||||
|
|
||||||
|
## Branch naming
|
||||||
|
|
||||||
|
Branches follow the pattern: `sync/YYYY-MM-DD-[short-commit-hash]`
|
||||||
|
- Example: `sync/2024-03-15-a1b2c3d`
|
||||||
|
|
||||||
|
## PR format
|
||||||
|
|
||||||
|
The generated PR includes:
|
||||||
|
|
||||||
|
### Title
|
||||||
|
- **New commits**: `🔄 Sync components (X commits) → a1b2c3d`
|
||||||
|
- **First sync**: `🎉 Initial sync from main repository → a1b2c3d`
|
||||||
|
|
||||||
|
### Content
|
||||||
|
- **Commits since last sync**: Collapsible commit history with links
|
||||||
|
- **Changed files**: Each file with collapsible diff view
|
||||||
|
- **Sync details**: Source repo, latest commit, date, directories synced
|
||||||
|
- **Automated processing**: Notes about `"use client"` removal
|
||||||
|
|
||||||
|
## State management
|
||||||
|
|
||||||
|
The workflow stores sync state in `.github/last-sync-commit` to track:
|
||||||
|
- Last successfully synced commit hash
|
||||||
|
- Enables incremental sync reporting
|
||||||
|
- Shows commit ranges in PR descriptions
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Repository must have `contents: write` and `pull-requests: write` permissions
|
||||||
|
- Uses `GITHUB_TOKEN` (automatically available in GitHub Actions)
|
||||||
|
- GitHub CLI (`gh`) for PR creation
|
||||||
380
.github/workflows/sync-components.yml
vendored
Normal file
380
.github/workflows/sync-components.yml
vendored
Normal file
@@ -0,0 +1,380 @@
|
|||||||
|
name: Sync Components from Main Repository
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
sync_directories:
|
||||||
|
description: "Directories to sync (comma-separated)"
|
||||||
|
required: false
|
||||||
|
default: "components,hooks,utils,styles"
|
||||||
|
type: string
|
||||||
|
sync_mode:
|
||||||
|
description: "Sync mode"
|
||||||
|
required: false
|
||||||
|
default: "all"
|
||||||
|
type: choice
|
||||||
|
options:
|
||||||
|
- all
|
||||||
|
- existing-only
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
sync-components:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "18"
|
||||||
|
|
||||||
|
- name: Configure git
|
||||||
|
run: |
|
||||||
|
git config --global user.name "github-actions[bot]"
|
||||||
|
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
|
|
||||||
|
- name: Fetch main repository and get commit info
|
||||||
|
id: fetch-main-repo
|
||||||
|
run: |
|
||||||
|
echo "Fetching main repository..."
|
||||||
|
|
||||||
|
# Create a temporary directory for cloning
|
||||||
|
mkdir -p /tmp/main-repo
|
||||||
|
cd /tmp/main-repo
|
||||||
|
|
||||||
|
# Clone the main repository
|
||||||
|
git clone https://github.com/untitleduico/react.git .
|
||||||
|
|
||||||
|
# Get latest commit info
|
||||||
|
LATEST_COMMIT_HASH=$(git rev-parse HEAD)
|
||||||
|
LATEST_COMMIT_SHORT=$(git rev-parse --short HEAD)
|
||||||
|
LATEST_COMMIT_MESSAGE=$(git log -1 --pretty=format:"%s")
|
||||||
|
LATEST_COMMIT_AUTHOR=$(git log -1 --pretty=format:"%an")
|
||||||
|
LATEST_COMMIT_DATE=$(git log -1 --pretty=format:"%ci")
|
||||||
|
|
||||||
|
echo "LATEST_COMMIT_HASH=$LATEST_COMMIT_HASH" >> $GITHUB_ENV
|
||||||
|
echo "LATEST_COMMIT_SHORT=$LATEST_COMMIT_SHORT" >> $GITHUB_ENV
|
||||||
|
echo "LATEST_COMMIT_MESSAGE=$LATEST_COMMIT_MESSAGE" >> $GITHUB_ENV
|
||||||
|
echo "LATEST_COMMIT_AUTHOR=$LATEST_COMMIT_AUTHOR" >> $GITHUB_ENV
|
||||||
|
echo "LATEST_COMMIT_DATE=$LATEST_COMMIT_DATE" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
echo "Latest commit: $LATEST_COMMIT_SHORT - $LATEST_COMMIT_MESSAGE"
|
||||||
|
|
||||||
|
# Get the directory structure (main repo has directories at root level, not in src/)
|
||||||
|
# Exclude demo and story files
|
||||||
|
find components hooks utils styles -type f ! -name "*.demo.tsx" ! -name "*.story.tsx" ! -name "*.stories.tsx" 2>/dev/null | sort > /tmp/main-repo-files.txt || echo "Some directories may not exist"
|
||||||
|
|
||||||
|
echo "Files to sync:"
|
||||||
|
cat /tmp/main-repo-files.txt
|
||||||
|
|
||||||
|
- name: Get last synced commit
|
||||||
|
id: get-last-sync
|
||||||
|
run: |
|
||||||
|
cd $GITHUB_WORKSPACE
|
||||||
|
|
||||||
|
# Check if we have a previous sync record
|
||||||
|
if [ -f ".github/last-sync-commit" ] && [ -s ".github/last-sync-commit" ]; then
|
||||||
|
# Read the first line and trim whitespace, ignore comments
|
||||||
|
LAST_SYNC_COMMIT=$(head -n 1 .github/last-sync-commit | xargs | grep -v '^#' || echo "")
|
||||||
|
|
||||||
|
if [ -n "$LAST_SYNC_COMMIT" ] && [ "$LAST_SYNC_COMMIT" != "" ]; then
|
||||||
|
echo "LAST_SYNC_COMMIT=$LAST_SYNC_COMMIT" >> $GITHUB_ENV
|
||||||
|
echo "Found previous sync commit: $LAST_SYNC_COMMIT"
|
||||||
|
else
|
||||||
|
echo "LAST_SYNC_COMMIT=" >> $GITHUB_ENV
|
||||||
|
echo "Sync file exists but no valid commit hash found"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "LAST_SYNC_COMMIT=" >> $GITHUB_ENV
|
||||||
|
echo "No previous sync record found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Get commits since last sync
|
||||||
|
id: get-commits-since-sync
|
||||||
|
run: |
|
||||||
|
cd /tmp/main-repo
|
||||||
|
|
||||||
|
if [ -n "$LAST_SYNC_COMMIT" ]; then
|
||||||
|
echo "Getting commits since $LAST_SYNC_COMMIT..."
|
||||||
|
|
||||||
|
# Get commit log since last sync
|
||||||
|
git log --oneline "$LAST_SYNC_COMMIT".."$LATEST_COMMIT_HASH" > /tmp/commits-since-sync.txt || echo "Could not get commit range"
|
||||||
|
|
||||||
|
# Count commits
|
||||||
|
COMMIT_COUNT=$(wc -l < /tmp/commits-since-sync.txt)
|
||||||
|
echo "COMMIT_COUNT=$COMMIT_COUNT" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
echo "Commits since last sync ($COMMIT_COUNT):"
|
||||||
|
cat /tmp/commits-since-sync.txt
|
||||||
|
else
|
||||||
|
echo "COMMIT_COUNT=unknown" >> $GITHUB_ENV
|
||||||
|
echo "No previous sync - this is the first sync"
|
||||||
|
echo "First sync - syncing from latest commit" > /tmp/commits-since-sync.txt
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Create sync branch
|
||||||
|
run: |
|
||||||
|
cd $GITHUB_WORKSPACE
|
||||||
|
BRANCH_NAME="sync/$(date +%Y-%m-%d-%H%M%S)-$LATEST_COMMIT_SHORT"
|
||||||
|
echo "BRANCH_NAME=$BRANCH_NAME" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
# Delete branch if it exists locally
|
||||||
|
git branch -D "$BRANCH_NAME" 2>/dev/null || true
|
||||||
|
|
||||||
|
# Delete branch if it exists remotely
|
||||||
|
git push origin --delete "$BRANCH_NAME" 2>/dev/null || true
|
||||||
|
|
||||||
|
# Create fresh branch
|
||||||
|
git checkout -b "$BRANCH_NAME"
|
||||||
|
echo "Created branch: $BRANCH_NAME"
|
||||||
|
|
||||||
|
- name: Sync files from main repository
|
||||||
|
run: |
|
||||||
|
cd /tmp/main-repo
|
||||||
|
|
||||||
|
# Read the sync directories from input
|
||||||
|
IFS=',' read -ra DIRS <<< "${{ github.event.inputs.sync_directories }}"
|
||||||
|
|
||||||
|
# Create sync summary file
|
||||||
|
echo "# Sync Summary" > /tmp/sync-summary.md
|
||||||
|
echo "" >> /tmp/sync-summary.md
|
||||||
|
|
||||||
|
for dir in "${DIRS[@]}"; do
|
||||||
|
dir=$(echo "$dir" | xargs) # trim whitespace
|
||||||
|
echo "## Syncing $dir directory" >> /tmp/sync-summary.md
|
||||||
|
|
||||||
|
# Main repo has directories at root level, target repo has them in src/
|
||||||
|
if [ -d "$dir" ]; then
|
||||||
|
# Create directory in target repo if it doesn't exist
|
||||||
|
mkdir -p "$GITHUB_WORKSPACE/src/$dir"
|
||||||
|
|
||||||
|
# Find all files in the directory from main repo, excluding demo and story files
|
||||||
|
find "$dir" -type f ! -name "*.demo.tsx" ! -name "*.story.tsx" ! -name "*.stories.tsx" | while read -r file; do
|
||||||
|
target_file="$GITHUB_WORKSPACE/src/$file"
|
||||||
|
|
||||||
|
# Check sync mode
|
||||||
|
if [ "${{ github.event.inputs.sync_mode }}" = "existing-only" ]; then
|
||||||
|
# Only process if target file already exists
|
||||||
|
if [ ! -f "$target_file" ]; then
|
||||||
|
echo "Skipping new file: $file (existing-only mode)"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Processing $file..."
|
||||||
|
|
||||||
|
# Copy file content and process it
|
||||||
|
content=$(cat "$file")
|
||||||
|
|
||||||
|
# Remove "use client" directive if present (including double newlines after it)
|
||||||
|
# This handles both double quotes and single quotes, with optional semicolon
|
||||||
|
processed_content=$(echo "$content" | sed -E '
|
||||||
|
/^"use client";?\s*$/{
|
||||||
|
N
|
||||||
|
N
|
||||||
|
s/"use client";?\s*\n\s*\n//
|
||||||
|
}
|
||||||
|
/^'\''use client'\'';?\s*$/{
|
||||||
|
N
|
||||||
|
N
|
||||||
|
s/'\''use client'\'';?\s*\n\s*\n//
|
||||||
|
}
|
||||||
|
')
|
||||||
|
|
||||||
|
# Target file goes in src/ directory
|
||||||
|
target_dir=$(dirname "$target_file")
|
||||||
|
mkdir -p "$target_dir"
|
||||||
|
|
||||||
|
# Write processed content to target file
|
||||||
|
echo "$processed_content" > "$target_file"
|
||||||
|
|
||||||
|
echo "- src/$file" >> /tmp/sync-summary.md
|
||||||
|
done
|
||||||
|
|
||||||
|
# Log skipped files for transparency
|
||||||
|
DEMO_STORY_SKIPPED=$(find "$dir" -type f \( -name "*.demo.tsx" -o -name "*.story.tsx" -o -name "*.stories.tsx" \) | wc -l)
|
||||||
|
|
||||||
|
if [ "${{ github.event.inputs.sync_mode }}" = "existing-only" ]; then
|
||||||
|
# Count new files that were skipped
|
||||||
|
NEW_FILES_SKIPPED=0
|
||||||
|
find "$dir" -type f ! -name "*.demo.tsx" ! -name "*.story.tsx" ! -name "*.stories.tsx" | while read -r file; do
|
||||||
|
target_file="$GITHUB_WORKSPACE/src/$file"
|
||||||
|
if [ ! -f "$target_file" ]; then
|
||||||
|
NEW_FILES_SKIPPED=$((NEW_FILES_SKIPPED + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$NEW_FILES_SKIPPED" -gt 0 ]; then
|
||||||
|
echo "Skipped $NEW_FILES_SKIPPED new files (existing-only mode)"
|
||||||
|
echo "- Skipped $NEW_FILES_SKIPPED new files (existing-only mode)" >> /tmp/sync-summary.md
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$DEMO_STORY_SKIPPED" -gt 0 ]; then
|
||||||
|
echo "Skipped $DEMO_STORY_SKIPPED demo/story files"
|
||||||
|
echo "- Skipped $DEMO_STORY_SKIPPED demo/story files" >> /tmp/sync-summary.md
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "Directory $dir not found in main repository" >> /tmp/sync-summary.md
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: Check for changes
|
||||||
|
id: check-changes
|
||||||
|
run: |
|
||||||
|
cd $GITHUB_WORKSPACE
|
||||||
|
if git diff --quiet; then
|
||||||
|
echo "changes=false" >> $GITHUB_OUTPUT
|
||||||
|
echo "No changes detected"
|
||||||
|
else
|
||||||
|
echo "changes=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "Changes detected"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Generate detailed diff summary
|
||||||
|
if: steps.check-changes.outputs.changes == 'true'
|
||||||
|
run: |
|
||||||
|
cd $GITHUB_WORKSPACE
|
||||||
|
|
||||||
|
echo "# Component sync changes" > /tmp/pr-description.md
|
||||||
|
echo "" >> /tmp/pr-description.md
|
||||||
|
echo "This PR synchronizes components from the main repository [untitleduico/react](https://github.com/untitleduico/react)." >> /tmp/pr-description.md
|
||||||
|
echo "" >> /tmp/pr-description.md
|
||||||
|
|
||||||
|
# Add commits since last sync section
|
||||||
|
echo "## 📝 Commits since last sync" >> /tmp/pr-description.md
|
||||||
|
echo "" >> /tmp/pr-description.md
|
||||||
|
if [ -n "$LAST_SYNC_COMMIT" ] && [ "$COMMIT_COUNT" != "unknown" ]; then
|
||||||
|
if [ "$COMMIT_COUNT" -gt 0 ]; then
|
||||||
|
echo "**$COMMIT_COUNT new commits** from [\`$LAST_SYNC_COMMIT\`](https://github.com/untitleduico/react/commit/$LAST_SYNC_COMMIT) to [\`$LATEST_COMMIT_SHORT\`](https://github.com/untitleduico/react/commit/$LATEST_COMMIT_HASH):" >> /tmp/pr-description.md
|
||||||
|
echo "" >> /tmp/pr-description.md
|
||||||
|
echo "<details>" >> /tmp/pr-description.md
|
||||||
|
echo "<summary>View commit history</summary>" >> /tmp/pr-description.md
|
||||||
|
echo "" >> /tmp/pr-description.md
|
||||||
|
while IFS= read -r commit_line; do
|
||||||
|
if [ -n "$commit_line" ]; then
|
||||||
|
commit_hash=$(echo "$commit_line" | cut -d' ' -f1)
|
||||||
|
commit_msg=$(echo "$commit_line" | cut -d' ' -f2-)
|
||||||
|
echo "- [\`$commit_hash\`](https://github.com/untitleduico/react/commit/$commit_hash) $commit_msg" >> /tmp/pr-description.md
|
||||||
|
fi
|
||||||
|
done < /tmp/commits-since-sync.txt
|
||||||
|
echo "" >> /tmp/pr-description.md
|
||||||
|
echo "</details>" >> /tmp/pr-description.md
|
||||||
|
else
|
||||||
|
echo "✅ **Repository is up to date** - no new commits since last sync [\`$LAST_SYNC_COMMIT\`](https://github.com/untitleduico/react/commit/$LAST_SYNC_COMMIT)" >> /tmp/pr-description.md
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "🎉 **First sync** - syncing from latest commit [\`$LATEST_COMMIT_SHORT\`](https://github.com/untitleduico/react/commit/$LATEST_COMMIT_HASH)" >> /tmp/pr-description.md
|
||||||
|
echo "" >> /tmp/pr-description.md
|
||||||
|
echo "**Latest commit**: $LATEST_COMMIT_MESSAGE" >> /tmp/pr-description.md
|
||||||
|
echo "**Author**: $LATEST_COMMIT_AUTHOR" >> /tmp/pr-description.md
|
||||||
|
echo "**Date**: $LATEST_COMMIT_DATE" >> /tmp/pr-description.md
|
||||||
|
fi
|
||||||
|
echo "" >> /tmp/pr-description.md
|
||||||
|
|
||||||
|
echo "## 📁 Modified files" >> /tmp/pr-description.md
|
||||||
|
echo "" >> /tmp/pr-description.md
|
||||||
|
|
||||||
|
# Get file statistics
|
||||||
|
TOTAL_FILES=$(git diff --name-only | wc -l)
|
||||||
|
echo "**$TOTAL_FILES files modified**" >> /tmp/pr-description.md
|
||||||
|
echo "" >> /tmp/pr-description.md
|
||||||
|
|
||||||
|
# List all changed files
|
||||||
|
git diff --name-only | while read -r file; do
|
||||||
|
if [ -n "$file" ]; then
|
||||||
|
echo "- \`$file\`" >> /tmp/pr-description.md
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
echo "" >> /tmp/pr-description.md
|
||||||
|
|
||||||
|
echo "## 🔧 Sync details" >> /tmp/pr-description.md
|
||||||
|
echo "" >> /tmp/pr-description.md
|
||||||
|
echo "- **Source repository**: [untitleduico/react](https://github.com/untitleduico/react)" >> /tmp/pr-description.md
|
||||||
|
echo "- **Latest commit**: [\`$LATEST_COMMIT_SHORT\`](https://github.com/untitleduico/react/commit/$LATEST_COMMIT_HASH)" >> /tmp/pr-description.md
|
||||||
|
echo "- **Sync date**: $(date -u '+%Y-%m-%d %H:%M:%S UTC')" >> /tmp/pr-description.md
|
||||||
|
echo "- **Directories synced**: ${{ github.event.inputs.sync_directories }}" >> /tmp/pr-description.md
|
||||||
|
echo "- **Sync mode**: ${{ github.event.inputs.sync_mode }}" >> /tmp/pr-description.md
|
||||||
|
echo "- **Automated processing**: Removed \`\"use client\";\` directives, excluded demo/story files" >> /tmp/pr-description.md
|
||||||
|
echo "" >> /tmp/pr-description.md
|
||||||
|
echo "---" >> /tmp/pr-description.md
|
||||||
|
echo "*This PR was automatically generated by the sync-components workflow.*" >> /tmp/pr-description.md
|
||||||
|
|
||||||
|
- name: Store sync commit record
|
||||||
|
if: steps.check-changes.outputs.changes == 'true'
|
||||||
|
run: |
|
||||||
|
cd $GITHUB_WORKSPACE
|
||||||
|
|
||||||
|
# Create .github directory if it doesn't exist
|
||||||
|
mkdir -p .github
|
||||||
|
|
||||||
|
# Store the latest commit hash for next sync
|
||||||
|
echo "$LATEST_COMMIT_HASH" > .github/last-sync-commit
|
||||||
|
|
||||||
|
echo "Stored sync commit: $LATEST_COMMIT_HASH"
|
||||||
|
|
||||||
|
- name: Commit changes
|
||||||
|
if: steps.check-changes.outputs.changes == 'true'
|
||||||
|
run: |
|
||||||
|
cd $GITHUB_WORKSPACE
|
||||||
|
git add .
|
||||||
|
|
||||||
|
# Create commit message
|
||||||
|
if [ -n "$LAST_SYNC_COMMIT" ] && [ "$COMMIT_COUNT" != "unknown" ] && [ "$COMMIT_COUNT" -gt 0 ]; then
|
||||||
|
COMMIT_MSG="sync: Update components from main repository ($COMMIT_COUNT commits)
|
||||||
|
|
||||||
|
Synced from: $LAST_SYNC_COMMIT..$LATEST_COMMIT_SHORT
|
||||||
|
Latest commit: $LATEST_COMMIT_MESSAGE
|
||||||
|
Directories: ${{ github.event.inputs.sync_directories }}
|
||||||
|
Source: https://github.com/untitleduico/react/commit/$LATEST_COMMIT_HASH
|
||||||
|
Automated processing: Removed 'use client' directives"
|
||||||
|
else
|
||||||
|
COMMIT_MSG="sync: Initial sync from main repository
|
||||||
|
|
||||||
|
Latest commit: $LATEST_COMMIT_SHORT - $LATEST_COMMIT_MESSAGE
|
||||||
|
Author: $LATEST_COMMIT_AUTHOR
|
||||||
|
Directories: ${{ github.event.inputs.sync_directories }}
|
||||||
|
Source: https://github.com/untitleduico/react/commit/$LATEST_COMMIT_HASH
|
||||||
|
Automated processing: Removed 'use client' directives"
|
||||||
|
fi
|
||||||
|
|
||||||
|
git commit -m "$COMMIT_MSG"
|
||||||
|
|
||||||
|
- name: Push changes and create PR
|
||||||
|
if: steps.check-changes.outputs.changes == 'true'
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
cd $GITHUB_WORKSPACE
|
||||||
|
git push origin "$BRANCH_NAME"
|
||||||
|
|
||||||
|
# Create PR title based on sync type
|
||||||
|
if [ -n "$LAST_SYNC_COMMIT" ] && [ "$COMMIT_COUNT" != "unknown" ] && [ "$COMMIT_COUNT" -gt 0 ]; then
|
||||||
|
PR_TITLE="🔄 Sync components ($COMMIT_COUNT commits) → $LATEST_COMMIT_SHORT"
|
||||||
|
else
|
||||||
|
PR_TITLE="🎉 Initial sync from main repository → $LATEST_COMMIT_SHORT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create PR using GitHub CLI
|
||||||
|
gh pr create \
|
||||||
|
--title "$PR_TITLE" \
|
||||||
|
--body-file /tmp/pr-description.md \
|
||||||
|
--head "$BRANCH_NAME" \
|
||||||
|
--base main
|
||||||
|
|
||||||
|
- name: Output results
|
||||||
|
run: |
|
||||||
|
if [ "${{ steps.check-changes.outputs.changes }}" == "true" ]; then
|
||||||
|
echo "✅ Sync completed successfully. PR created."
|
||||||
|
echo "Branch: $BRANCH_NAME"
|
||||||
|
else
|
||||||
|
echo "ℹ️ No changes detected. Repository is already in sync."
|
||||||
|
fi
|
||||||
25
.gitignore
vendored
Normal file
25
.gitignore
vendored
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
.env
|
||||||
3
.npmrc
Normal file
3
.npmrc
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
@awesome.me:registry=https://npm.fontawesome.com/
|
||||||
|
@fortawesome:registry=https://npm.fontawesome.com/
|
||||||
|
//npm.fontawesome.com/:_authToken=B9FAA215-4936-4B13-BEAA-FA58F042FA1A
|
||||||
19
.prettierrc
Normal file
19
.prettierrc
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"printWidth": 160,
|
||||||
|
"tabWidth": 4,
|
||||||
|
"plugins": [
|
||||||
|
"@trivago/prettier-plugin-sort-imports",
|
||||||
|
"prettier-plugin-tailwindcss"
|
||||||
|
],
|
||||||
|
"tailwindFunctions": ["sortCx", "cx"],
|
||||||
|
"importOrder": [
|
||||||
|
"^react$",
|
||||||
|
"^react-dom$",
|
||||||
|
"^(?!react$|react-dom$|@/|\\.).*",
|
||||||
|
"^@/.*",
|
||||||
|
"^\\.{1,2}/.*"
|
||||||
|
],
|
||||||
|
"importOrderSeparation": false,
|
||||||
|
"importOrderSortSpecifiers": true,
|
||||||
|
"tailwindStylesheet": "./src/styles/globals.css"
|
||||||
|
}
|
||||||
811
CLAUDE.md
Normal file
811
CLAUDE.md
Normal file
@@ -0,0 +1,811 @@
|
|||||||
|
## Project Overview
|
||||||
|
|
||||||
|
This is an **Untitled UI React** component library project built with:
|
||||||
|
|
||||||
|
- **React 19.1.1** with TypeScript
|
||||||
|
- **Tailwind CSS v4.1** for styling
|
||||||
|
- **React Aria Components** as the foundation for accessibility and behavior
|
||||||
|
|
||||||
|
## Key Architecture Principles
|
||||||
|
|
||||||
|
### Component Foundation
|
||||||
|
|
||||||
|
- All components are built on **React Aria Components** for consistent accessibility and behavior
|
||||||
|
- Components follow the compound component pattern with sub-components (e.g., `Select.Item`, `Select.ComboBox`)
|
||||||
|
- TypeScript is used throughout for type safety
|
||||||
|
|
||||||
|
### Import Naming Convention
|
||||||
|
|
||||||
|
**CRITICAL**: All imports from `react-aria-components` must be prefixed with `Aria*` for clarity and consistency:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ Correct
|
||||||
|
import { Button as AriaButton, TextField as AriaTextField } from "react-aria-components";
|
||||||
|
// ❌ Incorrect
|
||||||
|
import { Button, TextField } from "react-aria-components";
|
||||||
|
```
|
||||||
|
|
||||||
|
This convention:
|
||||||
|
|
||||||
|
- Prevents naming conflicts with custom components
|
||||||
|
- Makes it clear when using base React Aria components
|
||||||
|
- Maintains consistency across the entire codebase
|
||||||
|
|
||||||
|
### File Naming Convention
|
||||||
|
|
||||||
|
**IMPORTANT**: All files must be named in **kebab-case** for consistency:
|
||||||
|
|
||||||
|
```
|
||||||
|
✅ Correct:
|
||||||
|
- date-picker.tsx
|
||||||
|
- user-profile.tsx
|
||||||
|
- api-client.ts
|
||||||
|
- auth-context.tsx
|
||||||
|
|
||||||
|
❌ Incorrect:
|
||||||
|
- DatePicker.tsx
|
||||||
|
- userProfile.tsx
|
||||||
|
- apiClient.ts
|
||||||
|
- AuthContext.tsx
|
||||||
|
```
|
||||||
|
|
||||||
|
This applies to all file types including:
|
||||||
|
|
||||||
|
- Component files (.tsx, .jsx)
|
||||||
|
- TypeScript/JavaScript files (.ts, .js)
|
||||||
|
- Style files (.css, .scss)
|
||||||
|
- Test files (.test.ts, .spec.tsx)
|
||||||
|
- Configuration files (when creating new ones)
|
||||||
|
|
||||||
|
## Development Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Development
|
||||||
|
npm run dev # Start Vite development server (http://localhost:5173)
|
||||||
|
npm run build # Build for production (TypeScript compilation + Vite build)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
### Application Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── components/
|
||||||
|
│ ├── base/ # Core UI components (Button, Input, Select, etc.)
|
||||||
|
│ ├── application/ # Complex application components
|
||||||
|
│ ├── foundations/ # Design tokens and foundational elements
|
||||||
|
│ ├── marketing/ # Marketing-specific components
|
||||||
|
│ └── shared-assets/ # Reusable assets and illustrations
|
||||||
|
├── hooks/ # Custom React hooks
|
||||||
|
├── pages/ # Route components
|
||||||
|
├── providers/ # React context providers
|
||||||
|
├── styles/ # Global styles and theme
|
||||||
|
├── types/ # TypeScript type definitions
|
||||||
|
└── utils/ # Utility functions
|
||||||
|
```
|
||||||
|
|
||||||
|
### Component Patterns
|
||||||
|
|
||||||
|
#### 1. Base Components
|
||||||
|
|
||||||
|
Located in `components/base/`, these are the building blocks:
|
||||||
|
|
||||||
|
- `Button` - All button variants with loading states
|
||||||
|
- `Input` - Text inputs with validation and icons
|
||||||
|
- `Select` - Dropdown selections with complex options
|
||||||
|
- `Checkbox`, `Radio`, `Toggle` - Form controls
|
||||||
|
- `Avatar`, `Badge`, `Tooltip` - Display components
|
||||||
|
|
||||||
|
#### 2. Application Components
|
||||||
|
|
||||||
|
Located in `components/application/`, these are complex UI patterns:
|
||||||
|
|
||||||
|
- `DatePicker` - Calendar-based date selection
|
||||||
|
- `Modal` - Overlay dialogs
|
||||||
|
- `Pagination` - Data navigation
|
||||||
|
- `Table` - Data display with sorting
|
||||||
|
- `Tabs` - Content organization
|
||||||
|
|
||||||
|
#### 3. Styling Architecture
|
||||||
|
|
||||||
|
- Uses a `sortCx` utility for organized style objects
|
||||||
|
- Follows size variants: `sm`, `md`, `lg`, `xl`
|
||||||
|
- Color variants: `primary`, `secondary`, `tertiary`, `destructive`, etc.
|
||||||
|
- Responsive and state-aware styling with Tailwind
|
||||||
|
|
||||||
|
#### 4. Component Props Pattern
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface CommonProps {
|
||||||
|
size?: "sm" | "md" | "lg";
|
||||||
|
isDisabled?: boolean;
|
||||||
|
isLoading?: boolean;
|
||||||
|
// ... other common props
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ButtonProps extends CommonProps, HTMLButtonElement {
|
||||||
|
color?: "primary" | "secondary" | "tertiary";
|
||||||
|
iconLeading?: FC | ReactNode;
|
||||||
|
iconTrailing?: FC | ReactNode;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Styling Guidelines
|
||||||
|
|
||||||
|
### Tailwind CSS v4.1
|
||||||
|
|
||||||
|
- Uses the latest Tailwind CSS v4.1 features
|
||||||
|
- Custom design tokens defined in theme configuration
|
||||||
|
- Consistent spacing, colors, and typography scales
|
||||||
|
|
||||||
|
### Brand Color Customization
|
||||||
|
|
||||||
|
To change the main brand color across the entire application:
|
||||||
|
|
||||||
|
1. **Update Brand Color Variables**: Edit `src/styles/theme.css` and modify the `--color-brand-*` variables
|
||||||
|
2. **Maintain Color Scale**: Ensure you provide a complete color scale from 25 to 950 with proper contrast ratios
|
||||||
|
3. **Example Brand Color Scale**:
|
||||||
|
```css
|
||||||
|
--color-brand-25: rgb(252 250 255); /* Lightest tint */
|
||||||
|
--color-brand-50: rgb(249 245 255);
|
||||||
|
--color-brand-100: rgb(244 235 255);
|
||||||
|
--color-brand-200: rgb(233 215 254);
|
||||||
|
--color-brand-300: rgb(214 187 251);
|
||||||
|
--color-brand-400: rgb(182 146 246);
|
||||||
|
--color-brand-500: rgb(158 119 237); /* Base brand color */
|
||||||
|
--color-brand-600: rgb(127 86 217); /* Primary interactive color */
|
||||||
|
--color-brand-700: rgb(105 65 198);
|
||||||
|
--color-brand-800: rgb(83 56 158);
|
||||||
|
--color-brand-900: rgb(66 48 125);
|
||||||
|
--color-brand-950: rgb(44 28 95); /* Darkest shade */
|
||||||
|
```
|
||||||
|
|
||||||
|
The color scale automatically adapts to both light and dark modes through the CSS variable system.
|
||||||
|
|
||||||
|
### Style Organization
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export const styles = sortCx({
|
||||||
|
common: {
|
||||||
|
root: "base-classes-here",
|
||||||
|
icon: "icon-classes-here",
|
||||||
|
},
|
||||||
|
sizes: {
|
||||||
|
sm: { root: "small-size-classes" },
|
||||||
|
md: { root: "medium-size-classes" },
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
primary: { root: "primary-color-classes" },
|
||||||
|
secondary: { root: "secondary-color-classes" },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Utility Functions
|
||||||
|
|
||||||
|
- `cx()` - Class name utility (from `@/utils/cx`)
|
||||||
|
- `sortCx()` - Organized style objects
|
||||||
|
- `isReactComponent()` - Component type checking
|
||||||
|
|
||||||
|
## Icon Usage
|
||||||
|
|
||||||
|
### Available Libraries
|
||||||
|
|
||||||
|
- `@untitledui/icons` - 1,100+ line-style icons (free)
|
||||||
|
- `@untitledui/file-icons` - File type icons
|
||||||
|
- `@untitledui-pro/icons` - 4,600+ icons in 4 styles (Requires PRO access)
|
||||||
|
|
||||||
|
### Import & Usage
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Recommended: Named imports (tree-shakeable)
|
||||||
|
import { Home01, Settings01, ChevronDown } from "@untitledui/icons";
|
||||||
|
|
||||||
|
// Component props - pass as reference
|
||||||
|
<Button iconLeading={ChevronDown}>Options</Button>
|
||||||
|
|
||||||
|
// Standalone usage
|
||||||
|
<Home01 className="size-5 text-gray-600" />
|
||||||
|
|
||||||
|
// As JSX element - MUST include data-icon
|
||||||
|
<Button iconLeading={<ChevronDown data-icon className="size-4" />}>Options</Button>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Styling
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Size: use size-4 (16px), size-5 (20px), size-6 (24px)
|
||||||
|
<Home01 className="size-5" />
|
||||||
|
|
||||||
|
// Color: use semantic text colors
|
||||||
|
<Home01 className="size-5 text-brand-600" />
|
||||||
|
|
||||||
|
// Stroke width (line icons only)
|
||||||
|
<Home01 className="size-5" strokeWidth={2} />
|
||||||
|
|
||||||
|
// Accessibility: decorative icons need aria-hidden
|
||||||
|
<Home01 className="size-5" aria-hidden="true" />
|
||||||
|
```
|
||||||
|
|
||||||
|
### PRO Icon Styles
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Home01 } from "@untitledui-pro/icons";
|
||||||
|
// Line
|
||||||
|
import { Home01 } from "@untitledui-pro/icons/duocolor";
|
||||||
|
import { Home01 } from "@untitledui-pro/icons/duotone";
|
||||||
|
import { Home01 } from "@untitledui-pro/icons/solid";
|
||||||
|
```
|
||||||
|
|
||||||
|
## Form Handling
|
||||||
|
|
||||||
|
### Form Components
|
||||||
|
|
||||||
|
- `Input` - Text inputs with validation
|
||||||
|
- `Select` - Dropdown selections
|
||||||
|
- `Checkbox`, `Radio` - Selection controls
|
||||||
|
- `Textarea` - Multi-line text input
|
||||||
|
- `Form` - Form wrapper with validation
|
||||||
|
|
||||||
|
## Animation and Interactions
|
||||||
|
|
||||||
|
### Animation Libraries
|
||||||
|
|
||||||
|
- `motion` (Framer Motion) for complex animations
|
||||||
|
- `tailwindcss-animate` for utility-based animations
|
||||||
|
- CSS transitions for simple state changes
|
||||||
|
|
||||||
|
### CSS Transitions
|
||||||
|
|
||||||
|
For default small transition actions (hover states, color changes, etc.), use:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
className = "transition duration-100 ease-linear";
|
||||||
|
```
|
||||||
|
|
||||||
|
This provides a snappy 100ms linear transition that feels responsive without being jarring.
|
||||||
|
|
||||||
|
### Loading States
|
||||||
|
|
||||||
|
- Components support `isLoading` prop
|
||||||
|
- Built-in loading spinners
|
||||||
|
- Proper disabled states during loading
|
||||||
|
|
||||||
|
## Common Patterns
|
||||||
|
|
||||||
|
### Compound Components
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const Select = SelectComponent as typeof SelectComponent & {
|
||||||
|
Item: typeof SelectItem;
|
||||||
|
ComboBox: typeof ComboBox;
|
||||||
|
};
|
||||||
|
Select.Item = SelectItem;
|
||||||
|
Select.ComboBox = ComboBox;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Conditional Rendering
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
{label && <Label isRequired={isRequired}>{label}</Label>}
|
||||||
|
{hint && <HintText isInvalid={isInvalid}>{hint}</HintText>}
|
||||||
|
```
|
||||||
|
|
||||||
|
## State Management
|
||||||
|
|
||||||
|
### Component State
|
||||||
|
|
||||||
|
- Use React Aria's built-in state management
|
||||||
|
- Local state for component-specific data
|
||||||
|
- Context for shared component state (theme, router)
|
||||||
|
|
||||||
|
### Global State
|
||||||
|
|
||||||
|
- Theme context in `src/providers/theme.tsx`
|
||||||
|
- Router context in `src/providers/router-provider.tsx`
|
||||||
|
|
||||||
|
## Key Files and Utilities
|
||||||
|
|
||||||
|
### Core Utilities
|
||||||
|
|
||||||
|
- `src/utils/cx.ts` - Class name utilities
|
||||||
|
- `src/utils/is-react-component.ts` - Component type checking
|
||||||
|
- `src/hooks/` - Custom React hooks
|
||||||
|
|
||||||
|
### Style Configuration
|
||||||
|
|
||||||
|
- `src/styles/globals.css` - Global styles
|
||||||
|
- `src/styles/theme.css` - Theme definitions
|
||||||
|
- `src/styles/typography.css` - Typography styles
|
||||||
|
|
||||||
|
## Best Practices for AI Assistance
|
||||||
|
|
||||||
|
### When Adding New Components
|
||||||
|
|
||||||
|
1. Follow the existing component structure
|
||||||
|
2. Use React Aria Components as foundation
|
||||||
|
3. Implement proper TypeScript types
|
||||||
|
4. Add size and color variants where applicable
|
||||||
|
5. Include accessibility features
|
||||||
|
6. Follow the naming conventions
|
||||||
|
7. Add components to appropriate folders (`base/`, `application/`, etc.)
|
||||||
|
|
||||||
|
## Most Used Components Reference
|
||||||
|
|
||||||
|
### Button
|
||||||
|
|
||||||
|
The Button component is the most frequently used interactive element across the library.
|
||||||
|
|
||||||
|
**Import:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Button } from "@/components/base/buttons/button";
|
||||||
|
```
|
||||||
|
|
||||||
|
**Common Props:**
|
||||||
|
|
||||||
|
- `size`: `"sm" | "md" | "lg" | "xl"` - Button size (default: `"sm"`)
|
||||||
|
- `color`: `"primary" | "secondary" | "tertiary" | "link-gray" | "link-color" | "primary-destructive" | "secondary-destructive" | "tertiary-destructive" | "link-destructive"` - Button color variant (default: `"primary"`)
|
||||||
|
- `iconLeading`: `FC | ReactNode` - Icon or component to display before text
|
||||||
|
- `iconTrailing`: `FC | ReactNode` - Icon or component to display after text
|
||||||
|
- `isDisabled`: `boolean` - Disabled state
|
||||||
|
- `isLoading`: `boolean` - Loading state with spinner
|
||||||
|
- `showTextWhileLoading`: `boolean` - Keep text visible during loading
|
||||||
|
- `children`: `ReactNode` - Button content
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Basic button
|
||||||
|
<Button size="md">Save</Button>
|
||||||
|
|
||||||
|
// With leading icon
|
||||||
|
<Button iconLeading={Check} color="primary">Save</Button>
|
||||||
|
|
||||||
|
// Loading state
|
||||||
|
<Button isLoading showTextWhileLoading>Submitting...</Button>
|
||||||
|
|
||||||
|
// Destructive action
|
||||||
|
<Button color="primary-destructive" iconLeading={Trash02}>Delete</Button>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Input
|
||||||
|
|
||||||
|
Text input component with extensive customization options.
|
||||||
|
|
||||||
|
**Import:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Input } from "@/components/base/input/input";
|
||||||
|
import { InputGroup } from "@/components/base/input/input-group";
|
||||||
|
```
|
||||||
|
|
||||||
|
**Common Props:**
|
||||||
|
|
||||||
|
- `size`: `"sm" | "md"` - Input size (default: `"sm"`)
|
||||||
|
- `label`: `string` - Field label
|
||||||
|
- `placeholder`: `string` - Placeholder text
|
||||||
|
- `hint`: `string` - Helper text below input
|
||||||
|
- `tooltip`: `string` - Tooltip text for help icon
|
||||||
|
- `icon`: `FC` - Leading icon component
|
||||||
|
- `isRequired`: `boolean` - Required field indicator
|
||||||
|
- `isDisabled`: `boolean` - Disabled state
|
||||||
|
- `isInvalid`: `boolean` - Error state
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Basic input with label
|
||||||
|
<Input label="Email" placeholder="olivia@untitledui.com" />
|
||||||
|
|
||||||
|
// With icon and validation
|
||||||
|
<Input
|
||||||
|
icon={Mail01}
|
||||||
|
label="Email"
|
||||||
|
isRequired
|
||||||
|
isInvalid
|
||||||
|
hint="Please enter a valid email"
|
||||||
|
/>
|
||||||
|
|
||||||
|
// Input group with button
|
||||||
|
<InputGroup label="Website" trailingAddon={<Button>Copy</Button>}>
|
||||||
|
<InputBase placeholder="www.untitledui.com" />
|
||||||
|
</InputGroup>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Select
|
||||||
|
|
||||||
|
Dropdown selection component with search and multi-select capabilities.
|
||||||
|
|
||||||
|
**Import:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { MultiSelect } from "@/components/base/select/multi-select";
|
||||||
|
import { Select } from "@/components/base/select/select";
|
||||||
|
```
|
||||||
|
|
||||||
|
**Common Props:**
|
||||||
|
|
||||||
|
- `size`: `"sm" | "md"` - Select size (default: `"sm"`)
|
||||||
|
- `label`: `string` - Field label
|
||||||
|
- `placeholder`: `string` - Placeholder text
|
||||||
|
- `hint`: `string` - Helper text
|
||||||
|
- `tooltip`: `string` - Tooltip text
|
||||||
|
- `items`: `Array` - Data items to display
|
||||||
|
- `isRequired`: `boolean` - Required field
|
||||||
|
- `isDisabled`: `boolean` - Disabled state
|
||||||
|
- `placeholderIcon`: `FC | ReactNode` - Icon for placeholder
|
||||||
|
|
||||||
|
**Item Props:**
|
||||||
|
|
||||||
|
- `id`: `string` - Unique identifier
|
||||||
|
- `supportingText`: `string` - Secondary text
|
||||||
|
- `icon`: `FC | ReactNode` - Leading icon
|
||||||
|
- `avatarUrl`: `string` - Avatar image URL
|
||||||
|
- `isDisabled`: `boolean` - Disabled item
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Basic select
|
||||||
|
<Select label="Team member" placeholder="Select member" items={users}>
|
||||||
|
{(item) => (
|
||||||
|
<Select.Item id={item.id} supportingText={item.email}>
|
||||||
|
{item.name}
|
||||||
|
</Select.Item>
|
||||||
|
)}
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
// With search (ComboBox)
|
||||||
|
<Select.ComboBox label="Search" placeholder="Search users" items={users}>
|
||||||
|
{(item) => <Select.Item id={item.id}>{item.name}</Select.Item>}
|
||||||
|
</Select.ComboBox>
|
||||||
|
|
||||||
|
// With avatars
|
||||||
|
<Select items={users} placeholderIcon={User01}>
|
||||||
|
{(item) => (
|
||||||
|
<Select.Item avatarUrl={item.avatar} supportingText={item.role}>
|
||||||
|
{item.name}
|
||||||
|
</Select.Item>
|
||||||
|
)}
|
||||||
|
</Select>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Checkbox
|
||||||
|
|
||||||
|
Checkbox component for boolean selections.
|
||||||
|
|
||||||
|
**Import:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Checkbox } from "@/components/base/checkbox/checkbox";
|
||||||
|
```
|
||||||
|
|
||||||
|
**Common Props:**
|
||||||
|
|
||||||
|
- `size`: `"sm" | "md"` - Checkbox size (default: `"sm"`)
|
||||||
|
- `label`: `string` - Checkbox label
|
||||||
|
- `hint`: `string` - Helper text below label
|
||||||
|
- `isSelected`: `boolean` - Checked state
|
||||||
|
- `isDisabled`: `boolean` - Disabled state
|
||||||
|
- `isIndeterminate`: `boolean` - Indeterminate state
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Basic checkbox
|
||||||
|
<Checkbox label="Remember me" />
|
||||||
|
|
||||||
|
// With hint text
|
||||||
|
<Checkbox
|
||||||
|
label="Remember me"
|
||||||
|
hint="Save my login details for next time"
|
||||||
|
/>
|
||||||
|
|
||||||
|
// Controlled state
|
||||||
|
<Checkbox isSelected={checked} onChange={setChecked} />
|
||||||
|
```
|
||||||
|
|
||||||
|
### Badge
|
||||||
|
|
||||||
|
Badge components for status indicators and labels.
|
||||||
|
|
||||||
|
**Import:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Badge, BadgeWithDot, BadgeWithIcon } from "@/components/base/badges/badges";
|
||||||
|
```
|
||||||
|
|
||||||
|
**Common Props:**
|
||||||
|
|
||||||
|
- `size`: `"sm" | "md" | "lg"` - Badge size
|
||||||
|
- `color`: `"gray" | "brand" | "error" | "warning" | "success" | "blue-gray" | "blue-light" | "blue" | "indigo" | "purple" | "pink" | "rose" | "orange"` - Color theme
|
||||||
|
- `type`: `"pill-color" | "color" | "modern"` - Badge style variant
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Basic badge
|
||||||
|
<Badge color="brand" size="md">New</Badge>
|
||||||
|
|
||||||
|
// With dot indicator
|
||||||
|
<BadgeWithDot color="success" type="pill-color">Active</BadgeWithDot>
|
||||||
|
|
||||||
|
// With icon
|
||||||
|
<BadgeWithIcon iconLeading={ArrowUp} color="success">12%</BadgeWithIcon>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Avatar
|
||||||
|
|
||||||
|
Avatar component for user profile images.
|
||||||
|
|
||||||
|
**Import:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Avatar } from "@/components/base/avatar/avatar";
|
||||||
|
import { AvatarLabelGroup } from "@/components/base/avatar/avatar-label-group";
|
||||||
|
```
|
||||||
|
|
||||||
|
**Common Props:**
|
||||||
|
|
||||||
|
- `size`: `"xs" | "sm" | "md" | "lg" | "xl" | "2xl"` - Avatar size
|
||||||
|
- `src`: `string` - Image URL
|
||||||
|
- `alt`: `string` - Alt text for accessibility
|
||||||
|
- `initials`: `string` - Text initials when no image
|
||||||
|
- `placeholderIcon`: `FC` - Icon when no image
|
||||||
|
- `status`: `"online" | "offline"` - Status indicator
|
||||||
|
- `verified`: `boolean` - Verification badge
|
||||||
|
- `badge`: `ReactNode` - Custom badge element
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Basic avatar
|
||||||
|
<Avatar src="/avatar.jpg" alt="User Name" size="md" />
|
||||||
|
|
||||||
|
// With status
|
||||||
|
<Avatar src="/avatar.jpg" status="online" />
|
||||||
|
|
||||||
|
// With initials fallback
|
||||||
|
<Avatar initials="OR" size="lg" />
|
||||||
|
|
||||||
|
// Label group
|
||||||
|
<AvatarLabelGroup
|
||||||
|
src="/avatar.jpg"
|
||||||
|
title="Olivia Rhye"
|
||||||
|
subtitle="olivia@untitledui.com"
|
||||||
|
size="md"
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
### FeaturedIcon
|
||||||
|
|
||||||
|
Decorative icon component with themed backgrounds for emphasis and visual hierarchy.
|
||||||
|
|
||||||
|
**Import:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { FeaturedIcon } from "@/components/foundations/featured-icon/featured-icon";
|
||||||
|
```
|
||||||
|
|
||||||
|
**Common Props:**
|
||||||
|
|
||||||
|
- `icon`: `FC` - Icon component to display (required)
|
||||||
|
- `size`: `"sm" | "md" | "lg" | "xl"` - Icon container size
|
||||||
|
- `color`: `"brand" | "gray" | "error" | "warning" | "success"` - Color scheme
|
||||||
|
- `theme`: `"light" | "gradient" | "dark" | "modern" | "modern-neue" | "outline"` - Visual theme style
|
||||||
|
|
||||||
|
**Theme Styles:**
|
||||||
|
|
||||||
|
- `light`: Subtle background with colored icon
|
||||||
|
- `gradient`: Gradient background effect
|
||||||
|
- `dark`: Solid colored background with white icon
|
||||||
|
- `modern`: Contemporary gray styling (gray color only)
|
||||||
|
- `modern-neue`: Alternative modern style (gray color only)
|
||||||
|
- `outline`: Border style with transparent background
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Basic featured icon
|
||||||
|
<FeaturedIcon icon={CheckCircle} color="success" theme="light" size="lg" />
|
||||||
|
|
||||||
|
// With gradient theme
|
||||||
|
<FeaturedIcon icon={AlertCircle} color="warning" theme="gradient" size="xl" />
|
||||||
|
|
||||||
|
// Dark theme for emphasis
|
||||||
|
<FeaturedIcon icon={XCircle} color="error" theme="dark" size="md" />
|
||||||
|
|
||||||
|
// Outline style
|
||||||
|
<FeaturedIcon icon={InfoCircle} color="brand" theme="outline" size="lg" />
|
||||||
|
|
||||||
|
// Modern styles (IMPORTANT: gray only)
|
||||||
|
<FeaturedIcon icon={Settings} color="gray" theme="modern" size="lg" />
|
||||||
|
```
|
||||||
|
|
||||||
|
### Link
|
||||||
|
|
||||||
|
**Note**: There is no dedicated Link component. Instead, use the Button component with an `href` prop and link-specific color variants.
|
||||||
|
|
||||||
|
**Import:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Button } from "@/components/base/buttons/button";
|
||||||
|
```
|
||||||
|
|
||||||
|
**Link Colors:**
|
||||||
|
|
||||||
|
- `link-gray` - Gray link styling
|
||||||
|
- `link-color` - Brand color link styling
|
||||||
|
- `link-destructive` - Destructive link styling
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Basic link
|
||||||
|
<Button href="/dashboard" color="link-color">View Dashboard</Button>
|
||||||
|
|
||||||
|
// With icon
|
||||||
|
<Button href="/settings" color="link-gray" iconLeading={Settings01}>
|
||||||
|
Settings
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
// Destructive link
|
||||||
|
<Button href="/delete" color="link-destructive" iconLeading={Trash02}>
|
||||||
|
Delete Account
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
// External link
|
||||||
|
<Button href="https://example.com" color="link-color" iconTrailing={ExternalLink01}>
|
||||||
|
Visit Site
|
||||||
|
</Button>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Common Component Patterns
|
||||||
|
|
||||||
|
1. **Size Variants**: Most components support `sm`, `md`, `lg` sizes
|
||||||
|
2. **State Props**: `isDisabled`, `isLoading`, `isInvalid`, `isRequired` are common
|
||||||
|
3. **Icon Support**: Components accept icons as both components (`Icon`) or elements (`<Icon />`)
|
||||||
|
4. **Compound Components**: Complex components use dot notation (e.g., `Select.Item`, `Select.ComboBox`)
|
||||||
|
5. **Accessibility**: All components include proper ARIA attributes and keyboard support
|
||||||
|
|
||||||
|
### Icon Usage
|
||||||
|
|
||||||
|
When passing icons to components:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// As component reference (preferred)
|
||||||
|
<Button iconLeading={ChevronDown}>Options</Button>
|
||||||
|
|
||||||
|
// As element (must include data-icon)
|
||||||
|
<Button iconLeading={<ChevronDown data-icon className="size-4" />}>Options</Button>
|
||||||
|
```
|
||||||
|
|
||||||
|
## COLORS
|
||||||
|
|
||||||
|
MUST use color classes to style elements.
|
||||||
|
|
||||||
|
Bad:
|
||||||
|
|
||||||
|
- text-gray-900
|
||||||
|
- text-gray-600
|
||||||
|
- bg-blue-700
|
||||||
|
|
||||||
|
Good:
|
||||||
|
|
||||||
|
- text-primary
|
||||||
|
- text-secondary
|
||||||
|
- bg-primary
|
||||||
|
|
||||||
|
### Text Color
|
||||||
|
|
||||||
|
Use text color variables to manage all text fill colors in your designs across light and dark modes.
|
||||||
|
|
||||||
|
| Name | Usage |
|
||||||
|
| :------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| text-primary | Primary text such as page headings. |
|
||||||
|
| text-primary_on-brand | Primary text when used on solid brand color backgrounds. Commonly used for brand theme website sections (e.g. CTA sections). |
|
||||||
|
| text-secondary | Secondary text such as labels and section headings. |
|
||||||
|
| text-secondary_hover | Secondary text when in hover state. |
|
||||||
|
| text-secondary_on-brand | Secondary text when used on solid brand color backgrounds. Commonly used for brand theme website sections (e.g. CTA sections). |
|
||||||
|
| text-tertiary | Tertiary text such as supporting text and paragraph text. |
|
||||||
|
| text-tertiary_hover | Tertiary text when in hover state. |
|
||||||
|
| text-tertiary_on-brand | Tertiary text when used on solid brand color backgrounds. Commonly used for brand theme website sections (e.g. CTA sections). |
|
||||||
|
| text-quaternary | Quaternary text for more subtle and lower-contrast text, such as footer column headings. |
|
||||||
|
| text-quaternary_on-brand | Quaternary text when used on solid brand color backgrounds. Commonly used for brand theme website sections (e.g. footers). |
|
||||||
|
| text-white | Text that is always white, regardless of the mode. |
|
||||||
|
| text-disabled | Default color for disabled text such as disabled input fields or buttons. This can be changed to gray-400, but gray-500 is higher contrast and more accessible. |
|
||||||
|
| text-placeholder | Default color for placeholder text such as input field placeholders. This can be changed to gray-400, but gray-500 is more accessible because it is higher contrast. |
|
||||||
|
| text-placeholder_subtle | A more subtle (lower contrast) alternative placeholder text. Useful for components such as verification code input fields. |
|
||||||
|
| text-brand-primary | Primary brand text useful for headings (e.g. cards in pricing page headers). |
|
||||||
|
| text-brand-secondary | Secondary brand text for brand buttons, as well as accented text, highlights, and subheadings (e.g. subheadings in blog post cards). |
|
||||||
|
| text-brand-secondary_hover | Secondary brand text when in hover state (e.g. brand buttons). |
|
||||||
|
| text-brand-tertiary | Tertiary brand text for lighter accented text and highlights (e.g. numbers in metric cards). |
|
||||||
|
| text-brand-tertiary_alt | An alternative to tertiary brand text that is lighter in dark mode (e.g. numbers in metric cards). |
|
||||||
|
| text-error-primary | Default error state semantic text color (e.g. input field error states). |
|
||||||
|
| text-warning-primary | Default warning state semantic text color. |
|
||||||
|
| text-success-primary | Default success state semantic text color. |
|
||||||
|
|
||||||
|
### Border Color
|
||||||
|
|
||||||
|
Use border color variables to manage all stroke colors in your designs across light and dark modes. You can use the same values for `ring-` and `outline-` as well (i.e. `ring-primary` `outline-secondary`).
|
||||||
|
|
||||||
|
| Name | Usage |
|
||||||
|
| :--------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| border-primary | High contrast borders. These are used for components such as input fields, button groups, and checkboxes. |
|
||||||
|
| border-secondary | Medium contrast borders. This is the most commonly used border color and is the default for most components (e.g. file uploaders), cards (such as tables), and content dividers. |
|
||||||
|
| border-secondary_alt | An alternative to secondary border that uses alpha transparency. This is used exclusively for floating menus such as input dropdowns and notifications to create sharper bottom border. |
|
||||||
|
| border-tertiary | Low contrast borders useful for very subtle dividers and borders such as line and bar chart axis dividers. |
|
||||||
|
| border-disabled | Default disabled border color for disabled states in components such as input fields and checkboxes. |
|
||||||
|
| border-disabled_subtle | A more subtle (lower contrast) alternative for disabled borders such as disabled buttons. |
|
||||||
|
| border-brand | Default brand border color. Useful for active states in components such as input fields. |
|
||||||
|
| border-brand_alt | An brand border color that switches to gray when in dark mode. Useful for components such as brand-style variants of banners and footers. |
|
||||||
|
| border-error | Default error state semantic border color. Useful for error states in components such as input fields and file uploaders. |
|
||||||
|
| border-error_subtle | A more subtle (lower contrast) alternative for error state semantic borders such as error state input fields. |
|
||||||
|
|
||||||
|
### Foreground Color
|
||||||
|
|
||||||
|
Use foreground color variables to manage all non-text foreground elements in your designs across light and dark modes. Can be used via `text-`, `bg-`, `ring-`, `outline-`, `stroke-`, `fill-`, etc.
|
||||||
|
|
||||||
|
| Name | Usage |
|
||||||
|
| :--------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||||
|
| fg-primary | Highest contrast non-text foreground elements such as icons. |
|
||||||
|
| fg-secondary | High contrast non-text foreground elements such as icons. |
|
||||||
|
| fg-secondary_hover | Secondary foreground elements when in hover state. |
|
||||||
|
| fg-tertiary | Medium contrast non-text foreground elements such as icons. |
|
||||||
|
| fg-tertiary_hover | Tertiary foreground elements when in hover state. |
|
||||||
|
| fg-quaternary | Low contrast non-text foreground elements such as icons in buttons, help icons and icons used in input fields. |
|
||||||
|
| fg-quaternary_hover | Quaternary foreground elements when in hover state, such as help icons. |
|
||||||
|
| fg-white | Foreground elements that are always white, regardless of the mode. |
|
||||||
|
| fg-disabled | Default color for disabled non-text foreground elements such as icons in disabled button group buttons and input dropdown menu items. |
|
||||||
|
| fg-disabled_subtle | A more subtle (lower contrast) alternative for non-text disabled foreground elements such as disabled active checkboxes and tag checkboxes. |
|
||||||
|
| fg-brand-primary | Primary brand color non-text foreground elements such as featured icons and progress bars. |
|
||||||
|
| fg-brand-primary_alt | An alternative for primary brand color non-text foreground elements that switches to gray when in dark mode such as active horizontal tabs. |
|
||||||
|
| fg-brand-secondary | Secondary brand color non-text foreground elements such as accents and arrows in marketing site sections (e.g. hero header sections). |
|
||||||
|
| fg-brand-secondary_alt | An alternative for secondary brand color non-text foreground elements that switches to gray when in dark mode such as brand buttons. |
|
||||||
|
| fg-error-primary | Primary error state color for non-text foreground elements such as featured icons. |
|
||||||
|
| fg-error-secondary | Secondary error state color for non-text foreground elements such as icons in error state input fields and negative metrics item charts and icons. |
|
||||||
|
| fg-warning-primary | Primary warning state color for non-text foreground elements such as featured icons. |
|
||||||
|
| fg-warning-secondary | Secondary warning state color for non-text foreground elements. |
|
||||||
|
| fg-success-primary | Primary success state color for non-text foreground elements such as featured icons. |
|
||||||
|
| fg-success-secondary | Secondary success state color for non-text foreground elements such as button dots, avatar online indicator dots, and positive metrics item charts and icons. |
|
||||||
|
|
||||||
|
### Background Color
|
||||||
|
|
||||||
|
Use background color variables to manage all fill colors for elements in your designs across light and dark modes.
|
||||||
|
|
||||||
|
| Name | Usage |
|
||||||
|
| :---------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| bg-primary | The primary background color (white) used across all layouts and components. |
|
||||||
|
| bg-primary_alt | An alternative primary background color (white) that switches to bg-secondary when in dark mode. |
|
||||||
|
| bg-primary_hover | Primary background hover color. This acts as the default hover state background color for components with white backgrounds (e.g. input dropdown menu items). |
|
||||||
|
| bg-primary-solid | The primary dark background color used across layouts and components. This switches to bg-secondary when in dark mode and is useful for components such as tooltips and Text editor tooltips. |
|
||||||
|
| bg-secondary | The secondary background color used to create contrast against white backgrounds, such as website section backgrounds. |
|
||||||
|
| bg-secondary_alt | An alternative secondary background color that switches to bg-primary when in dark mode. Useful for components such as border-style horizontal tabs. |
|
||||||
|
| bg-secondary_hover | Secondary background hover color. Useful for hover states for components with gray-50 backgrounds such as active states (e.g. navigation items and date pickers). |
|
||||||
|
| bg-secondary_subtle | An alternative secondary background color that is slightly lighter and more subtle in light mode. This is useful for components such as banners. |
|
||||||
|
| bg-secondary-solid | The secondary dark background color used across layouts and components. This is useful for components such as featured icons. |
|
||||||
|
| bg-tertiary | The tertiary background color used to create contrast against light backgrounds such as toggles. |
|
||||||
|
| bg-quaternary | The quaternary background color used to create contrast against light backgrounds, such as sliders and progress bars. |
|
||||||
|
| bg-active | Default active background color for components such as selected menu items in input dropdowns. |
|
||||||
|
| bg-disabled | Default disabled background color for components such as disabled primary buttons and toggles. |
|
||||||
|
| bg-disabled_subtle | An alternative disabled background color that is more subtle. This is useful for components such as disabled input fields and checkboxes. |
|
||||||
|
| bg-overlay | Default background color for background overlays. These are useful for overlay components such as modals. |
|
||||||
|
| bg-brand-primary | The primary brand background color. Useful for components such as check icons. |
|
||||||
|
| bg-brand-primary_alt | An alternative primary brand background color that switches to bg-secondary when in dark mode. Useful for components such as active horizontal tabs. |
|
||||||
|
| bg-brand-secondary | The secondary brand background color. Useful for components such as featured icons. |
|
||||||
|
| bg-brand-solid | Default solid (dark) brand background color. Useful for components such as toggles and messages. |
|
||||||
|
| bg-brand-solid_hover | Solid brand background color when in hover state. Useful for components such as toggles. |
|
||||||
|
| bg-brand-section | This is the default dark brand color background used for website sections such as CTA sections and testimonials. Switches to bg-secondary when in dark mode. |
|
||||||
|
| bg-brand-section_subtle | An alternative brand section background color to provide contrast for website sections such as FAQ sections. Switches to bg-primary when in dark mode. |
|
||||||
|
| bg-error-primary | Primary error state background color for components such as buttons. |
|
||||||
|
| bg-error-secondary | Secondary error state background color for components such as featured icons. |
|
||||||
|
| bg-error-solid | Default solid (dark) error state background color for components such as buttons, featured icons and metric items. |
|
||||||
|
| bg-error-solid_hover | Default solid (dark) error hover state background color for components such as buttons. |
|
||||||
|
| bg-warning-primary | Primary warning state background color for components. |
|
||||||
|
| bg-warning-secondary | Secondary warning state background color for components such as featured icons. |
|
||||||
|
| bg-warning-solid | Default solid (dark) warning state background color for components such as featured icons. |
|
||||||
|
| bg-success-primary | Primary success state background color for components. |
|
||||||
|
| bg-success-secondary | Secondary success state background color for components such as featured icons. |
|
||||||
|
| bg-success-solid | Default solid (dark) success state background color for components such as featured icons and metric items. |
|
||||||
56
README.md
Normal file
56
README.md
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
# Untitled UI starter kit for Vite
|
||||||
|
|
||||||
|
This is an official Untitled UI starter kit for Vite. Kickstart your Untitled UI project with Vite in seconds.
|
||||||
|
|
||||||
|
## Untitled UI React
|
||||||
|
|
||||||
|
[Untitled UI React](https://www.untitledui.com/react) is the world’s largest collection of open-source React UI components. Everything you need to design and develop modern, beautiful interfaces—fast.
|
||||||
|
|
||||||
|
Built with React 19.1, Tailwind CSS v4.1, TypeScript 5.8, and React Aria, Untitled UI React components deliver modern performance, type safety, and maintainability.
|
||||||
|
|
||||||
|
[Learn more](https://www.untitledui.com/react) • [Documentation](https://www.untitledui.com/react/docs/introduction) • [Figma](https://www.untitledui.com/figma) • [FAQs](https://www.untitledui.com/faqs)
|
||||||
|
|
||||||
|
## Getting started
|
||||||
|
|
||||||
|
First, run the development server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
# or
|
||||||
|
yarn dev
|
||||||
|
# or
|
||||||
|
pnpm dev
|
||||||
|
# or
|
||||||
|
bun dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Open [http://localhost:5173](http://localhost:5173) with your browser to see the result.
|
||||||
|
|
||||||
|
You can start editing the app by modifying the components in `src/` folder. The page auto-updates as you edit the file.
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
Untitled UI React is built on top of [Untitled UI Figma](https://www.untitledui.com/figma), the world's largest and most popular Figma UI kit and design system. Explore more:
|
||||||
|
|
||||||
|
**[Untitled UI Figma:](https://www.untitledui.com/react/resources/figma-files)** The world's largest Figma UI kit and design system.
|
||||||
|
<br/>
|
||||||
|
**[Untitled UI Icons:](https://www.untitledui.com/react/resources/icons)** A clean, consistent, and neutral icon library crafted specifically for modern UI design.
|
||||||
|
<br/>
|
||||||
|
**[Untitled UI file icons:](https://www.untitledui.com/react/resources/file-icons)** Free file format icons, designed specifically for modern web and UI design.
|
||||||
|
<br/>
|
||||||
|
**[Untitled UI flag icons:](https://www.untitledui.com/react/resources/flag-icons)** Free country flag icons, designed specifically for modern web and UI design.
|
||||||
|
<br/>
|
||||||
|
**[Untitled UI avatars:](https://www.untitledui.com/react/resources/avatars)** Free placeholder user avatars and profile pictures to use in your projects.
|
||||||
|
<br/>
|
||||||
|
**[Untitled UI logos:](https://www.untitledui.com/react/resources/logos)** Free fictional company logos to use in your projects.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Untitled UI React open-source components are licensed under the MIT license, which means you can use them for free in unlimited commercial projects.
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> This license applies only to the starter kit and to the components included in this open-source repository. [Untitled UI React PRO](https://www.untitledui.com/react) includes hundreds more advanced UI components and page examples and is subject to a separate [license agreement](https://www.untitledui.com/license).
|
||||||
|
|
||||||
|
[Untitled UI license agreement →](https://www.untitledui.com/license)
|
||||||
|
|
||||||
|
[Frequently asked questions →](https://www.untitledui.com/faqs)
|
||||||
848
bun.lock
Normal file
848
bun.lock
Normal file
@@ -0,0 +1,848 @@
|
|||||||
|
{
|
||||||
|
"lockfileVersion": 1,
|
||||||
|
"workspaces": {
|
||||||
|
"": {
|
||||||
|
"name": "untitledui-vite-starter-kit",
|
||||||
|
"dependencies": {
|
||||||
|
"@tailwindcss/typography": "^0.5.19",
|
||||||
|
"@tailwindcss/vite": "^4.1.18",
|
||||||
|
"@untitledui/file-icons": "^0.0.8",
|
||||||
|
"@untitledui/icons": "^0.0.21",
|
||||||
|
"input-otp": "^1.4.2",
|
||||||
|
"motion": "^12.29.0",
|
||||||
|
"qr-code-styling": "^1.9.2",
|
||||||
|
"react": "^19.2.3",
|
||||||
|
"react-aria": "^3.46.0",
|
||||||
|
"react-aria-components": "^1.15.1",
|
||||||
|
"react-dom": "^19.2.3",
|
||||||
|
"react-hotkeys-hook": "^5.2.3",
|
||||||
|
"react-router": "^7.13.0",
|
||||||
|
"tailwind-merge": "^3.4.0",
|
||||||
|
"tailwindcss": "^4.1.18",
|
||||||
|
"tailwindcss-animate": "^1.0.7",
|
||||||
|
"tailwindcss-react-aria-components": "^2.0.1",
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tailwindcss/postcss": "^4.1.18",
|
||||||
|
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
|
||||||
|
"@types/node": "^24.10.9",
|
||||||
|
"@types/react": "^19.2.9",
|
||||||
|
"@types/react-dom": "^19.2.3",
|
||||||
|
"@vitejs/plugin-react-swc": "^4.2.2",
|
||||||
|
"globals": "^16.5.0",
|
||||||
|
"prettier": "^3.8.1",
|
||||||
|
"prettier-plugin-tailwindcss": "^0.6.14",
|
||||||
|
"typescript": "^5.9.3",
|
||||||
|
"typescript-eslint": "^8.53.1",
|
||||||
|
"vite": "^7.3.1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"packages": {
|
||||||
|
"@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="],
|
||||||
|
|
||||||
|
"@babel/code-frame": ["@babel/code-frame@7.28.6", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q=="],
|
||||||
|
|
||||||
|
"@babel/generator": ["@babel/generator@7.28.6", "", { "dependencies": { "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw=="],
|
||||||
|
|
||||||
|
"@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
|
||||||
|
|
||||||
|
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
|
||||||
|
|
||||||
|
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
|
||||||
|
|
||||||
|
"@babel/parser": ["@babel/parser@7.28.6", "", { "dependencies": { "@babel/types": "^7.28.6" }, "bin": "./bin/babel-parser.js" }, "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ=="],
|
||||||
|
|
||||||
|
"@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="],
|
||||||
|
|
||||||
|
"@babel/traverse": ["@babel/traverse@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.6", "@babel/template": "^7.28.6", "@babel/types": "^7.28.6", "debug": "^4.3.1" } }, "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg=="],
|
||||||
|
|
||||||
|
"@babel/types": ["@babel/types@7.28.6", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg=="],
|
||||||
|
|
||||||
|
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw=="],
|
||||||
|
|
||||||
|
"@esbuild/android-arm": ["@esbuild/android-arm@0.27.2", "", { "os": "android", "cpu": "arm" }, "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA=="],
|
||||||
|
|
||||||
|
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.2", "", { "os": "android", "cpu": "arm64" }, "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA=="],
|
||||||
|
|
||||||
|
"@esbuild/android-x64": ["@esbuild/android-x64@0.27.2", "", { "os": "android", "cpu": "x64" }, "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A=="],
|
||||||
|
|
||||||
|
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg=="],
|
||||||
|
|
||||||
|
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA=="],
|
||||||
|
|
||||||
|
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g=="],
|
||||||
|
|
||||||
|
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.2", "", { "os": "linux", "cpu": "arm" }, "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.2", "", { "os": "linux", "cpu": "x64" }, "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA=="],
|
||||||
|
|
||||||
|
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.2", "", { "os": "none", "cpu": "arm64" }, "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw=="],
|
||||||
|
|
||||||
|
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.2", "", { "os": "none", "cpu": "x64" }, "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA=="],
|
||||||
|
|
||||||
|
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA=="],
|
||||||
|
|
||||||
|
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg=="],
|
||||||
|
|
||||||
|
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.2", "", { "os": "none", "cpu": "arm64" }, "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag=="],
|
||||||
|
|
||||||
|
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg=="],
|
||||||
|
|
||||||
|
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg=="],
|
||||||
|
|
||||||
|
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ=="],
|
||||||
|
|
||||||
|
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.2", "", { "os": "win32", "cpu": "x64" }, "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ=="],
|
||||||
|
|
||||||
|
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="],
|
||||||
|
|
||||||
|
"@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="],
|
||||||
|
|
||||||
|
"@eslint/config-array": ["@eslint/config-array@0.21.1", "", { "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA=="],
|
||||||
|
|
||||||
|
"@eslint/config-helpers": ["@eslint/config-helpers@0.4.2", "", { "dependencies": { "@eslint/core": "^0.17.0" } }, "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw=="],
|
||||||
|
|
||||||
|
"@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="],
|
||||||
|
|
||||||
|
"@eslint/eslintrc": ["@eslint/eslintrc@3.3.3", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ=="],
|
||||||
|
|
||||||
|
"@eslint/js": ["@eslint/js@9.39.2", "", {}, "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA=="],
|
||||||
|
|
||||||
|
"@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="],
|
||||||
|
|
||||||
|
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="],
|
||||||
|
|
||||||
|
"@formatjs/ecma402-abstract": ["@formatjs/ecma402-abstract@2.3.6", "", { "dependencies": { "@formatjs/fast-memoize": "2.2.7", "@formatjs/intl-localematcher": "0.6.2", "decimal.js": "^10.4.3", "tslib": "^2.8.0" } }, "sha512-HJnTFeRM2kVFVr5gr5kH1XP6K0JcJtE7Lzvtr3FS/so5f1kpsqqqxy5JF+FRaO6H2qmcMfAUIox7AJteieRtVw=="],
|
||||||
|
|
||||||
|
"@formatjs/fast-memoize": ["@formatjs/fast-memoize@2.2.7", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ=="],
|
||||||
|
|
||||||
|
"@formatjs/icu-messageformat-parser": ["@formatjs/icu-messageformat-parser@2.11.4", "", { "dependencies": { "@formatjs/ecma402-abstract": "2.3.6", "@formatjs/icu-skeleton-parser": "1.8.16", "tslib": "^2.8.0" } }, "sha512-7kR78cRrPNB4fjGFZg3Rmj5aah8rQj9KPzuLsmcSn4ipLXQvC04keycTI1F7kJYDwIXtT2+7IDEto842CfZBtw=="],
|
||||||
|
|
||||||
|
"@formatjs/icu-skeleton-parser": ["@formatjs/icu-skeleton-parser@1.8.16", "", { "dependencies": { "@formatjs/ecma402-abstract": "2.3.6", "tslib": "^2.8.0" } }, "sha512-H13E9Xl+PxBd8D5/6TVUluSpxGNvFSlN/b3coUp0e0JpuWXXnQDiavIpY3NnvSp4xhEMoXyyBvVfdFX8jglOHQ=="],
|
||||||
|
|
||||||
|
"@formatjs/intl-localematcher": ["@formatjs/intl-localematcher@0.6.2", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-XOMO2Hupl0wdd172Y06h6kLpBz6Dv+J4okPLl4LPtzbr8f66WbIoy4ev98EBuZ6ZK4h5ydTN6XneT4QVpD7cdA=="],
|
||||||
|
|
||||||
|
"@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
|
||||||
|
|
||||||
|
"@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="],
|
||||||
|
|
||||||
|
"@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="],
|
||||||
|
|
||||||
|
"@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="],
|
||||||
|
|
||||||
|
"@internationalized/date": ["@internationalized/date@3.11.0", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-BOx5huLAWhicM9/ZFs84CzP+V3gBW6vlpM02yzsdYC7TGlZJX1OJiEEHcSayF00Z+3jLlm4w79amvSt6RqKN3Q=="],
|
||||||
|
|
||||||
|
"@internationalized/message": ["@internationalized/message@3.1.8", "", { "dependencies": { "@swc/helpers": "^0.5.0", "intl-messageformat": "^10.1.0" } }, "sha512-Rwk3j/TlYZhn3HQ6PyXUV0XP9Uv42jqZGNegt0BXlxjE6G3+LwHjbQZAGHhCnCPdaA6Tvd3ma/7QzLlLkJxAWA=="],
|
||||||
|
|
||||||
|
"@internationalized/number": ["@internationalized/number@3.6.5", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-6hY4Kl4HPBvtfS62asS/R22JzNNy8vi/Ssev7x6EobfCp+9QIB2hKvI2EtbdJ0VSQacxVNtqhE/NmF/NZ0gm6g=="],
|
||||||
|
|
||||||
|
"@internationalized/string": ["@internationalized/string@3.2.7", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-D4OHBjrinH+PFZPvfCXvG28n2LSykWcJ7GIioQL+ok0LON15SdfoUssoHzzOUmVZLbRoREsQXVzA6r8JKsbP6A=="],
|
||||||
|
|
||||||
|
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
|
||||||
|
|
||||||
|
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
|
||||||
|
|
||||||
|
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
|
||||||
|
|
||||||
|
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
|
||||||
|
|
||||||
|
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
||||||
|
|
||||||
|
"@react-aria/autocomplete": ["@react-aria/autocomplete@3.0.0-rc.5", "", { "dependencies": { "@react-aria/combobox": "^3.14.2", "@react-aria/focus": "^3.21.4", "@react-aria/i18n": "^3.12.15", "@react-aria/interactions": "^3.27.0", "@react-aria/listbox": "^3.15.2", "@react-aria/searchfield": "^3.8.11", "@react-aria/textfield": "^3.18.4", "@react-aria/utils": "^3.33.0", "@react-stately/autocomplete": "3.0.0-beta.4", "@react-stately/combobox": "^3.12.2", "@react-types/autocomplete": "3.0.0-alpha.37", "@react-types/button": "^3.15.0", "@react-types/shared": "^3.33.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-qcGr/ZlSJxw78QtXB29MnvCwGZKlJ5FGfSICjaX/KIg4ONGFR/u4QjP/axA+vhlPa9Ik7BNeikWQriTcYrkbhw=="],
|
||||||
|
|
||||||
|
"@react-aria/breadcrumbs": ["@react-aria/breadcrumbs@3.5.31", "", { "dependencies": { "@react-aria/i18n": "^3.12.15", "@react-aria/link": "^3.8.8", "@react-aria/utils": "^3.33.0", "@react-types/breadcrumbs": "^3.7.18", "@react-types/shared": "^3.33.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-j8F2NMHFGT/n3alfFKdO4bvrY/ymtdL04GdclY7Vc6zOmCnWoEZ2UA0sFuV7Rk9dOL8fAtYV1kMD1ZRO/EMcGA=="],
|
||||||
|
|
||||||
|
"@react-aria/button": ["@react-aria/button@3.14.4", "", { "dependencies": { "@react-aria/interactions": "^3.27.0", "@react-aria/toolbar": "3.0.0-beta.23", "@react-aria/utils": "^3.33.0", "@react-stately/toggle": "^3.9.4", "@react-types/button": "^3.15.0", "@react-types/shared": "^3.33.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-6mTPiSSQhELnWlnYJ1Tm1B0VL1GGKAs2PGAY3ZGbPGQPPDc6Wu82yIhuAO8TTFJrXkwAiqjQawgDLil/yB0V7Q=="],
|
||||||
|
|
||||||
|
"@react-aria/calendar": ["@react-aria/calendar@3.9.4", "", { "dependencies": { "@internationalized/date": "^3.11.0", "@react-aria/i18n": "^3.12.15", "@react-aria/interactions": "^3.27.0", "@react-aria/live-announcer": "^3.4.4", "@react-aria/utils": "^3.33.0", "@react-stately/calendar": "^3.9.2", "@react-types/button": "^3.15.0", "@react-types/calendar": "^3.8.2", "@react-types/shared": "^3.33.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-0BvU8cj6uHn622Vp8Xd21XxXtvp3Bh4Yk1pHloqDNmUvvdBN+ol3Xsm5gG3XKKkZ+6CCEi6asCbLaEg3SZSbyg=="],
|
||||||
|
|
||||||
|
"@react-aria/checkbox": ["@react-aria/checkbox@3.16.4", "", { "dependencies": { "@react-aria/form": "^3.1.4", "@react-aria/interactions": "^3.27.0", "@react-aria/label": "^3.7.24", "@react-aria/toggle": "^3.12.4", "@react-aria/utils": "^3.33.0", "@react-stately/checkbox": "^3.7.4", "@react-stately/form": "^3.2.3", "@react-stately/toggle": "^3.9.4", "@react-types/checkbox": "^3.10.3", "@react-types/shared": "^3.33.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-FcZj6/f27mNp2+G5yxyOMRZbZQjJ1cuWvo0PPnnZ4ybSPUmSzI4uUZBk1wvsJVP9F9n+J2hZuYVCaN8pyzLweA=="],
|
||||||
|
|
||||||
|
"@react-aria/collections": ["@react-aria/collections@3.0.2", "", { "dependencies": { "@react-aria/interactions": "^3.27.0", "@react-aria/ssr": "^3.9.10", "@react-aria/utils": "^3.33.0", "@react-types/shared": "^3.33.0", "@swc/helpers": "^0.5.0", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-5GV0fj1bvfdztHozlZQ1nzdmcZOAOdZ5BhwrSyuHbK5ptmQrpAoWUK+VTQlxkAfyn5i6niaaN/llP1v3RgEemw=="],
|
||||||
|
|
||||||
|
"@react-aria/color": ["@react-aria/color@3.1.4", "", { "dependencies": { "@react-aria/i18n": "^3.12.15", "@react-aria/interactions": "^3.27.0", "@react-aria/numberfield": "^3.12.4", "@react-aria/slider": "^3.8.4", "@react-aria/spinbutton": "^3.7.1", "@react-aria/textfield": "^3.18.4", "@react-aria/utils": "^3.33.0", "@react-aria/visually-hidden": "^3.8.30", "@react-stately/color": "^3.9.4", "@react-stately/form": "^3.2.3", "@react-types/color": "^3.1.3", "@react-types/shared": "^3.33.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-LNFo0A9EEn2HZ8O/hASschH++M+krfezcp01XPv0/2ZQJ5b5u7VvJlUOEXtPsD4i9+BzvkSAEoVUXdlJie9V2Q=="],
|
||||||
|
|
||||||
|
"@react-aria/combobox": ["@react-aria/combobox@3.14.2", "", { "dependencies": { "@react-aria/focus": "^3.21.4", "@react-aria/i18n": "^3.12.15", "@react-aria/listbox": "^3.15.2", "@react-aria/live-announcer": "^3.4.4", "@react-aria/menu": "^3.20.0", "@react-aria/overlays": "^3.31.1", "@react-aria/selection": "^3.27.1", "@react-aria/textfield": "^3.18.4", "@react-aria/utils": "^3.33.0", "@react-stately/collections": "^3.12.9", "@react-stately/combobox": "^3.12.2", "@react-stately/form": "^3.2.3", "@react-types/button": "^3.15.0", "@react-types/combobox": "^3.13.11", "@react-types/shared": "^3.33.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-qwBeb8cMgK3xwrvXYHPtcphduD/k+oTcU18JHPvEO2kmR32knB33H81C2/Zoh4x86zTDJXaEtPscXBWuQ/M7AQ=="],
|
||||||
|
|
||||||
|
"@react-aria/datepicker": ["@react-aria/datepicker@3.16.0", "", { "dependencies": { "@internationalized/date": "^3.11.0", "@internationalized/number": "^3.6.5", "@internationalized/string": "^3.2.7", "@react-aria/focus": "^3.21.4", "@react-aria/form": "^3.1.4", "@react-aria/i18n": "^3.12.15", "@react-aria/interactions": "^3.27.0", "@react-aria/label": "^3.7.24", "@react-aria/spinbutton": "^3.7.1", "@react-aria/utils": "^3.33.0", "@react-stately/datepicker": "^3.16.0", "@react-stately/form": "^3.2.3", "@react-types/button": "^3.15.0", "@react-types/calendar": "^3.8.2", "@react-types/datepicker": "^3.13.4", "@react-types/dialog": "^3.5.23", "@react-types/shared": "^3.33.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-QynYHIHE+wvuGopl/k05tphmDpykpfZ3l3eKnUfGrqvAYJEeCOyS0qoMlw7Vq3NscMLFbJI6ajqBmlmtgFNiSA=="],
|
||||||
|
|
||||||
|
"@react-aria/dialog": ["@react-aria/dialog@3.5.33", "", { "dependencies": { "@react-aria/interactions": "^3.27.0", "@react-aria/overlays": "^3.31.1", "@react-aria/utils": "^3.33.0", "@react-types/dialog": "^3.5.23", "@react-types/shared": "^3.33.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-C5FpLAMJU6gQU8gztWKlEJ2A0k/JKl0YijNOv3Lizk+vUdF5njROSrmFs16bY5Hd6ycmsK9x/Pqkq3m/OpNFXA=="],
|
||||||
|
|
||||||
|
"@react-aria/disclosure": ["@react-aria/disclosure@3.1.2", "", { "dependencies": { "@react-aria/ssr": "^3.9.10", "@react-aria/utils": "^3.33.0", "@react-stately/disclosure": "^3.0.10", "@react-types/button": "^3.15.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-UQ/CmWcdcROfRTMtvfsnYHrEsPPNbwZifZ/UErQpbvU4kzal2N+PpuP3+kpdf4G7TeMt+uJ8S9dLzyFVijOj9A=="],
|
||||||
|
|
||||||
|
"@react-aria/dnd": ["@react-aria/dnd@3.11.5", "", { "dependencies": { "@internationalized/string": "^3.2.7", "@react-aria/i18n": "^3.12.15", "@react-aria/interactions": "^3.27.0", "@react-aria/live-announcer": "^3.4.4", "@react-aria/overlays": "^3.31.1", "@react-aria/utils": "^3.33.0", "@react-stately/collections": "^3.12.9", "@react-stately/dnd": "^3.7.3", "@react-types/button": "^3.15.0", "@react-types/shared": "^3.33.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-3IGrABfK8Cf6/b/uEmGEDGeubWKMUK3umWunF/tdkWBnIaxpdj4gRkWFMw7siWQYnqir6AN567nrWXtHFcLKsA=="],
|
||||||
|
|
||||||
|
"@react-aria/focus": ["@react-aria/focus@3.21.4", "", { "dependencies": { "@react-aria/interactions": "^3.27.0", "@react-aria/utils": "^3.33.0", "@react-types/shared": "^3.33.0", "@swc/helpers": "^0.5.0", "clsx": "^2.0.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-6gz+j9ip0/vFRTKJMl3R30MHopn4i19HqqLfSQfElxJD+r9hBnYG1Q6Wd/kl/WRR1+CALn2F+rn06jUnf5sT8Q=="],
|
||||||
|
|
||||||
|
"@react-aria/form": ["@react-aria/form@3.1.4", "", { "dependencies": { "@react-aria/interactions": "^3.27.0", "@react-aria/utils": "^3.33.0", "@react-stately/form": "^3.2.3", "@react-types/shared": "^3.33.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-GjPS85cE/34zal3vs6MOi7FxUsXwbxN4y6l1LFor2g92UK97gVobp238f3xdMW2T8IuaWGcnHeYFg+cjiZ51pQ=="],
|
||||||
|
|
||||||
|
"@react-aria/grid": ["@react-aria/grid@3.14.7", "", { "dependencies": { "@react-aria/focus": "^3.21.4", "@react-aria/i18n": "^3.12.15", "@react-aria/interactions": "^3.27.0", "@react-aria/live-announcer": "^3.4.4", "@react-aria/selection": "^3.27.1", "@react-aria/utils": "^3.33.0", "@react-stately/collections": "^3.12.9", "@react-stately/grid": "^3.11.8", "@react-stately/selection": "^3.20.8", "@react-types/checkbox": "^3.10.3", "@react-types/grid": "^3.3.7", "@react-types/shared": "^3.33.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-8eaJThNHUs75Xf4+FQC2NKQtTOVYkkDdA8VbfbqG06oYDAn7ETb1yhbwoqh1jOv7MezCNkYjyFe4ADsz2rBVcw=="],
|
||||||
|
|
||||||
|
"@react-aria/gridlist": ["@react-aria/gridlist@3.14.3", "", { "dependencies": { "@react-aria/focus": "^3.21.4", "@react-aria/grid": "^3.14.7", "@react-aria/i18n": "^3.12.15", "@react-aria/interactions": "^3.27.0", "@react-aria/selection": "^3.27.1", "@react-aria/utils": "^3.33.0", "@react-stately/list": "^3.13.3", "@react-stately/tree": "^3.9.5", "@react-types/shared": "^3.33.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-t3nr29nU5jRG9MdWe9aiMd02V8o0pmidLU/7c4muWAu7hEH+IYdeDthGDdXL9tXAom/oQ+6yt6sOfLxpsVNmGA=="],
|
||||||
|
|
||||||
|
"@react-aria/i18n": ["@react-aria/i18n@3.12.15", "", { "dependencies": { "@internationalized/date": "^3.11.0", "@internationalized/message": "^3.1.8", "@internationalized/number": "^3.6.5", "@internationalized/string": "^3.2.7", "@react-aria/ssr": "^3.9.10", "@react-aria/utils": "^3.33.0", "@react-types/shared": "^3.33.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-3CrAN7ORVHrckvTmbPq76jFZabqq+rScosGT5+ElircJ5rF5+JcdT99Hp5Xg6R10jk74e8G3xiqdYsUd+7iJMA=="],
|
||||||
|
|
||||||
|
"@react-aria/interactions": ["@react-aria/interactions@3.27.0", "", { "dependencies": { "@react-aria/ssr": "^3.9.10", "@react-aria/utils": "^3.33.0", "@react-stately/flags": "^3.1.2", "@react-types/shared": "^3.33.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-D27pOy+0jIfHK60BB26AgqjjRFOYdvVSkwC31b2LicIzRCSPOSP06V4gMHuGmkhNTF4+YWDi1HHYjxIvMeiSlA=="],
|
||||||
|
|
||||||
|
"@react-aria/label": ["@react-aria/label@3.7.24", "", { "dependencies": { "@react-aria/utils": "^3.33.0", "@react-types/shared": "^3.33.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-lcJbUy6xyicWKNgzfrXksrJ2CeCST2rDxGAvHOmUxSbFOm26kK710DjaFvtO4tICWh/TKW5mC3sm77soNcVUGA=="],
|
||||||
|
|
||||||
|
"@react-aria/landmark": ["@react-aria/landmark@3.0.9", "", { "dependencies": { "@react-aria/utils": "^3.33.0", "@react-types/shared": "^3.33.0", "@swc/helpers": "^0.5.0", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-YYyluDBCXupnMh91ccE5g27fczjYmzPebHqTkVYjH4B6k45pOoqsMmWBCMnOTl0qOCeioI+daT8W0MamAZzoSw=="],
|
||||||
|
|
||||||
|
"@react-aria/link": ["@react-aria/link@3.8.8", "", { "dependencies": { "@react-aria/interactions": "^3.27.0", "@react-aria/utils": "^3.33.0", "@react-types/link": "^3.6.6", "@react-types/shared": "^3.33.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-hxQEvo5rrn2C0GOSwB/tROe+y//dyhmyXGbm8arDy6WF5Mj0wcjjrAu0/dhGYBqoltJa16iIEvs52xgzOC+f+Q=="],
|
||||||
|
|
||||||
|
"@react-aria/listbox": ["@react-aria/listbox@3.15.2", "", { "dependencies": { "@react-aria/interactions": "^3.27.0", "@react-aria/label": "^3.7.24", "@react-aria/selection": "^3.27.1", "@react-aria/utils": "^3.33.0", "@react-stately/collections": "^3.12.9", "@react-stately/list": "^3.13.3", "@react-types/listbox": "^3.7.5", "@react-types/shared": "^3.33.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-xcrgSediV8MaVmsuDrDPmWywF82/HOv+H+Y/dgr6GLCWl0XDj5Q7PyAhDzUsYdZNIne3B9muGh6IQc3HdkgWqg=="],
|
||||||
|
|
||||||
|
"@react-aria/live-announcer": ["@react-aria/live-announcer@3.4.4", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-PTTBIjNRnrdJOIRTDGNifY2d//kA7GUAwRFJNOEwSNG4FW+Bq9awqLiflw0JkpyB0VNIwou6lqKPHZVLsGWOXA=="],
|
||||||
|
|
||||||
|
"@react-aria/menu": ["@react-aria/menu@3.20.0", "", { "dependencies": { "@react-aria/focus": "^3.21.4", "@react-aria/i18n": "^3.12.15", "@react-aria/interactions": "^3.27.0", "@react-aria/overlays": "^3.31.1", "@react-aria/selection": "^3.27.1", "@react-aria/utils": "^3.33.0", "@react-stately/collections": "^3.12.9", "@react-stately/menu": "^3.9.10", "@react-stately/selection": "^3.20.8", "@react-stately/tree": "^3.9.5", "@react-types/button": "^3.15.0", "@react-types/menu": "^3.10.6", "@react-types/shared": "^3.33.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-BAsHuf7kTVmawNUkTUd5RB3ZvL6DQQT7hgZ2cYKd/1ZwYq4KO2wWGYdzyTOtK1qimZL0eyHyQwDYv4dNKBH4gw=="],
|
||||||
|
|
||||||
|
"@react-aria/meter": ["@react-aria/meter@3.4.29", "", { "dependencies": { "@react-aria/progress": "^3.4.29", "@react-types/meter": "^3.4.14", "@react-types/shared": "^3.33.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-XAhJf8LlYQl+QQXqtpWvzjlrT8MZKEG6c8N3apC5DONgSKlCwfmDm4laGEJPqtuz3QGiOopsfSfyTFYHjWsfZw=="],
|
||||||
|
|
||||||
|
"@react-aria/numberfield": ["@react-aria/numberfield@3.12.4", "", { "dependencies": { "@react-aria/i18n": "^3.12.15", "@react-aria/interactions": "^3.27.0", "@react-aria/spinbutton": "^3.7.1", "@react-aria/textfield": "^3.18.4", "@react-aria/utils": "^3.33.0", "@react-stately/form": "^3.2.3", "@react-stately/numberfield": "^3.10.4", "@react-types/button": "^3.15.0", "@react-types/numberfield": "^3.8.17", "@react-types/shared": "^3.33.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-TgKBjKOjyURzbqNR2wF4tSFmQKNK5DqE4QZSlQxpYYo1T6zuztkh+oTOUZ4IWCJymL5qLtuPfGHCZbR7B+DN2w=="],
|
||||||
|
|
||||||
|
"@react-aria/overlays": ["@react-aria/overlays@3.31.1", "", { "dependencies": { "@react-aria/focus": "^3.21.4", "@react-aria/i18n": "^3.12.15", "@react-aria/interactions": "^3.27.0", "@react-aria/ssr": "^3.9.10", "@react-aria/utils": "^3.33.0", "@react-aria/visually-hidden": "^3.8.30", "@react-stately/overlays": "^3.6.22", "@react-types/button": "^3.15.0", "@react-types/overlays": "^3.9.3", "@react-types/shared": "^3.33.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-U5BedzcXU97U5PWm4kIPnNoVpAs9KjTYfbkGx33vapmTVpGYhQyYW9eg6zW2E8ZKsyFJtQ/jkQnbWGen97aHSQ=="],
|
||||||
|
|
||||||
|
"@react-aria/progress": ["@react-aria/progress@3.4.29", "", { "dependencies": { "@react-aria/i18n": "^3.12.15", "@react-aria/label": "^3.7.24", "@react-aria/utils": "^3.33.0", "@react-types/progress": "^3.5.17", "@react-types/shared": "^3.33.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-orSaaFLX5LdD9UyxgBrmP1J/ivyEFX+5v4ENPQM5RH5+Hl+0OJa+8ozI0AfVKBqCYc89BOZfG7kzi7wFHACZcQ=="],
|
||||||
|
|
||||||
|
"@react-aria/radio": ["@react-aria/radio@3.12.4", "", { "dependencies": { "@react-aria/focus": "^3.21.4", "@react-aria/form": "^3.1.4", "@react-aria/i18n": "^3.12.15", "@react-aria/interactions": "^3.27.0", "@react-aria/label": "^3.7.24", "@react-aria/utils": "^3.33.0", "@react-stately/radio": "^3.11.4", "@react-types/radio": "^3.9.3", "@react-types/shared": "^3.33.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-2sjBAE8++EtAAfjwPdrqEVswbzR4Mvcy4n8SvwUxTo02yESa9nolBzCSdAUFUmhrNj3MiMA+zLxQ+KACfUjJOg=="],
|
||||||
|
|
||||||
|
"@react-aria/searchfield": ["@react-aria/searchfield@3.8.11", "", { "dependencies": { "@react-aria/i18n": "^3.12.15", "@react-aria/textfield": "^3.18.4", "@react-aria/utils": "^3.33.0", "@react-stately/searchfield": "^3.5.18", "@react-types/button": "^3.15.0", "@react-types/searchfield": "^3.6.7", "@react-types/shared": "^3.33.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-5R0prEC+jRFwPeJsK6G4RN8QG3V/+EaIuw9p79G1gFD+1dY81ZakiZIIJaLWRyO7AzYBGyC/QFHtz0m3KGQT/Q=="],
|
||||||
|
|
||||||
|
"@react-aria/select": ["@react-aria/select@3.17.2", "", { "dependencies": { "@react-aria/form": "^3.1.4", "@react-aria/i18n": "^3.12.15", "@react-aria/interactions": "^3.27.0", "@react-aria/label": "^3.7.24", "@react-aria/listbox": "^3.15.2", "@react-aria/menu": "^3.20.0", "@react-aria/selection": "^3.27.1", "@react-aria/utils": "^3.33.0", "@react-aria/visually-hidden": "^3.8.30", "@react-stately/select": "^3.9.1", "@react-types/button": "^3.15.0", "@react-types/select": "^3.12.1", "@react-types/shared": "^3.33.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-oMpHStyMluRf67qxrzH5Qfcvw6ETQgZT1Qw2xvAxQVRd5IBb0PfzZS7TGiULOcMLqXAUOC28O/ycUGrGRKLarg=="],
|
||||||
|
|
||||||
|
"@react-aria/selection": ["@react-aria/selection@3.27.1", "", { "dependencies": { "@react-aria/focus": "^3.21.4", "@react-aria/i18n": "^3.12.15", "@react-aria/interactions": "^3.27.0", "@react-aria/utils": "^3.33.0", "@react-stately/selection": "^3.20.8", "@react-types/shared": "^3.33.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-8WQ4AtWiBnk9UEeYkqpH12dd8KQW2aFbNZvM4sDfLtz7K7HWyY/MkqMe/snk9IcoSa7t4zr0bnoZJcWSGgn2PQ=="],
|
||||||
|
|
||||||
|
"@react-aria/separator": ["@react-aria/separator@3.4.15", "", { "dependencies": { "@react-aria/utils": "^3.33.0", "@react-types/shared": "^3.33.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-A1aPQhCaE8XeelNJYPjHtA2uh921ROh8PNiZI4o62x80wcziRoctN5PAtNHJAx7VKvX66A8ZVGbOqb7iqS3J5Q=="],
|
||||||
|
|
||||||
|
"@react-aria/slider": ["@react-aria/slider@3.8.4", "", { "dependencies": { "@react-aria/i18n": "^3.12.15", "@react-aria/interactions": "^3.27.0", "@react-aria/label": "^3.7.24", "@react-aria/utils": "^3.33.0", "@react-stately/slider": "^3.7.4", "@react-types/shared": "^3.33.0", "@react-types/slider": "^3.8.3", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-/FYCgK1qVqaz2VCDfR2x4BjyJ8lmWg1v8//+WIwKdIu4cz0KUs+U3yx0w1vp676RoERp3OEvkT3tb+/jHQ1hjA=="],
|
||||||
|
|
||||||
|
"@react-aria/spinbutton": ["@react-aria/spinbutton@3.7.1", "", { "dependencies": { "@react-aria/i18n": "^3.12.15", "@react-aria/live-announcer": "^3.4.4", "@react-aria/utils": "^3.33.0", "@react-types/button": "^3.15.0", "@react-types/shared": "^3.33.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-Nisah6yzxOC6983u/5ck0w+OQoa3sRKmpDvWpTEX0g2+ZIABOl8ttdSd65XKtxXmXHdK8X1zmrfeGOBfBR3sKA=="],
|
||||||
|
|
||||||
|
"@react-aria/ssr": ["@react-aria/ssr@3.9.10", "", { "dependencies": { "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-hvTm77Pf+pMBhuBm760Li0BVIO38jv1IBws1xFm1NoL26PU+fe+FMW5+VZWyANR6nYL65joaJKZqOdTQMkO9IQ=="],
|
||||||
|
|
||||||
|
"@react-aria/switch": ["@react-aria/switch@3.7.10", "", { "dependencies": { "@react-aria/toggle": "^3.12.4", "@react-stately/toggle": "^3.9.4", "@react-types/shared": "^3.33.0", "@react-types/switch": "^3.5.16", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-j7nrYnqX6H9J8GuqD0kdMECUozeqxeG19A2nsvfaTx3//Q7RhgIR9fqhQdVHW/wgraTlEHNH6AhDzmomBg0TNw=="],
|
||||||
|
|
||||||
|
"@react-aria/table": ["@react-aria/table@3.17.10", "", { "dependencies": { "@react-aria/focus": "^3.21.4", "@react-aria/grid": "^3.14.7", "@react-aria/i18n": "^3.12.15", "@react-aria/interactions": "^3.27.0", "@react-aria/live-announcer": "^3.4.4", "@react-aria/utils": "^3.33.0", "@react-aria/visually-hidden": "^3.8.30", "@react-stately/collections": "^3.12.9", "@react-stately/flags": "^3.1.2", "@react-stately/table": "^3.15.3", "@react-types/checkbox": "^3.10.3", "@react-types/grid": "^3.3.7", "@react-types/shared": "^3.33.0", "@react-types/table": "^3.13.5", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-xdEeyOzuETkOfAHhZrX7HOIwMUsCUr4rbPvHqdcNqg7Ngla2ck9iulZNAyvOPfFwELuBEd2rz1I9TYRQ2OzSQQ=="],
|
||||||
|
|
||||||
|
"@react-aria/tabs": ["@react-aria/tabs@3.11.0", "", { "dependencies": { "@react-aria/focus": "^3.21.4", "@react-aria/i18n": "^3.12.15", "@react-aria/selection": "^3.27.1", "@react-aria/utils": "^3.33.0", "@react-stately/tabs": "^3.8.8", "@react-types/shared": "^3.33.0", "@react-types/tabs": "^3.3.21", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-9Gwo118GHrMXSyteCZL1L/LHLVlGSYkhGgiTL3e/UgnYjHfEfDJVTkV2JikuE2O/4uig52gQRlq5E99axLeE9Q=="],
|
||||||
|
|
||||||
|
"@react-aria/tag": ["@react-aria/tag@3.8.0", "", { "dependencies": { "@react-aria/gridlist": "^3.14.3", "@react-aria/i18n": "^3.12.15", "@react-aria/interactions": "^3.27.0", "@react-aria/label": "^3.7.24", "@react-aria/selection": "^3.27.1", "@react-aria/utils": "^3.33.0", "@react-stately/list": "^3.13.3", "@react-types/button": "^3.15.0", "@react-types/shared": "^3.33.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-sTV6uRKFIFU1aljKb0QjM6fPPnzBuitrbkkCUZCJ0w0RIX1JinZPh96NknNtjFwWmqoROjVNCq51EUd0Hh2SQw=="],
|
||||||
|
|
||||||
|
"@react-aria/textfield": ["@react-aria/textfield@3.18.4", "", { "dependencies": { "@react-aria/form": "^3.1.4", "@react-aria/interactions": "^3.27.0", "@react-aria/label": "^3.7.24", "@react-aria/utils": "^3.33.0", "@react-stately/form": "^3.2.3", "@react-stately/utils": "^3.11.0", "@react-types/shared": "^3.33.0", "@react-types/textfield": "^3.12.7", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-ts3Vdy2qNOzjCVeO+4RH8FSgTYN2USAMcYFeGbHOriCukVOrvgRsqcDniW7xaT60LgFdlWMJsCusvltSIyo6xw=="],
|
||||||
|
|
||||||
|
"@react-aria/toast": ["@react-aria/toast@3.0.10", "", { "dependencies": { "@react-aria/i18n": "^3.12.15", "@react-aria/interactions": "^3.27.0", "@react-aria/landmark": "^3.0.9", "@react-aria/utils": "^3.33.0", "@react-stately/toast": "^3.1.3", "@react-types/button": "^3.15.0", "@react-types/shared": "^3.33.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-irW5Cr4msbPo4A4ysjT70MDJbpGCe1h9SkFgdYXBPA4Xbi4jRT7TiEZeIS1I7Hsvp6shAK1Ld/m6NBS0b/gyzg=="],
|
||||||
|
|
||||||
|
"@react-aria/toggle": ["@react-aria/toggle@3.12.4", "", { "dependencies": { "@react-aria/interactions": "^3.27.0", "@react-aria/utils": "^3.33.0", "@react-stately/toggle": "^3.9.4", "@react-types/checkbox": "^3.10.3", "@react-types/shared": "^3.33.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-yVcl8kEFLsV47aCA22EMPcd/KWoYqPIPSzoKjRD/iWmxcP6iGzSxDjdUgMQojNGY8Q6wL8lUxfRqKBjvl/uezQ=="],
|
||||||
|
|
||||||
|
"@react-aria/toolbar": ["@react-aria/toolbar@3.0.0-beta.23", "", { "dependencies": { "@react-aria/focus": "^3.21.4", "@react-aria/i18n": "^3.12.15", "@react-aria/utils": "^3.33.0", "@react-types/shared": "^3.33.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-FzvNf2hWtjEwk8F2MBf4qSs6AAR/p2WFSws6kJ4f0SrWXl4wR9VDEwBEUQcIPbWCK2aUsyOjubCh55Cl4t3MoQ=="],
|
||||||
|
|
||||||
|
"@react-aria/tooltip": ["@react-aria/tooltip@3.9.1", "", { "dependencies": { "@react-aria/interactions": "^3.27.0", "@react-aria/utils": "^3.33.0", "@react-stately/tooltip": "^3.5.10", "@react-types/shared": "^3.33.0", "@react-types/tooltip": "^3.5.1", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-mvEhqpvF4v/wj9zw3a8bsAEnySutGbxKXXt39s6WvF6dkVfaXfsmV9ahuMCHH//UGh/yidZGLrXX4YVdrgS8lA=="],
|
||||||
|
|
||||||
|
"@react-aria/tree": ["@react-aria/tree@3.1.6", "", { "dependencies": { "@react-aria/gridlist": "^3.14.3", "@react-aria/i18n": "^3.12.15", "@react-aria/selection": "^3.27.1", "@react-aria/utils": "^3.33.0", "@react-stately/tree": "^3.9.5", "@react-types/button": "^3.15.0", "@react-types/shared": "^3.33.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-igLX+OQrbXCBLrtPWgUevU0iDrgTSAJh1ncHoPzfD/YDcyTDLqKdy2nZhNbJ/IdHCwTyzIknhFJ700K20Ymw9A=="],
|
||||||
|
|
||||||
|
"@react-aria/utils": ["@react-aria/utils@3.33.0", "", { "dependencies": { "@react-aria/ssr": "^3.9.10", "@react-stately/flags": "^3.1.2", "@react-stately/utils": "^3.11.0", "@react-types/shared": "^3.33.0", "@swc/helpers": "^0.5.0", "clsx": "^2.0.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-yvz7CMH8d2VjwbSa5nGXqjU031tYhD8ddax95VzJsHSPyqHDEGfxul8RkhGV6oO7bVqZxVs6xY66NIgae+FHjw=="],
|
||||||
|
|
||||||
|
"@react-aria/virtualizer": ["@react-aria/virtualizer@4.1.12", "", { "dependencies": { "@react-aria/i18n": "^3.12.15", "@react-aria/interactions": "^3.27.0", "@react-aria/utils": "^3.33.0", "@react-stately/virtualizer": "^4.4.5", "@react-types/shared": "^3.33.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-va0VAD28nq7rk1vHZvnkq591EbWuDKBwh2NzAEn+zz9JjMtpg4utcihNXECJ1DwMRkpaT6q+KpOE7dSdzTxPBQ=="],
|
||||||
|
|
||||||
|
"@react-aria/visually-hidden": ["@react-aria/visually-hidden@3.8.30", "", { "dependencies": { "@react-aria/interactions": "^3.27.0", "@react-aria/utils": "^3.33.0", "@react-types/shared": "^3.33.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-iY44USEU8sJy0NOJ/sTDn3YlspbhHuVG3nx2YYrzfmxbS3i+lNwkCfG8kJ77dtmbuDLIdBGKENjGkbcwz3kiJg=="],
|
||||||
|
|
||||||
|
"@react-stately/autocomplete": ["@react-stately/autocomplete@3.0.0-beta.4", "", { "dependencies": { "@react-stately/utils": "^3.11.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-K2Uy7XEdseFvgwRQ8CyrYEHMupjVKEszddOapP8deNz4hntYvT1aRm0m+sKa5Kl/4kvg9c/3NZpQcrky/vRZIg=="],
|
||||||
|
|
||||||
|
"@react-stately/calendar": ["@react-stately/calendar@3.9.2", "", { "dependencies": { "@internationalized/date": "^3.11.0", "@react-stately/utils": "^3.11.0", "@react-types/calendar": "^3.8.2", "@react-types/shared": "^3.33.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-AQj8/izwb7eY+KFqKcMLI2ygvnbAIwLuQG5KPHgJsMygFqnN4yzXKz5orGqVJnxEXLKiLPteVztx7b5EQobrtw=="],
|
||||||
|
|
||||||
|
"@react-stately/checkbox": ["@react-stately/checkbox@3.7.4", "", { "dependencies": { "@react-stately/form": "^3.2.3", "@react-stately/utils": "^3.11.0", "@react-types/checkbox": "^3.10.3", "@react-types/shared": "^3.33.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-oXHMkK22CWLcmNlunDuu4p52QXYmkpx6es9AjWx/xlh3XLZdJzo/5SANioOH1QvBtwPA/c2KQy+ZBqC21NtMHw=="],
|
||||||
|
|
||||||
|
"@react-stately/collections": ["@react-stately/collections@3.12.9", "", { "dependencies": { "@react-types/shared": "^3.33.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-2jywPMhVgMOh0XtutxPqIxFCIiLOnL/GXIrRKoBEo8M3Q24NoMRBavUrn9RTvjqNnec1i/8w1/8sq8cmCKEohA=="],
|
||||||
|
|
||||||
|
"@react-stately/color": ["@react-stately/color@3.9.4", "", { "dependencies": { "@internationalized/number": "^3.6.5", "@internationalized/string": "^3.2.7", "@react-stately/form": "^3.2.3", "@react-stately/numberfield": "^3.10.4", "@react-stately/slider": "^3.7.4", "@react-stately/utils": "^3.11.0", "@react-types/color": "^3.1.3", "@react-types/shared": "^3.33.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-SprAP5STMg6K0jq+A3UoimsvvTCIGItUtWurS/lDRoQJYajFR8IUdz+mekU/GaXzvFhMN32dijOtFcfxnA4cfA=="],
|
||||||
|
|
||||||
|
"@react-stately/combobox": ["@react-stately/combobox@3.12.2", "", { "dependencies": { "@react-stately/collections": "^3.12.9", "@react-stately/form": "^3.2.3", "@react-stately/list": "^3.13.3", "@react-stately/overlays": "^3.6.22", "@react-stately/utils": "^3.11.0", "@react-types/combobox": "^3.13.11", "@react-types/shared": "^3.33.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-h4YRmzA+s3aMwUrXm6jyWLN0BWWXUNiodArB1wC24xNdeI7S8O3mxz6G2r3Ne8AE02FXmZXs9SD30Mx5vVVuqQ=="],
|
||||||
|
|
||||||
|
"@react-stately/data": ["@react-stately/data@3.15.1", "", { "dependencies": { "@react-types/shared": "^3.33.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-lchubLxCWg1Yswpe9yRYJAjmzP0eTYZe+AQyFJQRIT6axRi9Gs92RIZ7zhwLXxI0vcWpnAWADB9kD4bsos7xww=="],
|
||||||
|
|
||||||
|
"@react-stately/datepicker": ["@react-stately/datepicker@3.16.0", "", { "dependencies": { "@internationalized/date": "^3.11.0", "@internationalized/number": "^3.6.5", "@internationalized/string": "^3.2.7", "@react-stately/form": "^3.2.3", "@react-stately/overlays": "^3.6.22", "@react-stately/utils": "^3.11.0", "@react-types/datepicker": "^3.13.4", "@react-types/shared": "^3.33.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-mYtzKXufFVivrHjmxys3ryJFMPIQNhVqaSItmGnWv3ehxw+0HKBrROf3BFiEN4zP20euoP149ZaR4uNx90kMYw=="],
|
||||||
|
|
||||||
|
"@react-stately/disclosure": ["@react-stately/disclosure@3.0.10", "", { "dependencies": { "@react-stately/utils": "^3.11.0", "@react-types/shared": "^3.33.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-nUistLYMjBDy+yaS5H0y0Dwfcjr12zpIh7vjhQXF4wxIh3D08NRvV1NCQ0LV+IsMej/qoPJvKS4EnXHxBI3GmQ=="],
|
||||||
|
|
||||||
|
"@react-stately/dnd": ["@react-stately/dnd@3.7.3", "", { "dependencies": { "@react-stately/selection": "^3.20.8", "@react-types/shared": "^3.33.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-yBtzAimyYvJWnzP80Scx7l559+43TVSyjaMpUR6/s2IjqD3XoPKgPsv7KaFUmygBTkCBGBFJn404rYgMCOsu3g=="],
|
||||||
|
|
||||||
|
"@react-stately/flags": ["@react-stately/flags@3.1.2", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-2HjFcZx1MyQXoPqcBGALwWWmgFVUk2TuKVIQxCbRq7fPyWXIl6VHcakCLurdtYC2Iks7zizvz0Idv48MQ38DWg=="],
|
||||||
|
|
||||||
|
"@react-stately/form": ["@react-stately/form@3.2.3", "", { "dependencies": { "@react-types/shared": "^3.33.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-NPvjJtns1Pq9uvqeRJCf8HIdVmOm2ARLYQ2F/sqXj1w5IChJ4oWL4Xzvj29/zBitgE1vVjDhnrnwSfNlHZGX0g=="],
|
||||||
|
|
||||||
|
"@react-stately/grid": ["@react-stately/grid@3.11.8", "", { "dependencies": { "@react-stately/collections": "^3.12.9", "@react-stately/selection": "^3.20.8", "@react-types/grid": "^3.3.7", "@react-types/shared": "^3.33.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-tCabR5U7ype+uEElS5Chv5n6ntUv3drXa9DwebjO05cFevUmjTkEfYPJWixpgX4UlCCvjdUFgzeQlJF+gCiozg=="],
|
||||||
|
|
||||||
|
"@react-stately/layout": ["@react-stately/layout@4.5.3", "", { "dependencies": { "@react-stately/collections": "^3.12.9", "@react-stately/table": "^3.15.3", "@react-stately/virtualizer": "^4.4.5", "@react-types/grid": "^3.3.7", "@react-types/shared": "^3.33.0", "@react-types/table": "^3.13.5", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-BDYnvO2AKzvWfxxVM96kif3qCynsA+XcNoQC+T77exH+LLT8zlK9oOdarZXTlok/eZmjs6+5wmjq51PeL6eM5w=="],
|
||||||
|
|
||||||
|
"@react-stately/list": ["@react-stately/list@3.13.3", "", { "dependencies": { "@react-stately/collections": "^3.12.9", "@react-stately/selection": "^3.20.8", "@react-stately/utils": "^3.11.0", "@react-types/shared": "^3.33.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-xN0v7rzhIKshhcshOzx+ZgVngXnGCtMPRdhoDLGaHzQy5YfxvKBMNLCnr5Lm4T1U/kIvHbyzxmr5uwmH8WxoIg=="],
|
||||||
|
|
||||||
|
"@react-stately/menu": ["@react-stately/menu@3.9.10", "", { "dependencies": { "@react-stately/overlays": "^3.6.22", "@react-types/menu": "^3.10.6", "@react-types/shared": "^3.33.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-dY9FzjQ+6iNInVujZPyMklDGoSbaoO0yguUnALAY+yfkPAyStEElfm4aXZgRfNKOTNHe9E34oV7qefSYsclvTg=="],
|
||||||
|
|
||||||
|
"@react-stately/numberfield": ["@react-stately/numberfield@3.10.4", "", { "dependencies": { "@internationalized/number": "^3.6.5", "@react-stately/form": "^3.2.3", "@react-stately/utils": "^3.11.0", "@react-types/numberfield": "^3.8.17", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-EniHHwXOw/Ta0x5j61OvldDAvLoi/8xOo//bzrqwnDvf2/1IKGFMD9CHs7HYhQw+9oNl3Q2V1meOTNPc4PvoMQ=="],
|
||||||
|
|
||||||
|
"@react-stately/overlays": ["@react-stately/overlays@3.6.22", "", { "dependencies": { "@react-stately/utils": "^3.11.0", "@react-types/overlays": "^3.9.3", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-sWBnuy5dqVp8d+1e+ABTRVB3YBcOW86/90pF5PWY44au3bUFXVSUBO2QMdR/6JtojDoPRmrjufonI19/Zs/20w=="],
|
||||||
|
|
||||||
|
"@react-stately/radio": ["@react-stately/radio@3.11.4", "", { "dependencies": { "@react-stately/form": "^3.2.3", "@react-stately/utils": "^3.11.0", "@react-types/radio": "^3.9.3", "@react-types/shared": "^3.33.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-3svsW5VxJA5/p1vO+Qlxv+7Jq9g7f4rqX9Rbqdfd+pH7ykHaV0CUKkSRMaWfcY8Vgaf2xmcc6dvusPRqKX8T1A=="],
|
||||||
|
|
||||||
|
"@react-stately/searchfield": ["@react-stately/searchfield@3.5.18", "", { "dependencies": { "@react-stately/utils": "^3.11.0", "@react-types/searchfield": "^3.6.7", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-C3/1wOON5oK0QBljj0vSbHm/IWgd29NxB+7zT1JjZcxtbcFxCj4HOxKdnPCT/d8Pojb0YS26QgKzatLZ0NnhgQ=="],
|
||||||
|
|
||||||
|
"@react-stately/select": ["@react-stately/select@3.9.1", "", { "dependencies": { "@react-stately/form": "^3.2.3", "@react-stately/list": "^3.13.3", "@react-stately/overlays": "^3.6.22", "@react-stately/utils": "^3.11.0", "@react-types/select": "^3.12.1", "@react-types/shared": "^3.33.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-CJQRqv8Dg+0RRvcig3a2YfY6POJIscDINvidRF31yK6J72rsP01dY3ria9aJjizNDHR9Q5dWFp/z+ii0cOTWIQ=="],
|
||||||
|
|
||||||
|
"@react-stately/selection": ["@react-stately/selection@3.20.8", "", { "dependencies": { "@react-stately/collections": "^3.12.9", "@react-stately/utils": "^3.11.0", "@react-types/shared": "^3.33.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-V1kRN1NLW+i/3Xv+Q0pN9OzuM0zFEW9mdXOOOq7l+YL6hFjqIjttT2/q4KoyiNV3W0hfoRFSTQ7XCgqnqtwEng=="],
|
||||||
|
|
||||||
|
"@react-stately/slider": ["@react-stately/slider@3.7.4", "", { "dependencies": { "@react-stately/utils": "^3.11.0", "@react-types/shared": "^3.33.0", "@react-types/slider": "^3.8.3", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-cSOYSx2nsOQejMg6Ql0+GUpqAiPwRA5teYXUghNvuBDtVxnd4l2rnXs54Ww48tU43xf2+L3kkmMofThjABoEPw=="],
|
||||||
|
|
||||||
|
"@react-stately/table": ["@react-stately/table@3.15.3", "", { "dependencies": { "@react-stately/collections": "^3.12.9", "@react-stately/flags": "^3.1.2", "@react-stately/grid": "^3.11.8", "@react-stately/selection": "^3.20.8", "@react-stately/utils": "^3.11.0", "@react-types/grid": "^3.3.7", "@react-types/shared": "^3.33.0", "@react-types/table": "^3.13.5", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-W1wR0O/PmdD8hCUFIAelHICjUX/Ii6ZldPlH6EILr9olyGpoCaY7XmnyG7kii1aANuQGBeskjJdXvS6LX/gyDw=="],
|
||||||
|
|
||||||
|
"@react-stately/tabs": ["@react-stately/tabs@3.8.8", "", { "dependencies": { "@react-stately/list": "^3.13.3", "@react-types/shared": "^3.33.0", "@react-types/tabs": "^3.3.21", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-BZImWT+pHZitImRQkoL7jVhTtpGPSra1Rhh4pi8epzwogeqseEIEpuWpQebjQP74r1kfNi/iT2p5Qb31eWfh1Q=="],
|
||||||
|
|
||||||
|
"@react-stately/toast": ["@react-stately/toast@3.1.3", "", { "dependencies": { "@swc/helpers": "^0.5.0", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-mT9QJKmD523lqFpOp0VWZ6QHZENFK7HrodnNJDVc7g616s5GNmemdlkITV43fSY3tHeThCVvPu+Uzh7RvQ9mpQ=="],
|
||||||
|
|
||||||
|
"@react-stately/toggle": ["@react-stately/toggle@3.9.4", "", { "dependencies": { "@react-stately/utils": "^3.11.0", "@react-types/checkbox": "^3.10.3", "@react-types/shared": "^3.33.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-tjWsshRJtHC+PI5NYMlnDlV/BTo1eWq6fmR6x1mXlQfKuKGTJRzhgJyaQ2mc5K+LkifD7fchOhfapHCrRlzwMg=="],
|
||||||
|
|
||||||
|
"@react-stately/tooltip": ["@react-stately/tooltip@3.5.10", "", { "dependencies": { "@react-stately/overlays": "^3.6.22", "@react-types/tooltip": "^3.5.1", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-GauUdc6Of08Np2iUw4xx/DdgpvszS9CxJWYcRnNyAAGPLQrmniVrpJvb0EUKQTP9sUSci1SlmpvJh4SNZx26Bw=="],
|
||||||
|
|
||||||
|
"@react-stately/tree": ["@react-stately/tree@3.9.5", "", { "dependencies": { "@react-stately/collections": "^3.12.9", "@react-stately/selection": "^3.20.8", "@react-stately/utils": "^3.11.0", "@react-types/shared": "^3.33.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-UpvBlzL/MpFdOepDg+cohI/zvw8DEVM8cXY/OZ8tKUXWpew1HpUglwnAI3ivm0L2k9laUIB9siW0g04ZWiH9Lg=="],
|
||||||
|
|
||||||
|
"@react-stately/utils": ["@react-stately/utils@3.11.0", "", { "dependencies": { "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-8LZpYowJ9eZmmYLpudbo/eclIRnbhWIJZ994ncmlKlouNzKohtM8qTC6B1w1pwUbiwGdUoyzLuQbeaIor5Dvcw=="],
|
||||||
|
|
||||||
|
"@react-stately/virtualizer": ["@react-stately/virtualizer@4.4.5", "", { "dependencies": { "@react-types/shared": "^3.33.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-MP33zys3nRYTk/+3BPchxlil9GrwbMksc3XuvNACeZqYEA/oEidsHffgPL+LY0iitKCmQE6pg49MI5HvBuOd2w=="],
|
||||||
|
|
||||||
|
"@react-types/autocomplete": ["@react-types/autocomplete@3.0.0-alpha.37", "", { "dependencies": { "@react-types/combobox": "^3.13.11", "@react-types/searchfield": "^3.6.7", "@react-types/shared": "^3.33.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-9KkL/UEUHIqp4OD4PffeZPiRV93ZBKq84sBrzTbTIPN+os+N+Lfz45Mg67NM2RumR/KQSVE0gZp7OA0eOvxPYA=="],
|
||||||
|
|
||||||
|
"@react-types/breadcrumbs": ["@react-types/breadcrumbs@3.7.18", "", { "dependencies": { "@react-types/link": "^3.6.6", "@react-types/shared": "^3.33.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-zwltqx2XSELBRQeuCraxrdfT4fpIOVu6eQXsZ4RhWlsT7DLhzj3pUGkxdPDAMfYaVdyNBqc+nhiAnCwz6tUJ8A=="],
|
||||||
|
|
||||||
|
"@react-types/button": ["@react-types/button@3.15.0", "", { "dependencies": { "@react-types/shared": "^3.33.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-X/K2/Oeuq7Hi8nMIzx4/YlZuvWFiSOHZt27p4HmThCnNO/9IDFPmvPrpkYjWN5eN9Nuk+P5vZUb4A7QJgYpvGA=="],
|
||||||
|
|
||||||
|
"@react-types/calendar": ["@react-types/calendar@3.8.2", "", { "dependencies": { "@internationalized/date": "^3.11.0", "@react-types/shared": "^3.33.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-QbPFhvBQfrsz3x1Nnatr5SL+8XtbxvP4obESFuDrKmsqaaAv+jG5vwLiPTKp6Z3L+MWkCvKavBPuW+byhq+69A=="],
|
||||||
|
|
||||||
|
"@react-types/checkbox": ["@react-types/checkbox@3.10.3", "", { "dependencies": { "@react-types/shared": "^3.33.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-Xw4jHG7uK352Wc18XXzdzmtr3Xjg8d2tPoBGNgsw39f92EY2UpoDAPHxYR0BaDe04lGfAn6YwVivI4OGVbjXIg=="],
|
||||||
|
|
||||||
|
"@react-types/color": ["@react-types/color@3.1.3", "", { "dependencies": { "@react-types/shared": "^3.33.0", "@react-types/slider": "^3.8.3" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-XM0x8iZpAf036w9qceD2RFroehLxKRwkVer7EvdJNs8K8iUN8TuhCagzsomiSJtyYh5MFysEVQ2ir85toiAFyw=="],
|
||||||
|
|
||||||
|
"@react-types/combobox": ["@react-types/combobox@3.13.11", "", { "dependencies": { "@react-types/shared": "^3.33.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-5/tdmTAvqPpiWzEeaV7uLLSbSTkkoQ1mVz6NfKMPuw4ZBkY3lPc9JDkkQjY/JrquZao+KY4Dx8ZIoS0NqkrFrw=="],
|
||||||
|
|
||||||
|
"@react-types/datepicker": ["@react-types/datepicker@3.13.4", "", { "dependencies": { "@internationalized/date": "^3.11.0", "@react-types/calendar": "^3.8.2", "@react-types/overlays": "^3.9.3", "@react-types/shared": "^3.33.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-B5sAPoYZfluDBpgVK3ADlHbXBKRkFCQFO18Bs091IvRRwqzfoO/uf+/9UpXMw+BEF4pciLf0/kdiVQTvI3MzlA=="],
|
||||||
|
|
||||||
|
"@react-types/dialog": ["@react-types/dialog@3.5.23", "", { "dependencies": { "@react-types/overlays": "^3.9.3", "@react-types/shared": "^3.33.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-3tMzweYuaDOaufF5tZPMgXSA0pPFJNgdg89YRITh0wMXMG0pm+tAKVQJL1TSLLhOiLCEL08V8M/AK67dBdr2IA=="],
|
||||||
|
|
||||||
|
"@react-types/form": ["@react-types/form@3.7.17", "", { "dependencies": { "@react-types/shared": "^3.33.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-wBFRJ3jehHw2X2Td/KwUNxFWOqXCK7OTGG9A+W3ZI3nDGyflHQpIjqKCKV1jRySs6sv7huiPckJ7ScDleCKf7w=="],
|
||||||
|
|
||||||
|
"@react-types/grid": ["@react-types/grid@3.3.7", "", { "dependencies": { "@react-types/shared": "^3.33.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-riET3xeKPTcRWQy6hYCMxdbdL3yubPY5Ow66b2GA2rEqoYvmDBniYXAM2Oh+q9s+YgnAP7qJK++ym8NljvHiLA=="],
|
||||||
|
|
||||||
|
"@react-types/link": ["@react-types/link@3.6.6", "", { "dependencies": { "@react-types/shared": "^3.33.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-M6WXxUJFmiF6GNu7xUH0uHj0jsorFBN6npkfSCNM4puStC8NbUT2+ZPySQyZXCoHMQ89g6qZ6vCc8QduVkTE7Q=="],
|
||||||
|
|
||||||
|
"@react-types/listbox": ["@react-types/listbox@3.7.5", "", { "dependencies": { "@react-types/shared": "^3.33.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-Cn+yNip+YZBaGzu+z5xPNgmfSupnLl+li7uG5hRc+EArkk8/G42myRXz6M8wPrLM1bFAq3r85tAbyoXVmKG5Jw=="],
|
||||||
|
|
||||||
|
"@react-types/menu": ["@react-types/menu@3.10.6", "", { "dependencies": { "@react-types/overlays": "^3.9.3", "@react-types/shared": "^3.33.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-OJTznQ4xE/VddBJU+HO4x5tceSOdyQhiHA1bREE1aHl+PcgHOUZLdMjXp1zFaGF16HhItHJaxpifJ4hzf4hWQA=="],
|
||||||
|
|
||||||
|
"@react-types/meter": ["@react-types/meter@3.4.14", "", { "dependencies": { "@react-types/progress": "^3.5.17" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-rNw0Do2AM3zLGZ0pSWweViuddg1uW99PWzE6RQXE8nsTHTeiwDZt9SYGdObEnjd+nJ3YzemqekG0Kqt93iNBcA=="],
|
||||||
|
|
||||||
|
"@react-types/numberfield": ["@react-types/numberfield@3.8.17", "", { "dependencies": { "@react-types/shared": "^3.33.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-Q9n24OaSMXrebMowbtowmHLNclknN3XkcBIaYMwA2BIGIl+fZFnI8MERM0pG87W+wki6FepDExsDW9YxQF4pnw=="],
|
||||||
|
|
||||||
|
"@react-types/overlays": ["@react-types/overlays@3.9.3", "", { "dependencies": { "@react-types/shared": "^3.33.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-LzetThNNk8T26pQRbs1I7+isuFhdFYREy7wJCsZmbB0FnZgCukGTfOtThZWv+ry11veyVJiX68jfl4SV6ACTWA=="],
|
||||||
|
|
||||||
|
"@react-types/progress": ["@react-types/progress@3.5.17", "", { "dependencies": { "@react-types/shared": "^3.33.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-JtiGlek6QS04bFrRj1WfChjPNr7+3/+pd6yZayXGUkQUPHt1Z/cFnv3QZ/tSQTdUt1XXmjnCak9ZH9JQBqe64Q=="],
|
||||||
|
|
||||||
|
"@react-types/radio": ["@react-types/radio@3.9.3", "", { "dependencies": { "@react-types/shared": "^3.33.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-w2BrMGIiZxYXPCnnB2NQyifwE/rRFMIW87MyawrKO9zPSbnDkqLIHAAtqmlNk2zkz1ZEWjk9opNsuztjP7D4sA=="],
|
||||||
|
|
||||||
|
"@react-types/searchfield": ["@react-types/searchfield@3.6.7", "", { "dependencies": { "@react-types/shared": "^3.33.0", "@react-types/textfield": "^3.12.7" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-POo3spZcYD14aqo0f4eNbymJ8w9EKrlu0pOOjYYWI2P0GUSRmib9cBA9xZFhvRGHuNlHo3ePjeFitYQI7L3g1g=="],
|
||||||
|
|
||||||
|
"@react-types/select": ["@react-types/select@3.12.1", "", { "dependencies": { "@react-types/shared": "^3.33.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-PtIUymvQNIIzgr+piJtK/8gbH7akWtbswIbfoADPSxtZEd1/vfUIO0s8c750s3XYNlmx/4DrhugQsLYwgC35yg=="],
|
||||||
|
|
||||||
|
"@react-types/shared": ["@react-types/shared@3.33.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-xuUpP6MyuPmJtzNOqF5pzFUIHH2YogyOQfUQHag54PRmWB7AbjuGWBUv0l1UDmz6+AbzAYGmDVAzcRDOu2PFpw=="],
|
||||||
|
|
||||||
|
"@react-types/slider": ["@react-types/slider@3.8.3", "", { "dependencies": { "@react-types/shared": "^3.33.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-HCDegYiUA27CcJKvFwgpR8ktFKf2nAirXqQEgVPV4uxk6JIeiRx41yqM/xPJGfmaqa7BARYARLT41yN2V8Kadg=="],
|
||||||
|
|
||||||
|
"@react-types/switch": ["@react-types/switch@3.5.16", "", { "dependencies": { "@react-types/shared": "^3.33.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-6fynclkyg0wGHo3f1bwk4Z+gZZEg0Z63iP5TFhgHWdZ8W+Uq6F3u7V4IgQpuJ2NleL1c2jy2/CKdS9v06ac2Og=="],
|
||||||
|
|
||||||
|
"@react-types/table": ["@react-types/table@3.13.5", "", { "dependencies": { "@react-types/grid": "^3.3.7", "@react-types/shared": "^3.33.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-4/CixlNmXSuJuX2IKuUlgNd/dEgNh3WvfE/bdwuI1t5JBdShP9tHIzSkgZbrzE2xX46NeA2xq4vXNO5kBv+QDA=="],
|
||||||
|
|
||||||
|
"@react-types/tabs": ["@react-types/tabs@3.3.21", "", { "dependencies": { "@react-types/shared": "^3.33.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-Dq9bKI62rHoI4LGGcBGlZ5s0aSwB0G4Y8o0r7hQZvf1eZWc9fmqdAdTTaGG/RUyhMIGRYWl5RRUBUuC5RmaO6w=="],
|
||||||
|
|
||||||
|
"@react-types/textfield": ["@react-types/textfield@3.12.7", "", { "dependencies": { "@react-types/shared": "^3.33.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-ddiacsS6sLFtAn2/fym7lR8nbdsLgPfelNDcsDqHiu6XUHh5TCNe8ItXHFaIiyfnKTH8uJqZrSli4wfAYNfMsw=="],
|
||||||
|
|
||||||
|
"@react-types/tooltip": ["@react-types/tooltip@3.5.1", "", { "dependencies": { "@react-types/overlays": "^3.9.3", "@react-types/shared": "^3.33.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-h6xOAWbWUJKs9CzcCyzSPATLHq7W5dS866HkXLrtCrRDShLuzQnojZnctD2tKtNt17990hjnOhl36GUBuO5kyw=="],
|
||||||
|
|
||||||
|
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.47", "", {}, "sha512-8QagwMH3kNCuzD8EWL8R2YPW5e4OrHNSAHRFDdmFqEwEaD/KcNKjVoumo+gP2vW5eKB2UPbM6vTYiGZX0ixLnw=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.56.0", "", { "os": "android", "cpu": "arm" }, "sha512-LNKIPA5k8PF1+jAFomGe3qN3bbIgJe/IlpDBwuVjrDKrJhVWywgnJvflMt/zkbVNLFtF1+94SljYQS6e99klnw=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.56.0", "", { "os": "android", "cpu": "arm64" }, "sha512-lfbVUbelYqXlYiU/HApNMJzT1E87UPGvzveGg2h0ktUNlOCxKlWuJ9jtfvs1sKHdwU4fzY7Pl8sAl49/XaEk6Q=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.56.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-EgxD1ocWfhoD6xSOeEEwyE7tDvwTgZc8Bss7wCWe+uc7wO8G34HHCUH+Q6cHqJubxIAnQzAsyUsClt0yFLu06w=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.56.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-1vXe1vcMOssb/hOF8iv52A7feWW2xnu+c8BV4t1F//m9QVLTfNVpEdja5ia762j/UEJe2Z1jAmEqZAK42tVW3g=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.56.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-bof7fbIlvqsyv/DtaXSck4VYQ9lPtoWNFCB/JY4snlFuJREXfZnm+Ej6yaCHfQvofJDXLDMTVxWscVSuQvVWUQ=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.56.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-KNa6lYHloW+7lTEkYGa37fpvPq+NKG/EHKM8+G/g9WDU7ls4sMqbVRV78J6LdNuVaeeK5WB9/9VAFbKxcbXKYg=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.56.0", "", { "os": "linux", "cpu": "arm" }, "sha512-E8jKK87uOvLrrLN28jnAAAChNq5LeCd2mGgZF+fGF5D507WlG/Noct3lP/QzQ6MrqJ5BCKNwI9ipADB6jyiq2A=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.56.0", "", { "os": "linux", "cpu": "arm" }, "sha512-jQosa5FMYF5Z6prEpTCCmzCXz6eKr/tCBssSmQGEeozA9tkRUty/5Vx06ibaOP9RCrW1Pvb8yp3gvZhHwTDsJw=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.56.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-uQVoKkrC1KGEV6udrdVahASIsaF8h7iLG0U0W+Xn14ucFwi6uS539PsAr24IEF9/FoDtzMeeJXJIBo5RkbNWvQ=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.56.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-vLZ1yJKLxhQLFKTs42RwTwa6zkGln+bnXc8ueFGMYmBTLfNu58sl5/eXyxRa2RarTkJbXl8TKPgfS6V5ijNqEA=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.56.0", "", { "os": "linux", "cpu": "none" }, "sha512-FWfHOCub564kSE3xJQLLIC/hbKqHSVxy8vY75/YHHzWvbJL7aYJkdgwD/xGfUlL5UV2SB7otapLrcCj2xnF1dg=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.56.0", "", { "os": "linux", "cpu": "none" }, "sha512-z1EkujxIh7nbrKL1lmIpqFTc/sr0u8Uk0zK/qIEFldbt6EDKWFk/pxFq3gYj4Bjn3aa9eEhYRlL3H8ZbPT1xvA=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.56.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-iNFTluqgdoQC7AIE8Q34R3AuPrJGJirj5wMUErxj22deOcY7XwZRaqYmB6ZKFHoVGqRcRd0mqO+845jAibKCkw=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.56.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-MtMeFVlD2LIKjp2sE2xM2slq3Zxf9zwVuw0jemsxvh1QOpHSsSzfNOTH9uYW9i1MXFxUSMmLpeVeUzoNOKBaWg=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.56.0", "", { "os": "linux", "cpu": "none" }, "sha512-in+v6wiHdzzVhYKXIk5U74dEZHdKN9KH0Q4ANHOTvyXPG41bajYRsy7a8TPKbYPl34hU7PP7hMVHRvv/5aCSew=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.56.0", "", { "os": "linux", "cpu": "none" }, "sha512-yni2raKHB8m9NQpI9fPVwN754mn6dHQSbDTwxdr9SE0ks38DTjLMMBjrwvB5+mXrX+C0npX0CVeCUcvvvD8CNQ=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.56.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-zhLLJx9nQPu7wezbxt2ut+CI4YlXi68ndEve16tPc/iwoylWS9B3FxpLS2PkmfYgDQtosah07Mj9E0khc3Y+vQ=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.56.0", "", { "os": "linux", "cpu": "x64" }, "sha512-MVC6UDp16ZSH7x4rtuJPAEoE1RwS8N4oK9DLHy3FTEdFoUTCFVzMfJl/BVJ330C+hx8FfprA5Wqx4FhZXkj2Kw=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.56.0", "", { "os": "linux", "cpu": "x64" }, "sha512-ZhGH1eA4Qv0lxaV00azCIS1ChedK0V32952Md3FtnxSqZTBTd6tgil4nZT5cU8B+SIw3PFYkvyR4FKo2oyZIHA=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.56.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-O16XcmyDeFI9879pEcmtWvD/2nyxR9mF7Gs44lf1vGGx8Vg2DRNx11aVXBEqOQhWb92WN4z7fW/q4+2NYzCbBA=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.56.0", "", { "os": "none", "cpu": "arm64" }, "sha512-LhN/Reh+7F3RCgQIRbgw8ZMwUwyqJM+8pXNT6IIJAqm2IdKkzpCh/V9EdgOMBKuebIrzswqy4ATlrDgiOwbRcQ=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.56.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-kbFsOObXp3LBULg1d3JIUQMa9Kv4UitDmpS+k0tinPBz3watcUiV2/LUDMMucA6pZO3WGE27P7DsfaN54l9ing=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.56.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-vSSgny54D6P4vf2izbtFm/TcWYedw7f8eBrOiGGecyHyQB9q4Kqentjaj8hToe+995nob/Wv48pDqL5a62EWtg=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.56.0", "", { "os": "win32", "cpu": "x64" }, "sha512-FeCnkPCTHQJFbiGG49KjV5YGW/8b9rrXAM2Mz2kiIoktq2qsJxRD5giEMEOD2lPdgs72upzefaUvS+nc8E3UzQ=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.56.0", "", { "os": "win32", "cpu": "x64" }, "sha512-H8AE9Ur/t0+1VXujj90w0HrSOuv0Nq9r1vSZF2t5km20NTfosQsGGUXDaKdQZzwuLts7IyL1fYT4hM95TI9c4g=="],
|
||||||
|
|
||||||
|
"@swc/core": ["@swc/core@1.15.10", "", { "dependencies": { "@swc/counter": "^0.1.3", "@swc/types": "^0.1.25" }, "optionalDependencies": { "@swc/core-darwin-arm64": "1.15.10", "@swc/core-darwin-x64": "1.15.10", "@swc/core-linux-arm-gnueabihf": "1.15.10", "@swc/core-linux-arm64-gnu": "1.15.10", "@swc/core-linux-arm64-musl": "1.15.10", "@swc/core-linux-x64-gnu": "1.15.10", "@swc/core-linux-x64-musl": "1.15.10", "@swc/core-win32-arm64-msvc": "1.15.10", "@swc/core-win32-ia32-msvc": "1.15.10", "@swc/core-win32-x64-msvc": "1.15.10" }, "peerDependencies": { "@swc/helpers": ">=0.5.17" }, "optionalPeers": ["@swc/helpers"] }, "sha512-udNofxftduMUEv7nqahl2nvodCiCDQ4Ge0ebzsEm6P8s0RC2tBM0Hqx0nNF5J/6t9uagFJyWIDjXy3IIWMHDJw=="],
|
||||||
|
|
||||||
|
"@swc/core-darwin-arm64": ["@swc/core-darwin-arm64@1.15.10", "", { "os": "darwin", "cpu": "arm64" }, "sha512-U72pGqmJYbjrLhMndIemZ7u9Q9owcJczGxwtfJlz/WwMaGYAV/g4nkGiUVk/+QSX8sFCAjanovcU1IUsP2YulA=="],
|
||||||
|
|
||||||
|
"@swc/core-darwin-x64": ["@swc/core-darwin-x64@1.15.10", "", { "os": "darwin", "cpu": "x64" }, "sha512-NZpDXtwHH083L40xdyj1sY31MIwLgOxKfZEAGCI8xHXdHa+GWvEiVdGiu4qhkJctoHFzAEc7ZX3GN5phuJcPuQ=="],
|
||||||
|
|
||||||
|
"@swc/core-linux-arm-gnueabihf": ["@swc/core-linux-arm-gnueabihf@1.15.10", "", { "os": "linux", "cpu": "arm" }, "sha512-ioieF5iuRziUF1HkH1gg1r93e055dAdeBAPGAk40VjqpL5/igPJ/WxFHGvc6WMLhUubSJI4S0AiZAAhEAp1jDg=="],
|
||||||
|
|
||||||
|
"@swc/core-linux-arm64-gnu": ["@swc/core-linux-arm64-gnu@1.15.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-tD6BClOrxSsNus9cJL7Gxdv7z7Y2hlyvZd9l0NQz+YXzmTWqnfzLpg16ovEI7gknH2AgDBB5ywOsqu8hUgSeEQ=="],
|
||||||
|
|
||||||
|
"@swc/core-linux-arm64-musl": ["@swc/core-linux-arm64-musl@1.15.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-4uAHO3nbfbrTcmO/9YcVweTQdx5fN3l7ewwl5AEK4yoC4wXmoBTEPHAVdKNe4r9+xrTgd4BgyPsy0409OjjlMw=="],
|
||||||
|
|
||||||
|
"@swc/core-linux-x64-gnu": ["@swc/core-linux-x64-gnu@1.15.10", "", { "os": "linux", "cpu": "x64" }, "sha512-W0h9ONNw1pVIA0cN7wtboOSTl4Jk3tHq+w2cMPQudu9/+3xoCxpFb9ZdehwCAk29IsvdWzGzY6P7dDVTyFwoqg=="],
|
||||||
|
|
||||||
|
"@swc/core-linux-x64-musl": ["@swc/core-linux-x64-musl@1.15.10", "", { "os": "linux", "cpu": "x64" }, "sha512-XQNZlLZB62S8nAbw7pqoqwy91Ldy2RpaMRqdRN3T+tAg6Xg6FywXRKCsLh6IQOadr4p1+lGnqM/Wn35z5a/0Vw=="],
|
||||||
|
|
||||||
|
"@swc/core-win32-arm64-msvc": ["@swc/core-win32-arm64-msvc@1.15.10", "", { "os": "win32", "cpu": "arm64" }, "sha512-qnAGrRv5Nj/DATxAmCnJQRXXQqnJwR0trxLndhoHoxGci9MuguNIjWahS0gw8YZFjgTinbTxOwzatkoySihnmw=="],
|
||||||
|
|
||||||
|
"@swc/core-win32-ia32-msvc": ["@swc/core-win32-ia32-msvc@1.15.10", "", { "os": "win32", "cpu": "ia32" }, "sha512-i4X/q8QSvzVlaRtv1xfnfl+hVKpCfiJ+9th484rh937fiEZKxZGf51C+uO0lfKDP1FfnT6C1yBYwHy7FLBVXFw=="],
|
||||||
|
|
||||||
|
"@swc/core-win32-x64-msvc": ["@swc/core-win32-x64-msvc@1.15.10", "", { "os": "win32", "cpu": "x64" }, "sha512-HvY8XUFuoTXn6lSccDLYFlXv1SU/PzYi4PyUqGT++WfTnbw/68N/7BdUZqglGRwiSqr0qhYt/EhmBpULj0J9rA=="],
|
||||||
|
|
||||||
|
"@swc/counter": ["@swc/counter@0.1.3", "", {}, "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ=="],
|
||||||
|
|
||||||
|
"@swc/helpers": ["@swc/helpers@0.5.18", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ=="],
|
||||||
|
|
||||||
|
"@swc/types": ["@swc/types@0.1.25", "", { "dependencies": { "@swc/counter": "^0.1.3" } }, "sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g=="],
|
||||||
|
|
||||||
|
"@tailwindcss/node": ["@tailwindcss/node@4.1.18", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.6.1", "lightningcss": "1.30.2", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.1.18" } }, "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.18", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.18", "@tailwindcss/oxide-darwin-arm64": "4.1.18", "@tailwindcss/oxide-darwin-x64": "4.1.18", "@tailwindcss/oxide-freebsd-x64": "4.1.18", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", "@tailwindcss/oxide-linux-x64-musl": "4.1.18", "@tailwindcss/oxide-wasm32-wasi": "4.1.18", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" } }, "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.18", "", { "os": "android", "cpu": "arm64" }, "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.18", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.18", "", { "os": "darwin", "cpu": "x64" }, "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.18", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18", "", { "os": "linux", "cpu": "arm" }, "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.18", "", { "os": "linux", "cpu": "arm64" }, "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.18", "", { "os": "linux", "cpu": "arm64" }, "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.18", "", { "os": "linux", "cpu": "x64" }, "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.18", "", { "os": "linux", "cpu": "x64" }, "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.18", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.1.0", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.4.0" }, "cpu": "none" }, "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.18", "", { "os": "win32", "cpu": "arm64" }, "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.18", "", { "os": "win32", "cpu": "x64" }, "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q=="],
|
||||||
|
|
||||||
|
"@tailwindcss/postcss": ["@tailwindcss/postcss@4.1.18", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.1.18", "@tailwindcss/oxide": "4.1.18", "postcss": "^8.4.41", "tailwindcss": "4.1.18" } }, "sha512-Ce0GFnzAOuPyfV5SxjXGn0CubwGcuDB0zcdaPuCSzAa/2vII24JTkH+I6jcbXLb1ctjZMZZI6OjDaLPJQL1S0g=="],
|
||||||
|
|
||||||
|
"@tailwindcss/typography": ["@tailwindcss/typography@0.5.19", "", { "dependencies": { "postcss-selector-parser": "6.0.10" }, "peerDependencies": { "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" } }, "sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg=="],
|
||||||
|
|
||||||
|
"@tailwindcss/vite": ["@tailwindcss/vite@4.1.18", "", { "dependencies": { "@tailwindcss/node": "4.1.18", "@tailwindcss/oxide": "4.1.18", "tailwindcss": "4.1.18" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA=="],
|
||||||
|
|
||||||
|
"@trivago/prettier-plugin-sort-imports": ["@trivago/prettier-plugin-sort-imports@5.2.2", "", { "dependencies": { "@babel/generator": "^7.26.5", "@babel/parser": "^7.26.7", "@babel/traverse": "^7.26.7", "@babel/types": "^7.26.7", "javascript-natural-sort": "^0.7.1", "lodash": "^4.17.21" }, "peerDependencies": { "@vue/compiler-sfc": "3.x", "prettier": "2.x - 3.x", "prettier-plugin-svelte": "3.x", "svelte": "4.x || 5.x" }, "optionalPeers": ["@vue/compiler-sfc", "prettier-plugin-svelte", "svelte"] }, "sha512-fYDQA9e6yTNmA13TLVSA+WMQRc5Bn/c0EUBditUHNfMMxN7M82c38b1kEggVE3pLpZ0FwkwJkUEKMiOi52JXFA=="],
|
||||||
|
|
||||||
|
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
||||||
|
|
||||||
|
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
|
||||||
|
|
||||||
|
"@types/node": ["@types/node@24.10.9", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-ne4A0IpG3+2ETuREInjPNhUGis1SFjv1d5asp8MzEAGtOZeTeHVDOYqOgqfhvseqg/iXty2hjBf1zAOb7RNiNw=="],
|
||||||
|
|
||||||
|
"@types/react": ["@types/react@19.2.9", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-Lpo8kgb/igvMIPeNV2rsYKTgaORYdO1XGVZ4Qz3akwOj0ySGYMPlQWa8BaLn0G63D1aSaAQ5ldR06wCpChQCjA=="],
|
||||||
|
|
||||||
|
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
|
||||||
|
|
||||||
|
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.53.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.53.1", "@typescript-eslint/type-utils": "8.53.1", "@typescript-eslint/utils": "8.53.1", "@typescript-eslint/visitor-keys": "8.53.1", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.53.1", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-cFYYFZ+oQFi6hUnBTbLRXfTJiaQtYE3t4O692agbBl+2Zy+eqSKWtPjhPXJu1G7j4RLjKgeJPDdq3EqOwmX5Ag=="],
|
||||||
|
|
||||||
|
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.53.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.53.1", "@typescript-eslint/types": "8.53.1", "@typescript-eslint/typescript-estree": "8.53.1", "@typescript-eslint/visitor-keys": "8.53.1", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-nm3cvFN9SqZGXjmw5bZ6cGmvJSyJPn0wU9gHAZZHDnZl2wF9PhHv78Xf06E0MaNk4zLVHL8hb2/c32XvyJOLQg=="],
|
||||||
|
|
||||||
|
"@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.53.1", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.53.1", "@typescript-eslint/types": "^8.53.1", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-WYC4FB5Ra0xidsmlPb+1SsnaSKPmS3gsjIARwbEkHkoWloQmuzcfypljaJcR78uyLA1h8sHdWWPHSLDI+MtNog=="],
|
||||||
|
|
||||||
|
"@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.53.1", "", { "dependencies": { "@typescript-eslint/types": "8.53.1", "@typescript-eslint/visitor-keys": "8.53.1" } }, "sha512-Lu23yw1uJMFY8cUeq7JlrizAgeQvWugNQzJp8C3x8Eo5Jw5Q2ykMdiiTB9vBVOOUBysMzmRRmUfwFrZuI2C4SQ=="],
|
||||||
|
|
||||||
|
"@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.53.1", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-qfvLXS6F6b1y43pnf0pPbXJ+YoXIC7HKg0UGZ27uMIemKMKA6XH2DTxsEDdpdN29D+vHV07x/pnlPNVLhdhWiA=="],
|
||||||
|
|
||||||
|
"@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.53.1", "", { "dependencies": { "@typescript-eslint/types": "8.53.1", "@typescript-eslint/typescript-estree": "8.53.1", "@typescript-eslint/utils": "8.53.1", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-MOrdtNvyhy0rHyv0ENzub1d4wQYKb2NmIqG7qEqPWFW7Mpy2jzFC3pQ2yKDvirZB7jypm5uGjF2Qqs6OIqu47w=="],
|
||||||
|
|
||||||
|
"@typescript-eslint/types": ["@typescript-eslint/types@8.53.1", "", {}, "sha512-jr/swrr2aRmUAUjW5/zQHbMaui//vQlsZcJKijZf3M26bnmLj8LyZUpj8/Rd6uzaek06OWsqdofN/Thenm5O8A=="],
|
||||||
|
|
||||||
|
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.53.1", "", { "dependencies": { "@typescript-eslint/project-service": "8.53.1", "@typescript-eslint/tsconfig-utils": "8.53.1", "@typescript-eslint/types": "8.53.1", "@typescript-eslint/visitor-keys": "8.53.1", "debug": "^4.4.3", "minimatch": "^9.0.5", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-RGlVipGhQAG4GxV1s34O91cxQ/vWiHJTDHbXRr0li2q/BGg3RR/7NM8QDWgkEgrwQYCvmJV9ichIwyoKCQ+DTg=="],
|
||||||
|
|
||||||
|
"@typescript-eslint/utils": ["@typescript-eslint/utils@8.53.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.53.1", "@typescript-eslint/types": "8.53.1", "@typescript-eslint/typescript-estree": "8.53.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-c4bMvGVWW4hv6JmDUEG7fSYlWOl3II2I4ylt0NM+seinYQlZMQIaKaXIIVJWt9Ofh6whrpM+EdDQXKXjNovvrg=="],
|
||||||
|
|
||||||
|
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.53.1", "", { "dependencies": { "@typescript-eslint/types": "8.53.1", "eslint-visitor-keys": "^4.2.1" } }, "sha512-oy+wV7xDKFPRyNggmXuZQSBzvoLnpmJs+GhzRhPjrxl2b/jIlyjVokzm47CZCDUdXKr2zd7ZLodPfOBpOPyPlg=="],
|
||||||
|
|
||||||
|
"@untitledui/file-icons": ["@untitledui/file-icons@0.0.8", "", { "peerDependencies": { "react": ">= 18" } }, "sha512-lxOp2rreDedjD82SOKoETmCDhm5TckCD3y49EEsuZqxnFES/3bfg9drNiilOCOAuEco7ImvdB4L//I//B93asw=="],
|
||||||
|
|
||||||
|
"@untitledui/icons": ["@untitledui/icons@0.0.21", "", { "peerDependencies": { "react": ">= 16" } }, "sha512-+aVWUw/1se9PIJgwMD3qp5ohynGlVNu2oHSLzMwYpiSFp6bFcxv4kUGy685hQXB/hE4WAqnp7dIEVVMD0tfL7w=="],
|
||||||
|
|
||||||
|
"@vitejs/plugin-react-swc": ["@vitejs/plugin-react-swc@4.2.2", "", { "dependencies": { "@rolldown/pluginutils": "1.0.0-beta.47", "@swc/core": "^1.13.5" }, "peerDependencies": { "vite": "^4 || ^5 || ^6 || ^7" } }, "sha512-x+rE6tsxq/gxrEJN3Nv3dIV60lFflPj94c90b+NNo6n1QV1QQUTLoL0MpaOVasUZ0zqVBn7ead1B5ecx1JAGfA=="],
|
||||||
|
|
||||||
|
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
|
||||||
|
|
||||||
|
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
|
||||||
|
|
||||||
|
"ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
|
||||||
|
|
||||||
|
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||||
|
|
||||||
|
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
|
||||||
|
|
||||||
|
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||||
|
|
||||||
|
"brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
|
||||||
|
|
||||||
|
"callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
|
||||||
|
|
||||||
|
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
||||||
|
|
||||||
|
"client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="],
|
||||||
|
|
||||||
|
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
||||||
|
|
||||||
|
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
|
||||||
|
|
||||||
|
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
|
||||||
|
|
||||||
|
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
|
||||||
|
|
||||||
|
"cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
|
||||||
|
|
||||||
|
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
||||||
|
|
||||||
|
"cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="],
|
||||||
|
|
||||||
|
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
|
||||||
|
|
||||||
|
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||||
|
|
||||||
|
"decimal.js": ["decimal.js@10.6.0", "", {}, "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg=="],
|
||||||
|
|
||||||
|
"deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
|
||||||
|
|
||||||
|
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||||
|
|
||||||
|
"enhanced-resolve": ["enhanced-resolve@5.18.4", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q=="],
|
||||||
|
|
||||||
|
"esbuild": ["esbuild@0.27.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.2", "@esbuild/android-arm": "0.27.2", "@esbuild/android-arm64": "0.27.2", "@esbuild/android-x64": "0.27.2", "@esbuild/darwin-arm64": "0.27.2", "@esbuild/darwin-x64": "0.27.2", "@esbuild/freebsd-arm64": "0.27.2", "@esbuild/freebsd-x64": "0.27.2", "@esbuild/linux-arm": "0.27.2", "@esbuild/linux-arm64": "0.27.2", "@esbuild/linux-ia32": "0.27.2", "@esbuild/linux-loong64": "0.27.2", "@esbuild/linux-mips64el": "0.27.2", "@esbuild/linux-ppc64": "0.27.2", "@esbuild/linux-riscv64": "0.27.2", "@esbuild/linux-s390x": "0.27.2", "@esbuild/linux-x64": "0.27.2", "@esbuild/netbsd-arm64": "0.27.2", "@esbuild/netbsd-x64": "0.27.2", "@esbuild/openbsd-arm64": "0.27.2", "@esbuild/openbsd-x64": "0.27.2", "@esbuild/openharmony-arm64": "0.27.2", "@esbuild/sunos-x64": "0.27.2", "@esbuild/win32-arm64": "0.27.2", "@esbuild/win32-ia32": "0.27.2", "@esbuild/win32-x64": "0.27.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw=="],
|
||||||
|
|
||||||
|
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
|
||||||
|
|
||||||
|
"eslint": ["eslint@9.39.2", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.1", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.39.2", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw=="],
|
||||||
|
|
||||||
|
"eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="],
|
||||||
|
|
||||||
|
"eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="],
|
||||||
|
|
||||||
|
"espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="],
|
||||||
|
|
||||||
|
"esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="],
|
||||||
|
|
||||||
|
"esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="],
|
||||||
|
|
||||||
|
"estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
|
||||||
|
|
||||||
|
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
|
||||||
|
|
||||||
|
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
||||||
|
|
||||||
|
"fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
|
||||||
|
|
||||||
|
"fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
|
||||||
|
|
||||||
|
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
||||||
|
|
||||||
|
"file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
|
||||||
|
|
||||||
|
"find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="],
|
||||||
|
|
||||||
|
"flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="],
|
||||||
|
|
||||||
|
"flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="],
|
||||||
|
|
||||||
|
"framer-motion": ["framer-motion@12.29.0", "", { "dependencies": { "motion-dom": "^12.29.0", "motion-utils": "^12.27.2", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-1gEFGXHYV2BD42ZPTFmSU9buehppU+bCuOnHU0AD18DKh9j4DuTx47MvqY5ax+NNWRtK32qIcJf1UxKo1WwjWg=="],
|
||||||
|
|
||||||
|
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||||
|
|
||||||
|
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
|
||||||
|
|
||||||
|
"globals": ["globals@16.5.0", "", {}, "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ=="],
|
||||||
|
|
||||||
|
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
||||||
|
|
||||||
|
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
|
||||||
|
|
||||||
|
"ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
|
||||||
|
|
||||||
|
"import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
|
||||||
|
|
||||||
|
"imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
|
||||||
|
|
||||||
|
"input-otp": ["input-otp@1.4.2", "", { "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-l3jWwYNvrEa6NTCt7BECfCm48GvwuZzkoeG3gBL2w4CHeOXW3eKFmf9UNYkNfYc3mxMrthMnxjIE07MT0zLBQA=="],
|
||||||
|
|
||||||
|
"intl-messageformat": ["intl-messageformat@10.7.18", "", { "dependencies": { "@formatjs/ecma402-abstract": "2.3.6", "@formatjs/fast-memoize": "2.2.7", "@formatjs/icu-messageformat-parser": "2.11.4", "tslib": "^2.8.0" } }, "sha512-m3Ofv/X/tV8Y3tHXLohcuVuhWKo7BBq62cqY15etqmLxg2DZ34AGGgQDeR+SCta2+zICb1NX83af0GJmbQ1++g=="],
|
||||||
|
|
||||||
|
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
|
||||||
|
|
||||||
|
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
|
||||||
|
|
||||||
|
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||||
|
|
||||||
|
"javascript-natural-sort": ["javascript-natural-sort@0.7.1", "", {}, "sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw=="],
|
||||||
|
|
||||||
|
"jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
|
||||||
|
|
||||||
|
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
||||||
|
|
||||||
|
"js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="],
|
||||||
|
|
||||||
|
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
|
||||||
|
|
||||||
|
"json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
|
||||||
|
|
||||||
|
"json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
|
||||||
|
|
||||||
|
"json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="],
|
||||||
|
|
||||||
|
"keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
|
||||||
|
|
||||||
|
"levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
|
||||||
|
|
||||||
|
"lightningcss": ["lightningcss@1.30.2", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.30.2", "lightningcss-darwin-arm64": "1.30.2", "lightningcss-darwin-x64": "1.30.2", "lightningcss-freebsd-x64": "1.30.2", "lightningcss-linux-arm-gnueabihf": "1.30.2", "lightningcss-linux-arm64-gnu": "1.30.2", "lightningcss-linux-arm64-musl": "1.30.2", "lightningcss-linux-x64-gnu": "1.30.2", "lightningcss-linux-x64-musl": "1.30.2", "lightningcss-win32-arm64-msvc": "1.30.2", "lightningcss-win32-x64-msvc": "1.30.2" } }, "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ=="],
|
||||||
|
|
||||||
|
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.30.2", "", { "os": "android", "cpu": "arm64" }, "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A=="],
|
||||||
|
|
||||||
|
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA=="],
|
||||||
|
|
||||||
|
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.30.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ=="],
|
||||||
|
|
||||||
|
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.30.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA=="],
|
||||||
|
|
||||||
|
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.30.2", "", { "os": "linux", "cpu": "arm" }, "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA=="],
|
||||||
|
|
||||||
|
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A=="],
|
||||||
|
|
||||||
|
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA=="],
|
||||||
|
|
||||||
|
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w=="],
|
||||||
|
|
||||||
|
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA=="],
|
||||||
|
|
||||||
|
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.30.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ=="],
|
||||||
|
|
||||||
|
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw=="],
|
||||||
|
|
||||||
|
"locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
|
||||||
|
|
||||||
|
"lodash": ["lodash@4.17.23", "", {}, "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w=="],
|
||||||
|
|
||||||
|
"lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
|
||||||
|
|
||||||
|
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
||||||
|
|
||||||
|
"minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
||||||
|
|
||||||
|
"motion": ["motion@12.29.0", "", { "dependencies": { "framer-motion": "^12.29.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-rjB5CP2N9S2ESAyEFnAFMgTec6X8yvfxLNcz8n12gPq3M48R7ZbBeVYkDOTj8SPMwfvGIFI801SiPSr1+HCr9g=="],
|
||||||
|
|
||||||
|
"motion-dom": ["motion-dom@12.29.0", "", { "dependencies": { "motion-utils": "^12.27.2" } }, "sha512-3eiz9bb32yvY8Q6XNM4AwkSOBPgU//EIKTZwsSWgA9uzbPBhZJeScCVcBuwwYVqhfamewpv7ZNmVKTGp5qnzkA=="],
|
||||||
|
|
||||||
|
"motion-utils": ["motion-utils@12.27.2", "", {}, "sha512-B55gcoL85Mcdt2IEStY5EEAsrMSVE2sI14xQ/uAdPL+mfQxhKKFaEag9JmfxedJOR4vZpBGoPeC/Gm13I/4g5Q=="],
|
||||||
|
|
||||||
|
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||||
|
|
||||||
|
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||||
|
|
||||||
|
"natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
|
||||||
|
|
||||||
|
"optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
|
||||||
|
|
||||||
|
"p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
|
||||||
|
|
||||||
|
"p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="],
|
||||||
|
|
||||||
|
"parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="],
|
||||||
|
|
||||||
|
"path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
|
||||||
|
|
||||||
|
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
||||||
|
|
||||||
|
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||||
|
|
||||||
|
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
|
||||||
|
|
||||||
|
"postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
|
||||||
|
|
||||||
|
"postcss-selector-parser": ["postcss-selector-parser@6.0.10", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w=="],
|
||||||
|
|
||||||
|
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
|
||||||
|
|
||||||
|
"prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="],
|
||||||
|
|
||||||
|
"prettier-plugin-tailwindcss": ["prettier-plugin-tailwindcss@0.6.14", "", { "peerDependencies": { "@ianvs/prettier-plugin-sort-imports": "*", "@prettier/plugin-hermes": "*", "@prettier/plugin-oxc": "*", "@prettier/plugin-pug": "*", "@shopify/prettier-plugin-liquid": "*", "@trivago/prettier-plugin-sort-imports": "*", "@zackad/prettier-plugin-twig": "*", "prettier": "^3.0", "prettier-plugin-astro": "*", "prettier-plugin-css-order": "*", "prettier-plugin-import-sort": "*", "prettier-plugin-jsdoc": "*", "prettier-plugin-marko": "*", "prettier-plugin-multiline-arrays": "*", "prettier-plugin-organize-attributes": "*", "prettier-plugin-organize-imports": "*", "prettier-plugin-sort-imports": "*", "prettier-plugin-style-order": "*", "prettier-plugin-svelte": "*" }, "optionalPeers": ["@ianvs/prettier-plugin-sort-imports", "@prettier/plugin-hermes", "@prettier/plugin-oxc", "@prettier/plugin-pug", "@shopify/prettier-plugin-liquid", "@trivago/prettier-plugin-sort-imports", "@zackad/prettier-plugin-twig", "prettier-plugin-astro", "prettier-plugin-css-order", "prettier-plugin-import-sort", "prettier-plugin-jsdoc", "prettier-plugin-marko", "prettier-plugin-multiline-arrays", "prettier-plugin-organize-attributes", "prettier-plugin-organize-imports", "prettier-plugin-sort-imports", "prettier-plugin-style-order", "prettier-plugin-svelte"] }, "sha512-pi2e/+ZygeIqntN+vC573BcW5Cve8zUB0SSAGxqpB4f96boZF4M3phPVoOFCeypwkpRYdi7+jQ5YJJUwrkGUAg=="],
|
||||||
|
|
||||||
|
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
||||||
|
|
||||||
|
"qr-code-styling": ["qr-code-styling@1.9.2", "", { "dependencies": { "qrcode-generator": "^1.4.4" } }, "sha512-RgJaZJ1/RrXJ6N0j7a+pdw3zMBmzZU4VN2dtAZf8ZggCfRB5stEQ3IoDNGaNhYY3nnZKYlYSLl5YkfWN5dPutg=="],
|
||||||
|
|
||||||
|
"qrcode-generator": ["qrcode-generator@1.5.2", "", {}, "sha512-pItrW0Z9HnDBnFmgiNrY1uxRdri32Uh9EjNYLPVC2zZ3ZRIIEqBoDgm4DkvDwNNDHTK7FNkmr8zAa77BYc9xNw=="],
|
||||||
|
|
||||||
|
"react": ["react@19.2.3", "", {}, "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA=="],
|
||||||
|
|
||||||
|
"react-aria": ["react-aria@3.46.0", "", { "dependencies": { "@internationalized/string": "^3.2.7", "@react-aria/breadcrumbs": "^3.5.31", "@react-aria/button": "^3.14.4", "@react-aria/calendar": "^3.9.4", "@react-aria/checkbox": "^3.16.4", "@react-aria/color": "^3.1.4", "@react-aria/combobox": "^3.14.2", "@react-aria/datepicker": "^3.16.0", "@react-aria/dialog": "^3.5.33", "@react-aria/disclosure": "^3.1.2", "@react-aria/dnd": "^3.11.5", "@react-aria/focus": "^3.21.4", "@react-aria/gridlist": "^3.14.3", "@react-aria/i18n": "^3.12.15", "@react-aria/interactions": "^3.27.0", "@react-aria/label": "^3.7.24", "@react-aria/landmark": "^3.0.9", "@react-aria/link": "^3.8.8", "@react-aria/listbox": "^3.15.2", "@react-aria/menu": "^3.20.0", "@react-aria/meter": "^3.4.29", "@react-aria/numberfield": "^3.12.4", "@react-aria/overlays": "^3.31.1", "@react-aria/progress": "^3.4.29", "@react-aria/radio": "^3.12.4", "@react-aria/searchfield": "^3.8.11", "@react-aria/select": "^3.17.2", "@react-aria/selection": "^3.27.1", "@react-aria/separator": "^3.4.15", "@react-aria/slider": "^3.8.4", "@react-aria/ssr": "^3.9.10", "@react-aria/switch": "^3.7.10", "@react-aria/table": "^3.17.10", "@react-aria/tabs": "^3.11.0", "@react-aria/tag": "^3.8.0", "@react-aria/textfield": "^3.18.4", "@react-aria/toast": "^3.0.10", "@react-aria/tooltip": "^3.9.1", "@react-aria/tree": "^3.1.6", "@react-aria/utils": "^3.33.0", "@react-aria/visually-hidden": "^3.8.30", "@react-types/shared": "^3.33.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-We0diSsMK35jw53JFjgF9w8obBjehAUI/TRiynnzSrjRd9eoHYQcecHlptke/HEFxvya/Gcm+LA21Im1+qnIeQ=="],
|
||||||
|
|
||||||
|
"react-aria-components": ["react-aria-components@1.15.1", "", { "dependencies": { "@internationalized/date": "^3.11.0", "@internationalized/string": "^3.2.7", "@react-aria/autocomplete": "3.0.0-rc.5", "@react-aria/collections": "^3.0.2", "@react-aria/dnd": "^3.11.5", "@react-aria/focus": "^3.21.4", "@react-aria/interactions": "^3.27.0", "@react-aria/live-announcer": "^3.4.4", "@react-aria/overlays": "^3.31.1", "@react-aria/ssr": "^3.9.10", "@react-aria/textfield": "^3.18.4", "@react-aria/toolbar": "3.0.0-beta.23", "@react-aria/utils": "^3.33.0", "@react-aria/virtualizer": "^4.1.12", "@react-stately/autocomplete": "3.0.0-beta.4", "@react-stately/layout": "^4.5.3", "@react-stately/selection": "^3.20.8", "@react-stately/table": "^3.15.3", "@react-stately/utils": "^3.11.0", "@react-stately/virtualizer": "^4.4.5", "@react-types/form": "^3.7.17", "@react-types/grid": "^3.3.7", "@react-types/shared": "^3.33.0", "@react-types/table": "^3.13.5", "@swc/helpers": "^0.5.0", "client-only": "^0.0.1", "react-aria": "^3.46.0", "react-stately": "^3.44.0", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-irGhZ+vBvoY9xJHf/qzPLLwFZ8cBUrYwPERGhgjE62dy/RXMUiEW+1DeTHz0OvtjbvFbhNp/I7XM9IaBvmLALg=="],
|
||||||
|
|
||||||
|
"react-dom": ["react-dom@19.2.3", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.3" } }, "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg=="],
|
||||||
|
|
||||||
|
"react-hotkeys-hook": ["react-hotkeys-hook@5.2.3", "", { "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-Q27F8EuImYJOVSXAjSQrQPj9cx4GSNY+WdSdk5tSNN085H8/a00W6LZp0PrytEDwF6iT0pGTJeVEDKPRpEK2Bg=="],
|
||||||
|
|
||||||
|
"react-router": ["react-router@7.13.0", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-PZgus8ETambRT17BUm/LL8lX3Of+oiLaPuVTRH3l1eLvSPpKO3AvhAEb5N7ihAFZQrYDqkvvWfFh9p0z9VsjLw=="],
|
||||||
|
|
||||||
|
"react-stately": ["react-stately@3.44.0", "", { "dependencies": { "@react-stately/calendar": "^3.9.2", "@react-stately/checkbox": "^3.7.4", "@react-stately/collections": "^3.12.9", "@react-stately/color": "^3.9.4", "@react-stately/combobox": "^3.12.2", "@react-stately/data": "^3.15.1", "@react-stately/datepicker": "^3.16.0", "@react-stately/disclosure": "^3.0.10", "@react-stately/dnd": "^3.7.3", "@react-stately/form": "^3.2.3", "@react-stately/list": "^3.13.3", "@react-stately/menu": "^3.9.10", "@react-stately/numberfield": "^3.10.4", "@react-stately/overlays": "^3.6.22", "@react-stately/radio": "^3.11.4", "@react-stately/searchfield": "^3.5.18", "@react-stately/select": "^3.9.1", "@react-stately/selection": "^3.20.8", "@react-stately/slider": "^3.7.4", "@react-stately/table": "^3.15.3", "@react-stately/tabs": "^3.8.8", "@react-stately/toast": "^3.1.3", "@react-stately/toggle": "^3.9.4", "@react-stately/tooltip": "^3.5.10", "@react-stately/tree": "^3.9.5", "@react-types/shared": "^3.33.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-Il3trIp2Mo1SSa9PhQFraqOpC74zEFmwuMAlu5Fj3qdtihJOKOFqoyDl7ALRrVfnvCkau6rui155d/NMKvd+RQ=="],
|
||||||
|
|
||||||
|
"resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
|
||||||
|
|
||||||
|
"rollup": ["rollup@4.56.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.56.0", "@rollup/rollup-android-arm64": "4.56.0", "@rollup/rollup-darwin-arm64": "4.56.0", "@rollup/rollup-darwin-x64": "4.56.0", "@rollup/rollup-freebsd-arm64": "4.56.0", "@rollup/rollup-freebsd-x64": "4.56.0", "@rollup/rollup-linux-arm-gnueabihf": "4.56.0", "@rollup/rollup-linux-arm-musleabihf": "4.56.0", "@rollup/rollup-linux-arm64-gnu": "4.56.0", "@rollup/rollup-linux-arm64-musl": "4.56.0", "@rollup/rollup-linux-loong64-gnu": "4.56.0", "@rollup/rollup-linux-loong64-musl": "4.56.0", "@rollup/rollup-linux-ppc64-gnu": "4.56.0", "@rollup/rollup-linux-ppc64-musl": "4.56.0", "@rollup/rollup-linux-riscv64-gnu": "4.56.0", "@rollup/rollup-linux-riscv64-musl": "4.56.0", "@rollup/rollup-linux-s390x-gnu": "4.56.0", "@rollup/rollup-linux-x64-gnu": "4.56.0", "@rollup/rollup-linux-x64-musl": "4.56.0", "@rollup/rollup-openbsd-x64": "4.56.0", "@rollup/rollup-openharmony-arm64": "4.56.0", "@rollup/rollup-win32-arm64-msvc": "4.56.0", "@rollup/rollup-win32-ia32-msvc": "4.56.0", "@rollup/rollup-win32-x64-gnu": "4.56.0", "@rollup/rollup-win32-x64-msvc": "4.56.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-9FwVqlgUHzbXtDg9RCMgodF3Ua4Na6Gau+Sdt9vyCN4RhHfVKX2DCHy3BjMLTDd47ITDhYAnTwGulWTblJSDLg=="],
|
||||||
|
|
||||||
|
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
||||||
|
|
||||||
|
"semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
|
||||||
|
|
||||||
|
"set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="],
|
||||||
|
|
||||||
|
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
|
||||||
|
|
||||||
|
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
|
||||||
|
|
||||||
|
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||||
|
|
||||||
|
"strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
|
||||||
|
|
||||||
|
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
||||||
|
|
||||||
|
"tailwind-merge": ["tailwind-merge@3.4.0", "", {}, "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g=="],
|
||||||
|
|
||||||
|
"tailwindcss": ["tailwindcss@4.1.18", "", {}, "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw=="],
|
||||||
|
|
||||||
|
"tailwindcss-animate": ["tailwindcss-animate@1.0.7", "", { "peerDependencies": { "tailwindcss": ">=3.0.0 || insiders" } }, "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA=="],
|
||||||
|
|
||||||
|
"tailwindcss-react-aria-components": ["tailwindcss-react-aria-components@2.0.1", "", { "peerDependencies": { "tailwindcss": "^4.0.0" } }, "sha512-yTAfYv9BE/gKczS+b8UiFMqxnrEYKKNE6Y4vAWzGadkHGb4Yuawp0SHbZKkZJQgFvK0KjO3JpCq/0kzR5jJ9tw=="],
|
||||||
|
|
||||||
|
"tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="],
|
||||||
|
|
||||||
|
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
|
||||||
|
|
||||||
|
"ts-api-utils": ["ts-api-utils@2.4.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA=="],
|
||||||
|
|
||||||
|
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
||||||
|
"type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
|
||||||
|
|
||||||
|
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||||
|
|
||||||
|
"typescript-eslint": ["typescript-eslint@8.53.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.53.1", "@typescript-eslint/parser": "8.53.1", "@typescript-eslint/typescript-estree": "8.53.1", "@typescript-eslint/utils": "8.53.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-gB+EVQfP5RDElh9ittfXlhZJdjSU4jUSTyE2+ia8CYyNvet4ElfaLlAIqDvQV9JPknKx0jQH1racTYe/4LaLSg=="],
|
||||||
|
|
||||||
|
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
||||||
|
|
||||||
|
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
|
||||||
|
|
||||||
|
"use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="],
|
||||||
|
|
||||||
|
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
|
||||||
|
|
||||||
|
"vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="],
|
||||||
|
|
||||||
|
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||||
|
|
||||||
|
"word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
|
||||||
|
|
||||||
|
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
||||||
|
|
||||||
|
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
|
||||||
|
|
||||||
|
"@eslint/config-array/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
|
||||||
|
|
||||||
|
"@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="],
|
||||||
|
|
||||||
|
"@eslint/eslintrc/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
||||||
|
|
||||||
|
"@eslint/eslintrc/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" }, "bundled": true }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
||||||
|
"eslint/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
||||||
|
|
||||||
|
"eslint/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
|
||||||
|
|
||||||
|
"@eslint/config-array/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
|
||||||
|
|
||||||
|
"@eslint/eslintrc/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
|
||||||
|
|
||||||
|
"eslint/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
|
||||||
|
}
|
||||||
|
}
|
||||||
29
index.html
Normal file
29
index.html
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<meta name="description" content="Generated by Untitled UI CLI" />
|
||||||
|
<meta name="theme-color" content="#7f56d9" />
|
||||||
|
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,400..700;1,14..32,400..700&display=swap" rel="stylesheet" />
|
||||||
|
|
||||||
|
<title>Starter kit — Untitled UI</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body class="size-full bg-primary antialiased">
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
<script>
|
||||||
|
// On page load or when changing themes, best to add inline in `head` to avoid FOUC
|
||||||
|
if (localStorage.theme === "dark" || (!("theme" in localStorage) && window.matchMedia("(prefers-color-scheme: dark)").matches)) {
|
||||||
|
document.documentElement.classList.add("dark-mode");
|
||||||
|
} else {
|
||||||
|
document.documentElement.classList.remove("dark-mode");
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
5938
package-lock.json
generated
Normal file
5938
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
50
package.json
Normal file
50
package.json
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
{
|
||||||
|
"name": "untitledui-vite-starter-kit",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc -b && vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@fortawesome/fontawesome-svg-core": "^7.2.0",
|
||||||
|
"@fortawesome/free-solid-svg-icons": "^7.2.0",
|
||||||
|
"@fortawesome/pro-light-svg-icons": "^7.2.0",
|
||||||
|
"@fortawesome/pro-regular-svg-icons": "^7.2.0",
|
||||||
|
"@fortawesome/pro-solid-svg-icons": "^7.2.0",
|
||||||
|
"@fortawesome/react-fontawesome": "^3.2.0",
|
||||||
|
"@tailwindcss/typography": "^0.5.19",
|
||||||
|
"@tailwindcss/vite": "^4.1.18",
|
||||||
|
"@untitledui/file-icons": "^0.0.8",
|
||||||
|
"@untitledui/icons": "^0.0.21",
|
||||||
|
"input-otp": "^1.4.2",
|
||||||
|
"motion": "^12.29.0",
|
||||||
|
"qr-code-styling": "^1.9.2",
|
||||||
|
"react": "^19.2.3",
|
||||||
|
"react-aria": "^3.46.0",
|
||||||
|
"react-aria-components": "^1.15.1",
|
||||||
|
"react-dom": "^19.2.3",
|
||||||
|
"react-hotkeys-hook": "^5.2.3",
|
||||||
|
"react-router": "^7.13.0",
|
||||||
|
"tailwind-merge": "^3.4.0",
|
||||||
|
"tailwindcss": "^4.1.18",
|
||||||
|
"tailwindcss-animate": "^1.0.7",
|
||||||
|
"tailwindcss-react-aria-components": "^2.0.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tailwindcss/postcss": "^4.1.18",
|
||||||
|
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
|
||||||
|
"@types/node": "^24.10.9",
|
||||||
|
"@types/react": "^19.2.9",
|
||||||
|
"@types/react-dom": "^19.2.3",
|
||||||
|
"@vitejs/plugin-react-swc": "^4.2.2",
|
||||||
|
"globals": "^16.5.0",
|
||||||
|
"prettier": "^3.8.1",
|
||||||
|
"prettier-plugin-tailwindcss": "^0.6.14",
|
||||||
|
"typescript": "^5.9.3",
|
||||||
|
"typescript-eslint": "^8.53.1",
|
||||||
|
"vite": "^7.3.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
1
public/vite.svg
Normal file
1
public/vite.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,56 @@
|
|||||||
|
import type { PropsWithChildren } from "react";
|
||||||
|
import { X as CloseIcon, Menu02 } from "@untitledui/icons";
|
||||||
|
import {
|
||||||
|
Button as AriaButton,
|
||||||
|
Dialog as AriaDialog,
|
||||||
|
DialogTrigger as AriaDialogTrigger,
|
||||||
|
Modal as AriaModal,
|
||||||
|
ModalOverlay as AriaModalOverlay,
|
||||||
|
} from "react-aria-components";
|
||||||
|
import { UntitledLogo } from "@/components/foundations/logo/untitledui-logo";
|
||||||
|
import { cx } from "@/utils/cx";
|
||||||
|
|
||||||
|
export const MobileNavigationHeader = ({ children }: PropsWithChildren) => {
|
||||||
|
return (
|
||||||
|
<AriaDialogTrigger>
|
||||||
|
<header className="flex h-16 items-center justify-between border-b border-secondary bg-primary py-3 pr-2 pl-4 lg:hidden">
|
||||||
|
<UntitledLogo />
|
||||||
|
|
||||||
|
<AriaButton
|
||||||
|
aria-label="Expand navigation menu"
|
||||||
|
className="group flex items-center justify-center rounded-lg bg-primary p-2 text-fg-secondary outline-focus-ring hover:bg-primary_hover hover:text-fg-secondary_hover focus-visible:outline-2 focus-visible:outline-offset-2"
|
||||||
|
>
|
||||||
|
<Menu02 className="size-6 transition duration-200 ease-in-out group-aria-expanded:opacity-0" />
|
||||||
|
<CloseIcon className="absolute size-6 opacity-0 transition duration-200 ease-in-out group-aria-expanded:opacity-100" />
|
||||||
|
</AriaButton>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<AriaModalOverlay
|
||||||
|
isDismissable
|
||||||
|
className={({ isEntering, isExiting }) =>
|
||||||
|
cx(
|
||||||
|
"fixed inset-0 z-50 cursor-pointer bg-overlay/70 pr-16 backdrop-blur-md lg:hidden",
|
||||||
|
isEntering && "duration-300 ease-in-out animate-in fade-in",
|
||||||
|
isExiting && "duration-200 ease-in-out animate-out fade-out",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{({ state }) => (
|
||||||
|
<>
|
||||||
|
<AriaButton
|
||||||
|
aria-label="Close navigation menu"
|
||||||
|
onPress={() => state.close()}
|
||||||
|
className="fixed top-3 right-2 flex cursor-pointer items-center justify-center rounded-lg p-2 text-fg-white/70 outline-focus-ring hover:bg-white/10 hover:text-fg-white focus-visible:outline-2 focus-visible:outline-offset-2"
|
||||||
|
>
|
||||||
|
<CloseIcon className="size-6" />
|
||||||
|
</AriaButton>
|
||||||
|
|
||||||
|
<AriaModal className="w-full cursor-auto will-change-transform">
|
||||||
|
<AriaDialog className="h-dvh outline-hidden focus:outline-hidden">{children}</AriaDialog>
|
||||||
|
</AriaModal>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</AriaModalOverlay>
|
||||||
|
</AriaDialogTrigger>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,209 @@
|
|||||||
|
import type { FC, HTMLAttributes } from "react";
|
||||||
|
import { useCallback, useEffect, useRef } from "react";
|
||||||
|
import type { Placement } from "@react-types/overlays";
|
||||||
|
import { BookOpen01, ChevronSelectorVertical, LogOut01, Plus, Settings01, User01 } from "@untitledui/icons";
|
||||||
|
import { useFocusManager } from "react-aria";
|
||||||
|
import type { DialogProps as AriaDialogProps } from "react-aria-components";
|
||||||
|
import { Button as AriaButton, Dialog as AriaDialog, DialogTrigger as AriaDialogTrigger, Popover as AriaPopover } from "react-aria-components";
|
||||||
|
import { AvatarLabelGroup } from "@/components/base/avatar/avatar-label-group";
|
||||||
|
import { Button } from "@/components/base/buttons/button";
|
||||||
|
import { RadioButtonBase } from "@/components/base/radio-buttons/radio-buttons";
|
||||||
|
import { useBreakpoint } from "@/hooks/use-breakpoint";
|
||||||
|
import { cx } from "@/utils/cx";
|
||||||
|
|
||||||
|
type NavAccountType = {
|
||||||
|
/** Unique identifier for the nav item. */
|
||||||
|
id: string;
|
||||||
|
/** Name of the account holder. */
|
||||||
|
name: string;
|
||||||
|
/** Email address of the account holder. */
|
||||||
|
email: string;
|
||||||
|
/** Avatar image URL. */
|
||||||
|
avatar: string;
|
||||||
|
/** Online status of the account holder. This is used to display the online status indicator. */
|
||||||
|
status: "online" | "offline";
|
||||||
|
};
|
||||||
|
|
||||||
|
const placeholderAccounts: NavAccountType[] = [
|
||||||
|
{
|
||||||
|
id: "olivia",
|
||||||
|
name: "Olivia Rhye",
|
||||||
|
email: "olivia@untitledui.com",
|
||||||
|
avatar: "https://www.untitledui.com/images/avatars/olivia-rhye?fm=webp&q=80",
|
||||||
|
status: "online",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "sienna",
|
||||||
|
name: "Sienna Hewitt",
|
||||||
|
email: "sienna@untitledui.com",
|
||||||
|
avatar: "https://www.untitledui.com/images/avatars/transparent/sienna-hewitt?bg=%23E0E0E0",
|
||||||
|
status: "online",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const NavAccountMenu = ({
|
||||||
|
className,
|
||||||
|
selectedAccountId = "olivia",
|
||||||
|
...dialogProps
|
||||||
|
}: AriaDialogProps & { className?: string; accounts?: NavAccountType[]; selectedAccountId?: string }) => {
|
||||||
|
const focusManager = useFocusManager();
|
||||||
|
const dialogRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const onKeyDown = useCallback(
|
||||||
|
(e: KeyboardEvent) => {
|
||||||
|
switch (e.key) {
|
||||||
|
case "ArrowDown":
|
||||||
|
focusManager?.focusNext({ tabbable: true, wrap: true });
|
||||||
|
break;
|
||||||
|
case "ArrowUp":
|
||||||
|
focusManager?.focusPrevious({ tabbable: true, wrap: true });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[focusManager],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const element = dialogRef.current;
|
||||||
|
if (element) {
|
||||||
|
element.addEventListener("keydown", onKeyDown);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (element) {
|
||||||
|
element.removeEventListener("keydown", onKeyDown);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [onKeyDown]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AriaDialog
|
||||||
|
{...dialogProps}
|
||||||
|
ref={dialogRef}
|
||||||
|
className={cx("w-66 rounded-xl bg-secondary_alt shadow-lg ring ring-secondary_alt outline-hidden", className)}
|
||||||
|
>
|
||||||
|
<div className="rounded-xl bg-primary ring-1 ring-secondary">
|
||||||
|
<div className="flex flex-col gap-0.5 py-1.5">
|
||||||
|
<NavAccountCardMenuItem label="View profile" icon={User01} shortcut="⌘K->P" />
|
||||||
|
<NavAccountCardMenuItem label="Account settings" icon={Settings01} shortcut="⌘S" />
|
||||||
|
<NavAccountCardMenuItem label="Documentation" icon={BookOpen01} />
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-0.5 border-t border-secondary py-1.5">
|
||||||
|
<div className="px-3 pt-1.5 pb-1 text-xs font-semibold text-tertiary">Switch account</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-0.5 px-1.5">
|
||||||
|
{placeholderAccounts.map((account) => (
|
||||||
|
<button
|
||||||
|
key={account.id}
|
||||||
|
className={cx(
|
||||||
|
"relative w-full cursor-pointer rounded-md px-2 py-1.5 text-left outline-focus-ring hover:bg-primary_hover focus:z-10 focus-visible:outline-2 focus-visible:outline-offset-2",
|
||||||
|
account.id === selectedAccountId && "bg-primary_hover",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<AvatarLabelGroup status="online" size="md" src={account.avatar} title={account.name} subtitle={account.email} />
|
||||||
|
|
||||||
|
<RadioButtonBase isSelected={account.id === selectedAccountId} className="absolute top-2 right-2" />
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-2 px-2 pt-0.5 pb-2">
|
||||||
|
<Button iconLeading={Plus} color="secondary" size="sm">
|
||||||
|
Add account
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="pt-1 pb-1.5">
|
||||||
|
<NavAccountCardMenuItem label="Sign out" icon={LogOut01} shortcut="⌥⇧Q" />
|
||||||
|
</div>
|
||||||
|
</AriaDialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const NavAccountCardMenuItem = ({
|
||||||
|
icon: Icon,
|
||||||
|
label,
|
||||||
|
shortcut,
|
||||||
|
...buttonProps
|
||||||
|
}: {
|
||||||
|
icon?: FC<{ className?: string }>;
|
||||||
|
label: string;
|
||||||
|
shortcut?: string;
|
||||||
|
} & HTMLAttributes<HTMLButtonElement>) => {
|
||||||
|
return (
|
||||||
|
<button {...buttonProps} className={cx("group/item w-full cursor-pointer px-1.5 focus:outline-hidden", buttonProps.className)}>
|
||||||
|
<div
|
||||||
|
className={cx(
|
||||||
|
"flex w-full items-center justify-between gap-3 rounded-md p-2 group-hover/item:bg-primary_hover",
|
||||||
|
// Focus styles.
|
||||||
|
"outline-focus-ring group-focus-visible/item:outline-2 group-focus-visible/item:outline-offset-2",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex gap-2 text-sm font-semibold text-secondary group-hover/item:text-secondary_hover">
|
||||||
|
{Icon && <Icon className="size-5 text-fg-quaternary" />} {label}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{shortcut && (
|
||||||
|
<kbd className="flex rounded px-1 py-px font-body text-xs font-medium text-tertiary ring-1 ring-secondary ring-inset">{shortcut}</kbd>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const NavAccountCard = ({
|
||||||
|
popoverPlacement,
|
||||||
|
selectedAccountId = "olivia",
|
||||||
|
items = placeholderAccounts,
|
||||||
|
}: {
|
||||||
|
popoverPlacement?: Placement;
|
||||||
|
selectedAccountId?: string;
|
||||||
|
items?: NavAccountType[];
|
||||||
|
}) => {
|
||||||
|
const triggerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const isDesktop = useBreakpoint("lg");
|
||||||
|
|
||||||
|
const selectedAccount = placeholderAccounts.find((account) => account.id === selectedAccountId);
|
||||||
|
|
||||||
|
if (!selectedAccount) {
|
||||||
|
console.warn(`Account with ID ${selectedAccountId} not found in <NavAccountCard />`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={triggerRef} className="relative flex items-center gap-3 rounded-xl p-3 ring-1 ring-secondary ring-inset">
|
||||||
|
<AvatarLabelGroup
|
||||||
|
size="md"
|
||||||
|
src={selectedAccount.avatar}
|
||||||
|
title={selectedAccount.name}
|
||||||
|
subtitle={selectedAccount.email}
|
||||||
|
status={selectedAccount.status}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="absolute top-1.5 right-1.5">
|
||||||
|
<AriaDialogTrigger>
|
||||||
|
<AriaButton className="flex cursor-pointer items-center justify-center rounded-md p-1.5 text-fg-quaternary outline-focus-ring transition duration-100 ease-linear hover:bg-primary_hover hover:text-fg-quaternary_hover focus-visible:outline-2 focus-visible:outline-offset-2 pressed:bg-primary_hover pressed:text-fg-quaternary_hover">
|
||||||
|
<ChevronSelectorVertical className="size-4 shrink-0" />
|
||||||
|
</AriaButton>
|
||||||
|
<AriaPopover
|
||||||
|
placement={popoverPlacement ?? (isDesktop ? "right bottom" : "top right")}
|
||||||
|
triggerRef={triggerRef}
|
||||||
|
offset={8}
|
||||||
|
className={({ isEntering, isExiting }) =>
|
||||||
|
cx(
|
||||||
|
"origin-(--trigger-anchor-point) will-change-transform",
|
||||||
|
isEntering &&
|
||||||
|
"duration-150 ease-out animate-in fade-in placement-right:slide-in-from-left-0.5 placement-top:slide-in-from-bottom-0.5 placement-bottom:slide-in-from-top-0.5",
|
||||||
|
isExiting &&
|
||||||
|
"duration-100 ease-in animate-out fade-out placement-right:slide-out-to-left-0.5 placement-top:slide-out-to-bottom-0.5 placement-bottom:slide-out-to-top-0.5",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<NavAccountMenu selectedAccountId={selectedAccountId} accounts={items} />
|
||||||
|
</AriaPopover>
|
||||||
|
</AriaDialogTrigger>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import type { FC, MouseEventHandler } from "react";
|
||||||
|
import { Pressable } from "react-aria-components";
|
||||||
|
import { Tooltip } from "@/components/base/tooltip/tooltip";
|
||||||
|
import { cx } from "@/utils/cx";
|
||||||
|
|
||||||
|
const styles = {
|
||||||
|
md: {
|
||||||
|
root: "size-10",
|
||||||
|
icon: "size-5",
|
||||||
|
},
|
||||||
|
lg: {
|
||||||
|
root: "size-12",
|
||||||
|
icon: "size-6",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
interface NavItemButtonProps {
|
||||||
|
/** Whether the collapsible nav item is open. */
|
||||||
|
open?: boolean;
|
||||||
|
/** URL to navigate to when the button is clicked. */
|
||||||
|
href?: string;
|
||||||
|
/** Label text for the button. */
|
||||||
|
label: string;
|
||||||
|
/** Icon component to display. */
|
||||||
|
icon: FC<{ className?: string }>;
|
||||||
|
/** Whether the button is currently active. */
|
||||||
|
current?: boolean;
|
||||||
|
/** Size of the button. */
|
||||||
|
size?: "md" | "lg";
|
||||||
|
/** Handler for click events. */
|
||||||
|
onClick?: MouseEventHandler;
|
||||||
|
/** Additional CSS classes to apply to the button. */
|
||||||
|
className?: string;
|
||||||
|
/** Placement of the tooltip. */
|
||||||
|
tooltipPlacement?: "top" | "right" | "bottom" | "left";
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NavItemButton = ({
|
||||||
|
current: current,
|
||||||
|
label,
|
||||||
|
href,
|
||||||
|
icon: Icon,
|
||||||
|
size = "md",
|
||||||
|
className,
|
||||||
|
tooltipPlacement = "right",
|
||||||
|
onClick,
|
||||||
|
}: NavItemButtonProps) => {
|
||||||
|
return (
|
||||||
|
<Tooltip title={label} placement={tooltipPlacement}>
|
||||||
|
<Pressable>
|
||||||
|
<a
|
||||||
|
href={href}
|
||||||
|
aria-label={label}
|
||||||
|
onClick={onClick}
|
||||||
|
className={cx(
|
||||||
|
"relative flex w-full cursor-pointer items-center justify-center rounded-md bg-primary p-2 text-fg-quaternary outline-focus-ring transition duration-100 ease-linear select-none hover:bg-primary_hover hover:text-fg-quaternary_hover focus-visible:z-10 focus-visible:outline-2 focus-visible:outline-offset-2",
|
||||||
|
current && "bg-active text-fg-quaternary_hover hover:bg-secondary_hover",
|
||||||
|
styles[size].root,
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Icon aria-hidden="true" className={cx("shrink-0 transition-inherit-all", styles[size].icon)} />
|
||||||
|
</a>
|
||||||
|
</Pressable>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
import type { FC, HTMLAttributes, MouseEventHandler, ReactNode } from "react";
|
||||||
|
import { ChevronDown, Share04 } from "@untitledui/icons";
|
||||||
|
import { Link as AriaLink } from "react-aria-components";
|
||||||
|
import { Badge } from "@/components/base/badges/badges";
|
||||||
|
import { cx, sortCx } from "@/utils/cx";
|
||||||
|
|
||||||
|
const styles = sortCx({
|
||||||
|
root: "group relative flex w-full cursor-pointer items-center rounded-md bg-primary outline-focus-ring transition duration-100 ease-linear select-none hover:bg-primary_hover focus-visible:z-10 focus-visible:outline-2 focus-visible:outline-offset-2",
|
||||||
|
rootSelected: "bg-active hover:bg-secondary_hover",
|
||||||
|
});
|
||||||
|
|
||||||
|
interface NavItemBaseProps {
|
||||||
|
/** Whether the nav item shows only an icon. */
|
||||||
|
iconOnly?: boolean;
|
||||||
|
/** Whether the collapsible nav item is open. */
|
||||||
|
open?: boolean;
|
||||||
|
/** URL to navigate to when the nav item is clicked. */
|
||||||
|
href?: string;
|
||||||
|
/** Type of the nav item. */
|
||||||
|
type: "link" | "collapsible" | "collapsible-child";
|
||||||
|
/** Icon component to display. */
|
||||||
|
icon?: FC<HTMLAttributes<HTMLOrSVGElement>>;
|
||||||
|
/** Badge to display. */
|
||||||
|
badge?: ReactNode;
|
||||||
|
/** Whether the nav item is currently active. */
|
||||||
|
current?: boolean;
|
||||||
|
/** Whether to truncate the label text. */
|
||||||
|
truncate?: boolean;
|
||||||
|
/** Handler for click events. */
|
||||||
|
onClick?: MouseEventHandler;
|
||||||
|
/** Content to display. */
|
||||||
|
children?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NavItemBase = ({ current, type, badge, href, icon: Icon, children, truncate = true, onClick }: NavItemBaseProps) => {
|
||||||
|
const iconElement = Icon && <Icon aria-hidden="true" className="mr-2 size-5 shrink-0 text-fg-quaternary transition-inherit-all" />;
|
||||||
|
|
||||||
|
const badgeElement =
|
||||||
|
badge && (typeof badge === "string" || typeof badge === "number") ? (
|
||||||
|
<Badge className="ml-3" color="gray" type="pill-color" size="sm">
|
||||||
|
{badge}
|
||||||
|
</Badge>
|
||||||
|
) : (
|
||||||
|
badge
|
||||||
|
);
|
||||||
|
|
||||||
|
const labelElement = (
|
||||||
|
<span
|
||||||
|
className={cx(
|
||||||
|
"flex-1 text-md font-semibold text-secondary transition-inherit-all group-hover:text-secondary_hover",
|
||||||
|
truncate && "truncate",
|
||||||
|
current && "text-secondary_hover",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
|
||||||
|
const isExternal = href && href.startsWith("http");
|
||||||
|
const externalIcon = isExternal && <Share04 className="size-4 stroke-[2.5px] text-fg-quaternary" />;
|
||||||
|
|
||||||
|
if (type === "collapsible") {
|
||||||
|
return (
|
||||||
|
<summary className={cx("px-3 py-2", styles.root, current && styles.rootSelected)} onClick={onClick}>
|
||||||
|
{iconElement}
|
||||||
|
|
||||||
|
{labelElement}
|
||||||
|
|
||||||
|
{badgeElement}
|
||||||
|
|
||||||
|
<ChevronDown aria-hidden="true" className="ml-3 size-4 shrink-0 stroke-[2.5px] text-fg-quaternary in-open:-scale-y-100" />
|
||||||
|
</summary>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === "collapsible-child") {
|
||||||
|
return (
|
||||||
|
<AriaLink
|
||||||
|
href={href!}
|
||||||
|
target={isExternal ? "_blank" : "_self"}
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className={cx("py-2 pr-3 pl-10", styles.root, current && styles.rootSelected)}
|
||||||
|
onClick={onClick}
|
||||||
|
aria-current={current ? "page" : undefined}
|
||||||
|
>
|
||||||
|
{labelElement}
|
||||||
|
{externalIcon}
|
||||||
|
{badgeElement}
|
||||||
|
</AriaLink>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AriaLink
|
||||||
|
href={href!}
|
||||||
|
target={isExternal ? "_blank" : "_self"}
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className={cx("px-3 py-2", styles.root, current && styles.rootSelected)}
|
||||||
|
onClick={onClick}
|
||||||
|
aria-current={current ? "page" : undefined}
|
||||||
|
>
|
||||||
|
{iconElement}
|
||||||
|
{labelElement}
|
||||||
|
{externalIcon}
|
||||||
|
{badgeElement}
|
||||||
|
</AriaLink>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { cx } from "@/utils/cx";
|
||||||
|
import type { NavItemDividerType, NavItemType } from "../config";
|
||||||
|
import { NavItemBase } from "./nav-item";
|
||||||
|
|
||||||
|
interface NavListProps {
|
||||||
|
/** URL of the currently active item. */
|
||||||
|
activeUrl?: string;
|
||||||
|
/** Additional CSS classes to apply to the list. */
|
||||||
|
className?: string;
|
||||||
|
/** List of items to display. */
|
||||||
|
items: (NavItemType | NavItemDividerType)[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NavList = ({ activeUrl, items, className }: NavListProps) => {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const activeItem = items.find((item) => item.href === activeUrl || item.items?.some((subItem) => subItem.href === activeUrl));
|
||||||
|
const [currentItem, setCurrentItem] = useState(activeItem);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ul className={cx("mt-4 flex flex-col px-2 lg:px-4", className)}>
|
||||||
|
{items.map((item, index) => {
|
||||||
|
if (item.divider) {
|
||||||
|
return (
|
||||||
|
<li key={index} className="w-full px-0.5 py-2">
|
||||||
|
<hr className="h-px w-full border-none bg-border-secondary" />
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.items?.length) {
|
||||||
|
return (
|
||||||
|
<details
|
||||||
|
key={item.label}
|
||||||
|
open={activeItem?.href === item.href}
|
||||||
|
className="appearance-none py-0.5"
|
||||||
|
onToggle={(e) => {
|
||||||
|
setOpen(e.currentTarget.open);
|
||||||
|
setCurrentItem(item);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<NavItemBase href={item.href} badge={item.badge} icon={item.icon} type="collapsible">
|
||||||
|
{item.label}
|
||||||
|
</NavItemBase>
|
||||||
|
|
||||||
|
<dd>
|
||||||
|
<ul className="py-0.5">
|
||||||
|
{item.items.map((childItem) => (
|
||||||
|
<li key={childItem.label} className="py-0.5">
|
||||||
|
<NavItemBase
|
||||||
|
href={childItem.href}
|
||||||
|
badge={childItem.badge}
|
||||||
|
type="collapsible-child"
|
||||||
|
current={activeUrl === childItem.href}
|
||||||
|
>
|
||||||
|
{childItem.label}
|
||||||
|
</NavItemBase>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</dd>
|
||||||
|
</details>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li key={item.label} className="py-0.5">
|
||||||
|
<NavItemBase
|
||||||
|
type="link"
|
||||||
|
badge={item.badge}
|
||||||
|
icon={item.icon}
|
||||||
|
href={item.href}
|
||||||
|
current={currentItem?.href === item.href}
|
||||||
|
open={open && currentItem?.href === item.href}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</NavItemBase>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
};
|
||||||
23
src/components/application/app-navigation/config.ts
Normal file
23
src/components/application/app-navigation/config.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import type { FC, ReactNode } from "react";
|
||||||
|
|
||||||
|
export type NavItemType = {
|
||||||
|
/** Label text for the nav item. */
|
||||||
|
label: string;
|
||||||
|
/** URL to navigate to when the nav item is clicked. */
|
||||||
|
href?: string;
|
||||||
|
/** Icon component to display. */
|
||||||
|
icon?: FC<{ className?: string }>;
|
||||||
|
/** Badge to display. */
|
||||||
|
badge?: ReactNode;
|
||||||
|
/** List of sub-items to display. */
|
||||||
|
items?: { label: string; href: string; icon?: FC<{ className?: string }>; badge?: ReactNode }[];
|
||||||
|
/** Whether this nav item is a divider. */
|
||||||
|
divider?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type NavItemDividerType = Omit<NavItemType, "icon" | "label" | "divider"> & {
|
||||||
|
/** Label text for the divider. */
|
||||||
|
label?: string;
|
||||||
|
/** Whether this nav item is a divider. */
|
||||||
|
divider: true;
|
||||||
|
};
|
||||||
202
src/components/application/app-navigation/header-navigation.tsx
Normal file
202
src/components/application/app-navigation/header-navigation.tsx
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
import type { FC, ReactNode } from "react";
|
||||||
|
import { Bell01, LifeBuoy01, SearchLg, Settings01 } from "@untitledui/icons";
|
||||||
|
import { Button as AriaButton, DialogTrigger, Popover } from "react-aria-components";
|
||||||
|
import { Avatar } from "@/components/base/avatar/avatar";
|
||||||
|
import { BadgeWithDot } from "@/components/base/badges/badges";
|
||||||
|
import { Input } from "@/components/base/input/input";
|
||||||
|
import { UntitledLogo } from "@/components/foundations/logo/untitledui-logo";
|
||||||
|
import { cx } from "@/utils/cx";
|
||||||
|
import { MobileNavigationHeader } from "./base-components/mobile-header";
|
||||||
|
import { NavAccountCard, NavAccountMenu } from "./base-components/nav-account-card";
|
||||||
|
import { NavItemBase } from "./base-components/nav-item";
|
||||||
|
import { NavItemButton } from "./base-components/nav-item-button";
|
||||||
|
import { NavList } from "./base-components/nav-list";
|
||||||
|
|
||||||
|
type NavItem = {
|
||||||
|
/** Label text for the nav item. */
|
||||||
|
label: string;
|
||||||
|
/** URL to navigate to when the nav item is clicked. */
|
||||||
|
href: string;
|
||||||
|
/** Whether the nav item is currently active. */
|
||||||
|
current?: boolean;
|
||||||
|
/** Icon component to display. */
|
||||||
|
icon?: FC<{ className?: string }>;
|
||||||
|
/** Badge to display. */
|
||||||
|
badge?: ReactNode;
|
||||||
|
/** List of sub-items to display. */
|
||||||
|
items?: NavItem[];
|
||||||
|
};
|
||||||
|
|
||||||
|
interface HeaderNavigationBaseProps {
|
||||||
|
/** URL of the currently active item. */
|
||||||
|
activeUrl?: string;
|
||||||
|
/** List of items to display. */
|
||||||
|
items: NavItem[];
|
||||||
|
/** List of sub-items to display. */
|
||||||
|
subItems?: NavItem[];
|
||||||
|
/** Content to display in the trailing position. */
|
||||||
|
trailingContent?: ReactNode;
|
||||||
|
/** Whether to show the avatar dropdown. */
|
||||||
|
showAvatarDropdown?: boolean;
|
||||||
|
/** Whether to hide the bottom border. */
|
||||||
|
hideBorder?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const HeaderNavigationBase = ({
|
||||||
|
activeUrl,
|
||||||
|
items,
|
||||||
|
subItems,
|
||||||
|
trailingContent,
|
||||||
|
showAvatarDropdown = true,
|
||||||
|
hideBorder = false,
|
||||||
|
}: HeaderNavigationBaseProps) => {
|
||||||
|
const activeSubNavItems = subItems || items.find((item) => item.current && item.items && item.items.length > 0)?.items;
|
||||||
|
|
||||||
|
const showSecondaryNav = activeSubNavItems && activeSubNavItems.length > 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<MobileNavigationHeader>
|
||||||
|
<aside className="flex h-full max-w-full flex-col justify-between overflow-auto border-r border-secondary bg-primary pt-4 lg:pt-6">
|
||||||
|
<div className="flex flex-col gap-5 px-4 lg:px-5">
|
||||||
|
<UntitledLogo className="h-8" />
|
||||||
|
<Input shortcut size="sm" aria-label="Search" placeholder="Search" icon={SearchLg} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<NavList items={items} />
|
||||||
|
|
||||||
|
<div className="mt-auto flex flex-col gap-4 px-2 py-4 lg:px-4 lg:py-6">
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<NavItemBase type="link" href="#" icon={LifeBuoy01}>
|
||||||
|
Support
|
||||||
|
</NavItemBase>
|
||||||
|
<NavItemBase
|
||||||
|
type="link"
|
||||||
|
href="#"
|
||||||
|
icon={Settings01}
|
||||||
|
badge={
|
||||||
|
<BadgeWithDot color="success" type="modern" size="sm">
|
||||||
|
Online
|
||||||
|
</BadgeWithDot>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Settings
|
||||||
|
</NavItemBase>
|
||||||
|
<NavItemBase type="link" href="https://www.untitledui.com/" icon={Settings01}>
|
||||||
|
Open in browser
|
||||||
|
</NavItemBase>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<NavAccountCard />
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
</MobileNavigationHeader>
|
||||||
|
|
||||||
|
<header className="max-lg:hidden">
|
||||||
|
<section
|
||||||
|
className={cx(
|
||||||
|
"flex h-16 w-full items-center justify-center bg-primary md:h-18",
|
||||||
|
(!hideBorder || showSecondaryNav) && "border-b border-secondary",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex w-full max-w-container justify-between pr-3 pl-4 md:px-8">
|
||||||
|
<div className="flex flex-1 items-center gap-4">
|
||||||
|
<a
|
||||||
|
aria-label="Go to homepage"
|
||||||
|
href="/"
|
||||||
|
className="rounded-xs outline-focus-ring focus-visible:outline-2 focus-visible:outline-offset-2"
|
||||||
|
>
|
||||||
|
<UntitledLogo className="h-8" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<nav>
|
||||||
|
<ul className="flex items-center gap-0.5">
|
||||||
|
{items.map((item) => (
|
||||||
|
<li key={item.label} className="py-0.5">
|
||||||
|
<NavItemBase icon={item.icon} href={item.href} current={item.current} badge={item.badge} type="link">
|
||||||
|
{item.label}
|
||||||
|
</NavItemBase>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
{trailingContent}
|
||||||
|
|
||||||
|
<div className="flex gap-0.5">
|
||||||
|
<NavItemButton
|
||||||
|
current={activeUrl === "/settings-01"}
|
||||||
|
size="md"
|
||||||
|
icon={Settings01}
|
||||||
|
label="Settings"
|
||||||
|
href="/settings-01"
|
||||||
|
tooltipPlacement="bottom"
|
||||||
|
/>
|
||||||
|
<NavItemButton
|
||||||
|
current={activeUrl === "/notifications-01"}
|
||||||
|
size="md"
|
||||||
|
icon={Bell01}
|
||||||
|
label="Notifications"
|
||||||
|
href="/notifications-01"
|
||||||
|
tooltipPlacement="bottom"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{showAvatarDropdown && (
|
||||||
|
<DialogTrigger>
|
||||||
|
<AriaButton
|
||||||
|
className={({ isPressed, isFocused }) =>
|
||||||
|
cx(
|
||||||
|
"group relative inline-flex cursor-pointer",
|
||||||
|
(isPressed || isFocused) && "rounded-full outline-2 outline-offset-2 outline-focus-ring",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Avatar alt="Olivia Rhye" src="https://www.untitledui.com/images/avatars/olivia-rhye?bg=%23E0E0E0" size="md" />
|
||||||
|
</AriaButton>
|
||||||
|
<Popover
|
||||||
|
placement="bottom right"
|
||||||
|
offset={8}
|
||||||
|
className={({ isEntering, isExiting }) =>
|
||||||
|
cx(
|
||||||
|
"will-change-transform",
|
||||||
|
isEntering &&
|
||||||
|
"duration-300 ease-out animate-in fade-in placement-right:slide-in-from-left-2 placement-top:slide-in-from-bottom-2 placement-bottom:slide-in-from-top-2",
|
||||||
|
isExiting &&
|
||||||
|
"duration-150 ease-in animate-out fade-out placement-right:slide-out-to-left-2 placement-top:slide-out-to-bottom-2 placement-bottom:slide-out-to-top-2",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<NavAccountMenu />
|
||||||
|
</Popover>
|
||||||
|
</DialogTrigger>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{showSecondaryNav && (
|
||||||
|
<section className={cx("flex h-16 w-full items-center justify-center bg-primary", !hideBorder && "border-b border-secondary")}>
|
||||||
|
<div className="flex w-full max-w-container items-center justify-between gap-8 px-8">
|
||||||
|
<nav>
|
||||||
|
<ul className="flex items-center gap-0.5">
|
||||||
|
{activeSubNavItems.map((item) => (
|
||||||
|
<li key={item.label} className="py-0.5">
|
||||||
|
<NavItemBase icon={item.icon} href={item.href} current={item.current} badge={item.badge} type="link">
|
||||||
|
{item.label}
|
||||||
|
</NavItemBase>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<Input shortcut aria-label="Search" placeholder="Search" icon={SearchLg} size="sm" className="max-w-xs" />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
</header>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export { MobileNavigationHeader } from "./base-components/mobile-header";
|
||||||
|
export { NavAccountCard } from "./base-components/nav-account-card";
|
||||||
|
export { NavItemButton } from "./base-components/nav-item-button";
|
||||||
|
export { NavItemBase } from "./base-components/nav-item";
|
||||||
|
export { NavList } from "./base-components/nav-list";
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
import type { ReactNode } from "react";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { SearchLg } from "@untitledui/icons";
|
||||||
|
import { AnimatePresence, motion } from "motion/react";
|
||||||
|
import { Input } from "@/components/base/input/input";
|
||||||
|
import { UntitledLogo } from "@/components/foundations/logo/untitledui-logo";
|
||||||
|
import { cx } from "@/utils/cx";
|
||||||
|
import { MobileNavigationHeader } from "../base-components/mobile-header";
|
||||||
|
import { NavAccountCard } from "../base-components/nav-account-card";
|
||||||
|
import { NavItemBase } from "../base-components/nav-item";
|
||||||
|
import { NavList } from "../base-components/nav-list";
|
||||||
|
import type { NavItemType } from "../config";
|
||||||
|
|
||||||
|
interface SidebarNavigationDualTierProps {
|
||||||
|
/** URL of the currently active item. */
|
||||||
|
activeUrl?: string;
|
||||||
|
/** Feature card to display. */
|
||||||
|
featureCard?: ReactNode;
|
||||||
|
/** List of items to display. */
|
||||||
|
items: NavItemType[];
|
||||||
|
/** List of footer items to display. */
|
||||||
|
footerItems?: NavItemType[];
|
||||||
|
/** Whether to hide the right side border. */
|
||||||
|
hideBorder?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SidebarNavigationDualTier = ({ activeUrl, hideBorder, items, footerItems = [], featureCard }: SidebarNavigationDualTierProps) => {
|
||||||
|
const activeItem = [...items, ...footerItems].find((item) => item.href === activeUrl || item.items?.some((subItem) => subItem.href === activeUrl));
|
||||||
|
const [currentItem, setCurrentItem] = useState(activeItem || items[1]);
|
||||||
|
const [isHovering, setIsHovering] = useState(false);
|
||||||
|
|
||||||
|
const isSecondarySidebarVisible = isHovering && Boolean(currentItem.items?.length);
|
||||||
|
|
||||||
|
const MAIN_SIDEBAR_WIDTH = 296;
|
||||||
|
const SECONDARY_SIDEBAR_WIDTH = 256;
|
||||||
|
|
||||||
|
const mainSidebar = (
|
||||||
|
<aside className="group flex h-full max-h-full max-w-full overflow-y-auto bg-primary">
|
||||||
|
<div
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
"--width": `${MAIN_SIDEBAR_WIDTH}px`,
|
||||||
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
|
className={cx(
|
||||||
|
"relative flex w-full flex-col border-r border-secondary pt-4 transition duration-300 lg:w-(--width) lg:pt-6",
|
||||||
|
hideBorder && !isSecondarySidebarVisible && "border-transparent",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-5 px-4 lg:px-5">
|
||||||
|
<UntitledLogo className="h-8" />
|
||||||
|
<Input shortcut size="sm" aria-label="Search" placeholder="Search" icon={SearchLg} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<NavList activeUrl={activeUrl} items={items} className="lg:hidden" />
|
||||||
|
|
||||||
|
<ul className="mt-4 hidden flex-col px-4 lg:flex">
|
||||||
|
{items.map((item) => (
|
||||||
|
<li key={item.label + item.href} className="py-0.5">
|
||||||
|
<NavItemBase
|
||||||
|
current={currentItem.href === item.href}
|
||||||
|
href={item.href}
|
||||||
|
badge={item.badge}
|
||||||
|
icon={item.icon}
|
||||||
|
type="link"
|
||||||
|
onClick={() => setCurrentItem(item)}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</NavItemBase>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
<div className="mt-auto flex flex-col gap-4 px-2 py-4 lg:px-4 lg:py-6">
|
||||||
|
{footerItems.length > 0 && (
|
||||||
|
<ul className="flex flex-col">
|
||||||
|
{footerItems.map((item) => (
|
||||||
|
<li key={item.label + item.href} className="py-0.5">
|
||||||
|
<NavItemBase
|
||||||
|
current={currentItem.href === item.href}
|
||||||
|
href={item.href}
|
||||||
|
badge={item.badge}
|
||||||
|
icon={item.icon}
|
||||||
|
type="link"
|
||||||
|
onClick={() => setCurrentItem(item)}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</NavItemBase>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{featureCard}
|
||||||
|
|
||||||
|
<NavAccountCard />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
);
|
||||||
|
|
||||||
|
const secondarySidebar = (
|
||||||
|
<AnimatePresence initial={false}>
|
||||||
|
{isSecondarySidebarVisible && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ width: 0, borderColor: "var(--color-border-secondary)" }}
|
||||||
|
animate={{ width: SECONDARY_SIDEBAR_WIDTH, borderColor: "var(--color-border-secondary)" }}
|
||||||
|
exit={{ width: 0, borderColor: "rgba(0,0,0,0)", transition: { borderColor: { type: "tween", delay: 0.05 } } }}
|
||||||
|
transition={{ type: "spring", damping: 26, stiffness: 220, bounce: 0 }}
|
||||||
|
className={cx("relative h-full overflow-x-hidden overflow-y-auto bg-primary", !hideBorder && "box-content border-r-[1.5px]")}
|
||||||
|
>
|
||||||
|
<ul style={{ width: SECONDARY_SIDEBAR_WIDTH }} className="flex h-full flex-col p-4 py-6">
|
||||||
|
{currentItem.items?.map((item) => (
|
||||||
|
<li key={item.label + item.href} className="py-0.5">
|
||||||
|
<NavItemBase current={activeUrl === item.href} href={item.href} icon={item.icon} badge={item.badge} type="link">
|
||||||
|
{item.label}
|
||||||
|
</NavItemBase>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Mobile header navigation */}
|
||||||
|
<MobileNavigationHeader>{mainSidebar}</MobileNavigationHeader>
|
||||||
|
|
||||||
|
{/* Desktop sidebar navigation */}
|
||||||
|
<div
|
||||||
|
className="z-50 hidden lg:fixed lg:inset-y-0 lg:left-0 lg:flex"
|
||||||
|
onPointerEnter={() => setIsHovering(true)}
|
||||||
|
onPointerLeave={() => setIsHovering(false)}
|
||||||
|
>
|
||||||
|
{mainSidebar}
|
||||||
|
{secondarySidebar}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Placeholder to take up physical space because the real sidebar has `fixed` position. */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
paddingLeft: MAIN_SIDEBAR_WIDTH,
|
||||||
|
}}
|
||||||
|
className="invisible hidden lg:sticky lg:top-0 lg:bottom-0 lg:left-0 lg:block"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import { SearchLg } from "@untitledui/icons";
|
||||||
|
import { Input } from "@/components/base/input/input";
|
||||||
|
import { UntitledLogo } from "@/components/foundations/logo/untitledui-logo";
|
||||||
|
import { MobileNavigationHeader } from "../base-components/mobile-header";
|
||||||
|
import { NavAccountCard } from "../base-components/nav-account-card";
|
||||||
|
import { NavList } from "../base-components/nav-list";
|
||||||
|
import type { NavItemDividerType, NavItemType } from "../config";
|
||||||
|
|
||||||
|
interface SidebarNavigationSectionDividersProps {
|
||||||
|
/** URL of the currently active item. */
|
||||||
|
activeUrl?: string;
|
||||||
|
/** List of items to display. */
|
||||||
|
items: (NavItemType | NavItemDividerType)[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SidebarNavigationSectionDividers = ({ activeUrl, items }: SidebarNavigationSectionDividersProps) => {
|
||||||
|
const MAIN_SIDEBAR_WIDTH = 292;
|
||||||
|
|
||||||
|
const content = (
|
||||||
|
<aside
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
"--width": `${MAIN_SIDEBAR_WIDTH}px`,
|
||||||
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
|
className="flex h-full w-full max-w-full flex-col justify-between overflow-auto border-secondary bg-primary pt-4 shadow-xs md:border-r lg:w-(--width) lg:rounded-xl lg:border lg:pt-5"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-5 px-4 lg:px-5">
|
||||||
|
<UntitledLogo className="h-8" />
|
||||||
|
<Input shortcut size="sm" aria-label="Search" placeholder="Search" icon={SearchLg} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<NavList activeUrl={activeUrl} items={items} className="mt-5" />
|
||||||
|
|
||||||
|
<div className="mt-auto flex flex-col gap-5 px-2 py-4 lg:gap-6 lg:px-4 lg:py-4">
|
||||||
|
<NavAccountCard />
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Mobile header navigation */}
|
||||||
|
<MobileNavigationHeader>{content}</MobileNavigationHeader>
|
||||||
|
|
||||||
|
{/* Desktop sidebar navigation */}
|
||||||
|
<div className="hidden lg:fixed lg:inset-y-0 lg:left-0 lg:flex lg:py-1 lg:pl-1">{content}</div>
|
||||||
|
|
||||||
|
{/* Placeholder to take up physical space because the real sidebar has `fixed` position. */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
paddingLeft: MAIN_SIDEBAR_WIDTH + 4, // Add 4px to account for the padding in the sidebar wrapper
|
||||||
|
}}
|
||||||
|
className="invisible hidden lg:sticky lg:top-0 lg:bottom-0 lg:left-0 lg:block"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import { UntitledLogo } from "@/components/foundations/logo/untitledui-logo";
|
||||||
|
import { MobileNavigationHeader } from "../base-components/mobile-header";
|
||||||
|
import { NavAccountCard } from "../base-components/nav-account-card";
|
||||||
|
import { NavItemBase } from "../base-components/nav-item";
|
||||||
|
import type { NavItemType } from "../config";
|
||||||
|
|
||||||
|
interface SidebarNavigationSectionsSubheadingsProps {
|
||||||
|
/** URL of the currently active item. */
|
||||||
|
activeUrl?: string;
|
||||||
|
/** List of items to display. */
|
||||||
|
items: Array<{ label: string; items: NavItemType[] }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SidebarNavigationSectionsSubheadings = ({ activeUrl = "/", items }: SidebarNavigationSectionsSubheadingsProps) => {
|
||||||
|
const MAIN_SIDEBAR_WIDTH = 292;
|
||||||
|
|
||||||
|
const content = (
|
||||||
|
<aside
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
"--width": `${MAIN_SIDEBAR_WIDTH}px`,
|
||||||
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
|
className="flex h-full w-full max-w-full flex-col justify-between overflow-auto border-secondary bg-primary pt-4 shadow-xs md:border-r lg:w-(--width) lg:rounded-xl lg:border lg:pt-5"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-5 px-4 lg:px-5">
|
||||||
|
<UntitledLogo className="h-8" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul className="mt-8">
|
||||||
|
{items.map((group) => (
|
||||||
|
<li key={group.label}>
|
||||||
|
<div className="px-5 pb-1">
|
||||||
|
<p className="text-xs font-bold text-quaternary uppercase">{group.label}</p>
|
||||||
|
</div>
|
||||||
|
<ul className="px-4 pb-5">
|
||||||
|
{group.items.map((item) => (
|
||||||
|
<li key={item.label} className="py-0.5">
|
||||||
|
<NavItemBase icon={item.icon} href={item.href} badge={item.badge} type="link" current={item.href === activeUrl}>
|
||||||
|
{item.label}
|
||||||
|
</NavItemBase>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div className="mt-auto flex flex-col gap-5 px-2 py-4 lg:gap-6 lg:px-4 lg:py-4">
|
||||||
|
<NavAccountCard />
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Mobile header navigation */}
|
||||||
|
<MobileNavigationHeader>{content}</MobileNavigationHeader>
|
||||||
|
|
||||||
|
{/* Desktop sidebar navigation */}
|
||||||
|
<div className="hidden lg:fixed lg:inset-y-0 lg:left-0 lg:flex lg:py-1 lg:pl-1">{content}</div>
|
||||||
|
|
||||||
|
{/* Placeholder to take up physical space because the real sidebar has `fixed` position. */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
paddingLeft: MAIN_SIDEBAR_WIDTH + 4,
|
||||||
|
}}
|
||||||
|
className="invisible hidden lg:sticky lg:top-0 lg:bottom-0 lg:left-0 lg:block"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
import type { ReactNode } from "react";
|
||||||
|
import { SearchLg } from "@untitledui/icons";
|
||||||
|
import { Input } from "@/components/base/input/input";
|
||||||
|
import { UntitledLogo } from "@/components/foundations/logo/untitledui-logo";
|
||||||
|
import { cx } from "@/utils/cx";
|
||||||
|
import { MobileNavigationHeader } from "../base-components/mobile-header";
|
||||||
|
import { NavAccountCard } from "../base-components/nav-account-card";
|
||||||
|
import { NavItemBase } from "../base-components/nav-item";
|
||||||
|
import { NavList } from "../base-components/nav-list";
|
||||||
|
import type { NavItemType } from "../config";
|
||||||
|
|
||||||
|
interface SidebarNavigationProps {
|
||||||
|
/** URL of the currently active item. */
|
||||||
|
activeUrl?: string;
|
||||||
|
/** List of items to display. */
|
||||||
|
items: NavItemType[];
|
||||||
|
/** List of footer items to display. */
|
||||||
|
footerItems?: NavItemType[];
|
||||||
|
/** Feature card to display. */
|
||||||
|
featureCard?: ReactNode;
|
||||||
|
/** Whether to show the account card. */
|
||||||
|
showAccountCard?: boolean;
|
||||||
|
/** Whether to hide the right side border. */
|
||||||
|
hideBorder?: boolean;
|
||||||
|
/** Additional CSS classes to apply to the sidebar. */
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SidebarNavigationSimple = ({
|
||||||
|
activeUrl,
|
||||||
|
items,
|
||||||
|
footerItems = [],
|
||||||
|
featureCard,
|
||||||
|
showAccountCard = true,
|
||||||
|
hideBorder = false,
|
||||||
|
className,
|
||||||
|
}: SidebarNavigationProps) => {
|
||||||
|
const MAIN_SIDEBAR_WIDTH = 296;
|
||||||
|
|
||||||
|
const content = (
|
||||||
|
<aside
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
"--width": `${MAIN_SIDEBAR_WIDTH}px`,
|
||||||
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
|
className={cx(
|
||||||
|
"flex h-full w-full max-w-full flex-col justify-between overflow-auto bg-primary pt-4 lg:w-(--width) lg:pt-6",
|
||||||
|
!hideBorder && "border-secondary md:border-r",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-5 px-4 lg:px-5">
|
||||||
|
<UntitledLogo className="h-8" />
|
||||||
|
<Input shortcut size="sm" aria-label="Search" placeholder="Search" icon={SearchLg} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<NavList activeUrl={activeUrl} items={items} />
|
||||||
|
|
||||||
|
<div className="mt-auto flex flex-col gap-4 px-2 py-4 lg:px-4 lg:py-6">
|
||||||
|
{footerItems.length > 0 && (
|
||||||
|
<ul className="flex flex-col">
|
||||||
|
{footerItems.map((item) => (
|
||||||
|
<li key={item.label} className="py-0.5">
|
||||||
|
<NavItemBase badge={item.badge} icon={item.icon} href={item.href} type="link" current={item.href === activeUrl}>
|
||||||
|
{item.label}
|
||||||
|
</NavItemBase>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{featureCard}
|
||||||
|
|
||||||
|
{showAccountCard && <NavAccountCard />}
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Mobile header navigation */}
|
||||||
|
<MobileNavigationHeader>{content}</MobileNavigationHeader>
|
||||||
|
|
||||||
|
{/* Desktop sidebar navigation */}
|
||||||
|
<div className="hidden lg:fixed lg:inset-y-0 lg:left-0 lg:flex">{content}</div>
|
||||||
|
|
||||||
|
{/* Placeholder to take up physical space because the real sidebar has `fixed` position. */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
paddingLeft: MAIN_SIDEBAR_WIDTH,
|
||||||
|
}}
|
||||||
|
className="invisible hidden lg:sticky lg:top-0 lg:bottom-0 lg:left-0 lg:block"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,226 @@
|
|||||||
|
import type { FC } from "react";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { LifeBuoy01, LogOut01, Settings01 } from "@untitledui/icons";
|
||||||
|
import { AnimatePresence, motion } from "motion/react";
|
||||||
|
import { Button as AriaButton, DialogTrigger as AriaDialogTrigger, Popover as AriaPopover } from "react-aria-components";
|
||||||
|
import { Avatar } from "@/components/base/avatar/avatar";
|
||||||
|
import { AvatarLabelGroup } from "@/components/base/avatar/avatar-label-group";
|
||||||
|
import { Button } from "@/components/base/buttons/button";
|
||||||
|
import { ButtonUtility } from "@/components/base/buttons/button-utility";
|
||||||
|
import { UntitledLogo } from "@/components/foundations/logo/untitledui-logo";
|
||||||
|
import { UntitledLogoMinimal } from "@/components/foundations/logo/untitledui-logo-minimal";
|
||||||
|
import { cx } from "@/utils/cx";
|
||||||
|
import { MobileNavigationHeader } from "../base-components/mobile-header";
|
||||||
|
import { NavAccountMenu } from "../base-components/nav-account-card";
|
||||||
|
import { NavItemBase } from "../base-components/nav-item";
|
||||||
|
import { NavItemButton } from "../base-components/nav-item-button";
|
||||||
|
import { NavList } from "../base-components/nav-list";
|
||||||
|
import type { NavItemType } from "../config";
|
||||||
|
|
||||||
|
interface SidebarNavigationSlimProps {
|
||||||
|
/** URL of the currently active item. */
|
||||||
|
activeUrl?: string;
|
||||||
|
/** List of items to display. */
|
||||||
|
items: (NavItemType & { icon: FC<{ className?: string }> })[];
|
||||||
|
/** List of footer items to display. */
|
||||||
|
footerItems?: (NavItemType & { icon: FC<{ className?: string }> })[];
|
||||||
|
/** Whether to hide the border. */
|
||||||
|
hideBorder?: boolean;
|
||||||
|
/** Whether to hide the right side border. */
|
||||||
|
hideRightBorder?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SidebarNavigationSlim = ({ activeUrl, items, footerItems = [], hideBorder, hideRightBorder }: SidebarNavigationSlimProps) => {
|
||||||
|
const activeItem = [...items, ...footerItems].find((item) => item.href === activeUrl || item.items?.some((subItem) => subItem.href === activeUrl));
|
||||||
|
const [currentItem, setCurrentItem] = useState(activeItem || items[1]);
|
||||||
|
const [isHovering, setIsHovering] = useState(false);
|
||||||
|
|
||||||
|
const isSecondarySidebarVisible = isHovering && Boolean(currentItem.items?.length);
|
||||||
|
|
||||||
|
const MAIN_SIDEBAR_WIDTH = 68;
|
||||||
|
const SECONDARY_SIDEBAR_WIDTH = 268;
|
||||||
|
|
||||||
|
const mainSidebar = (
|
||||||
|
<aside
|
||||||
|
style={{
|
||||||
|
width: MAIN_SIDEBAR_WIDTH,
|
||||||
|
}}
|
||||||
|
className={cx(
|
||||||
|
"group flex h-full max-h-full max-w-full overflow-y-auto py-1 pl-1 transition duration-100 ease-linear",
|
||||||
|
isSecondarySidebarVisible && "bg-primary",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={cx(
|
||||||
|
"flex w-auto flex-col justify-between rounded-xl bg-primary pt-5 ring-1 ring-secondary transition duration-300 ring-inset",
|
||||||
|
hideBorder && !isSecondarySidebarVisible && "ring-transparent",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex justify-center px-3">
|
||||||
|
<UntitledLogoMinimal className="size-8" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul className="mt-4 flex flex-col gap-0.5 px-3">
|
||||||
|
{items.map((item) => (
|
||||||
|
<li key={item.label}>
|
||||||
|
<NavItemButton
|
||||||
|
size="md"
|
||||||
|
current={currentItem.href === item.href}
|
||||||
|
href={item.href}
|
||||||
|
label={item.label || ""}
|
||||||
|
icon={item.icon}
|
||||||
|
onClick={() => setCurrentItem(item)}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
<div className="mt-auto flex flex-col gap-4 px-3 py-5">
|
||||||
|
{footerItems.length > 0 && (
|
||||||
|
<ul className="flex flex-col gap-0.5">
|
||||||
|
{footerItems.map((item) => (
|
||||||
|
<li key={item.label}>
|
||||||
|
<NavItemButton
|
||||||
|
size="md"
|
||||||
|
current={currentItem.href === item.href}
|
||||||
|
label={item.label || ""}
|
||||||
|
href={item.href}
|
||||||
|
icon={item.icon}
|
||||||
|
onClick={() => setCurrentItem(item)}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<AriaDialogTrigger>
|
||||||
|
<AriaButton
|
||||||
|
className={({ isPressed, isFocused }) =>
|
||||||
|
cx("group relative inline-flex rounded-full", (isPressed || isFocused) && "outline-2 outline-offset-2 outline-focus-ring")
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Avatar status="online" src="https://www.untitledui.com/images/avatars/olivia-rhye?fm=webp&q=80" size="md" alt="Olivia Rhye" />
|
||||||
|
</AriaButton>
|
||||||
|
<AriaPopover
|
||||||
|
placement="right bottom"
|
||||||
|
offset={8}
|
||||||
|
crossOffset={6}
|
||||||
|
className={({ isEntering, isExiting }) =>
|
||||||
|
cx(
|
||||||
|
"will-change-transform",
|
||||||
|
isEntering &&
|
||||||
|
"duration-300 ease-out animate-in fade-in placement-right:slide-in-from-left-2 placement-top:slide-in-from-bottom-2 placement-bottom:slide-in-from-top-2",
|
||||||
|
isExiting &&
|
||||||
|
"duration-150 ease-in animate-out fade-out placement-right:slide-out-to-left-2 placement-top:slide-out-to-bottom-2 placement-bottom:slide-out-to-top-2",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<NavAccountMenu />
|
||||||
|
</AriaPopover>
|
||||||
|
</AriaDialogTrigger>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
);
|
||||||
|
|
||||||
|
const secondarySidebar = (
|
||||||
|
<AnimatePresence initial={false}>
|
||||||
|
{isSecondarySidebarVisible && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ width: 0, borderColor: "var(--color-border-secondary)" }}
|
||||||
|
animate={{ width: SECONDARY_SIDEBAR_WIDTH, borderColor: "var(--color-border-secondary)" }}
|
||||||
|
exit={{ width: 0, borderColor: "rgba(0,0,0,0)", transition: { borderColor: { type: "tween", delay: 0.05 } } }}
|
||||||
|
transition={{ type: "spring", damping: 26, stiffness: 220, bounce: 0 }}
|
||||||
|
className={cx(
|
||||||
|
"relative h-full overflow-x-hidden overflow-y-auto bg-primary",
|
||||||
|
!(hideBorder || hideRightBorder) && "box-content border-r-[1.5px]",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div style={{ width: SECONDARY_SIDEBAR_WIDTH }} className="flex h-full flex-col px-4 pt-6">
|
||||||
|
<h3 className="text-sm font-semibold text-brand-secondary">{currentItem.label}</h3>
|
||||||
|
<ul className="py-2">
|
||||||
|
{currentItem.items?.map((item) => (
|
||||||
|
<li key={item.label} className="py-0.5">
|
||||||
|
<NavItemBase current={activeUrl === item.href} href={item.href} icon={item.icon} badge={item.badge} type="link">
|
||||||
|
{item.label}
|
||||||
|
</NavItemBase>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
<div className="sticky bottom-0 mt-auto flex justify-between border-t border-secondary bg-primary px-2 py-5">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-semibold text-primary">Olivia Rhye</p>
|
||||||
|
<p className="text-sm text-tertiary">olivia@untitledui.com</p>
|
||||||
|
</div>
|
||||||
|
<div className="absolute top-2.5 right-0">
|
||||||
|
<ButtonUtility size="sm" color="tertiary" tooltip="Log out" icon={LogOut01} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Desktop sidebar navigation */}
|
||||||
|
<div
|
||||||
|
className="z-50 hidden lg:fixed lg:inset-y-0 lg:left-0 lg:flex"
|
||||||
|
onPointerEnter={() => setIsHovering(true)}
|
||||||
|
onPointerLeave={() => setIsHovering(false)}
|
||||||
|
>
|
||||||
|
{mainSidebar}
|
||||||
|
{secondarySidebar}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Placeholder to take up physical space because the real sidebar has `fixed` position. */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
paddingLeft: MAIN_SIDEBAR_WIDTH,
|
||||||
|
}}
|
||||||
|
className="invisible hidden lg:sticky lg:top-0 lg:bottom-0 lg:left-0 lg:block"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Mobile header navigation */}
|
||||||
|
<MobileNavigationHeader>
|
||||||
|
<aside className="group flex h-full max-h-full w-full max-w-full flex-col justify-between overflow-y-auto bg-primary pt-4">
|
||||||
|
<div className="px-4">
|
||||||
|
<UntitledLogo className="h-8" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<NavList items={items} />
|
||||||
|
|
||||||
|
<div className="mt-auto flex flex-col gap-5 px-2 py-4">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<NavItemBase current={activeUrl === "/support"} type="link" href="/support" icon={LifeBuoy01}>
|
||||||
|
Support
|
||||||
|
</NavItemBase>
|
||||||
|
<NavItemBase current={activeUrl === "/settings"} type="link" href="/settings" icon={Settings01}>
|
||||||
|
Settings
|
||||||
|
</NavItemBase>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="relative flex items-center gap-3 border-t border-secondary pt-6 pr-8 pl-2">
|
||||||
|
<AvatarLabelGroup
|
||||||
|
status="online"
|
||||||
|
size="md"
|
||||||
|
src="https://www.untitledui.com/images/avatars/olivia-rhye?fm=webp&q=80"
|
||||||
|
title="Olivia Rhye"
|
||||||
|
subtitle="olivia@untitledui.com"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="absolute top-1/2 right-0 -translate-y-1/2">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
color="tertiary"
|
||||||
|
iconLeading={<LogOut01 className="size-5 text-fg-quaternary transition-inherit-all group-hover:text-fg-quaternary_hover" />}
|
||||||
|
className="p-1.5!"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
</MobileNavigationHeader>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
99
src/components/application/date-picker/calendar.tsx
Normal file
99
src/components/application/date-picker/calendar.tsx
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
import type { HTMLAttributes, PropsWithChildren } from "react";
|
||||||
|
import { Fragment, useContext, useState } from "react";
|
||||||
|
import { type CalendarDate, getLocalTimeZone, today } from "@internationalized/date";
|
||||||
|
import { ChevronLeft, ChevronRight } from "@untitledui/icons";
|
||||||
|
import type { CalendarProps as AriaCalendarProps, DateValue } from "react-aria-components";
|
||||||
|
import {
|
||||||
|
Calendar as AriaCalendar,
|
||||||
|
CalendarContext as AriaCalendarContext,
|
||||||
|
CalendarGrid as AriaCalendarGrid,
|
||||||
|
CalendarGridBody as AriaCalendarGridBody,
|
||||||
|
CalendarGridHeader as AriaCalendarGridHeader,
|
||||||
|
CalendarHeaderCell as AriaCalendarHeaderCell,
|
||||||
|
CalendarStateContext as AriaCalendarStateContext,
|
||||||
|
Heading as AriaHeading,
|
||||||
|
useSlottedContext,
|
||||||
|
} from "react-aria-components";
|
||||||
|
import { Button } from "@/components/base/buttons/button";
|
||||||
|
import { cx } from "@/utils/cx";
|
||||||
|
import { CalendarCell } from "./cell";
|
||||||
|
import { DateInput } from "./date-input";
|
||||||
|
|
||||||
|
export const CalendarContextProvider = ({ children }: PropsWithChildren) => {
|
||||||
|
const [value, onChange] = useState<DateValue | null>(null);
|
||||||
|
const [focusedValue, onFocusChange] = useState<DateValue | undefined>();
|
||||||
|
|
||||||
|
return <AriaCalendarContext.Provider value={{ value, onChange, focusedValue, onFocusChange }}>{children}</AriaCalendarContext.Provider>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const PresetButton = ({ value, children, ...props }: HTMLAttributes<HTMLButtonElement> & { value: CalendarDate }) => {
|
||||||
|
const context = useContext(AriaCalendarStateContext);
|
||||||
|
|
||||||
|
if (!context) {
|
||||||
|
throw new Error("Preset must be used within a Calendar component");
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClick = () => {
|
||||||
|
context.setValue(value);
|
||||||
|
context.setFocusedDate(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
{...props}
|
||||||
|
// It's important to give `null` explicitly to the `slot` prop
|
||||||
|
// otherwise the button will throw an error due to not using one of
|
||||||
|
// the required slots inside the Calendar component.
|
||||||
|
// Passing `null` will tell the button to not use a slot context.
|
||||||
|
slot={null}
|
||||||
|
size="md"
|
||||||
|
color="secondary"
|
||||||
|
onClick={handleClick}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface CalendarProps extends AriaCalendarProps<DateValue> {
|
||||||
|
/** The dates to highlight. */
|
||||||
|
highlightedDates?: DateValue[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Calendar = ({ highlightedDates, className, ...props }: CalendarProps) => {
|
||||||
|
const context = useSlottedContext(AriaCalendarContext)!;
|
||||||
|
|
||||||
|
const ContextWrapper = context ? Fragment : CalendarContextProvider;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ContextWrapper>
|
||||||
|
<AriaCalendar {...props} className={(state) => cx("flex flex-col gap-3", typeof className === "function" ? className(state) : className)}>
|
||||||
|
<header className="flex items-center justify-between">
|
||||||
|
<Button slot="previous" iconLeading={ChevronLeft} size="sm" color="tertiary" className="size-8" />
|
||||||
|
<AriaHeading className="text-sm font-semibold text-fg-secondary" />
|
||||||
|
<Button slot="next" iconLeading={ChevronRight} size="sm" color="tertiary" className="size-8" />
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<DateInput className="flex-1" />
|
||||||
|
<PresetButton value={today(getLocalTimeZone())}>Today</PresetButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<AriaCalendarGrid weekdayStyle="short" className="w-max">
|
||||||
|
<AriaCalendarGridHeader className="border-b-4 border-transparent">
|
||||||
|
{(day) => (
|
||||||
|
<AriaCalendarHeaderCell className="p-0">
|
||||||
|
<div className="flex size-10 items-center justify-center text-sm font-medium text-secondary">{day.slice(0, 2)}</div>
|
||||||
|
</AriaCalendarHeaderCell>
|
||||||
|
)}
|
||||||
|
</AriaCalendarGridHeader>
|
||||||
|
<AriaCalendarGridBody className="[&_td]:p-0 [&_tr]:border-b-4 [&_tr]:border-transparent [&_tr:last-of-type]:border-none">
|
||||||
|
{(date) => (
|
||||||
|
<CalendarCell date={date} isHighlighted={highlightedDates?.some((highlightedDate) => date.compare(highlightedDate) === 0)} />
|
||||||
|
)}
|
||||||
|
</AriaCalendarGridBody>
|
||||||
|
</AriaCalendarGrid>
|
||||||
|
</AriaCalendar>
|
||||||
|
</ContextWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
104
src/components/application/date-picker/cell.tsx
Normal file
104
src/components/application/date-picker/cell.tsx
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
import { getDayOfWeek, getLocalTimeZone, isToday } from "@internationalized/date";
|
||||||
|
import type { CalendarCellProps as AriaCalendarCellProps } from "react-aria-components";
|
||||||
|
import { CalendarCell as AriaCalendarCell, RangeCalendarContext, useLocale, useSlottedContext } from "react-aria-components";
|
||||||
|
import { cx } from "@/utils/cx";
|
||||||
|
|
||||||
|
interface CalendarCellProps extends AriaCalendarCellProps {
|
||||||
|
/** Whether the calendar is a range calendar. */
|
||||||
|
isRangeCalendar?: boolean;
|
||||||
|
/** Whether the cell is highlighted. */
|
||||||
|
isHighlighted?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CalendarCell = ({ date, isHighlighted, ...props }: CalendarCellProps) => {
|
||||||
|
const { locale } = useLocale();
|
||||||
|
const dayOfWeek = getDayOfWeek(date, locale);
|
||||||
|
const rangeCalendarContext = useSlottedContext(RangeCalendarContext);
|
||||||
|
|
||||||
|
const isRangeCalendar = !!rangeCalendarContext;
|
||||||
|
|
||||||
|
const start = rangeCalendarContext?.value?.start;
|
||||||
|
const end = rangeCalendarContext?.value?.end;
|
||||||
|
|
||||||
|
const isAfterStart = start ? date.compare(start) > 0 : true;
|
||||||
|
const isBeforeEnd = end ? date.compare(end) < 0 : true;
|
||||||
|
|
||||||
|
const isAfterOrOnStart = start && date.compare(start) >= 0;
|
||||||
|
const isBeforeOrOnEnd = end && date.compare(end) <= 0;
|
||||||
|
const isInRange = isAfterOrOnStart && isBeforeOrOnEnd;
|
||||||
|
|
||||||
|
const lastDayOfMonth = new Date(date.year, date.month, 0).getDate();
|
||||||
|
const isLastDayOfMonth = date.day === lastDayOfMonth;
|
||||||
|
const isFirstDayOfMonth = date.day === 1;
|
||||||
|
|
||||||
|
const isTodayDate = isToday(date, getLocalTimeZone());
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AriaCalendarCell
|
||||||
|
{...props}
|
||||||
|
date={date}
|
||||||
|
className={({ isDisabled, isFocusVisible, isSelectionStart, isSelectionEnd, isSelected, isOutsideMonth }) => {
|
||||||
|
const isRoundedLeft = isSelectionStart || dayOfWeek === 0;
|
||||||
|
const isRoundedRight = isSelectionEnd || dayOfWeek === 6;
|
||||||
|
|
||||||
|
return cx(
|
||||||
|
"relative size-10 focus:outline-none",
|
||||||
|
isRoundedLeft && "rounded-l-full",
|
||||||
|
isRoundedRight && "rounded-r-full",
|
||||||
|
isInRange && isDisabled && "bg-active",
|
||||||
|
isSelected && isRangeCalendar && "bg-active",
|
||||||
|
isDisabled ? "pointer-events-none" : "cursor-pointer",
|
||||||
|
isFocusVisible ? "z-10" : "z-0",
|
||||||
|
isRangeCalendar && isOutsideMonth && "hidden",
|
||||||
|
|
||||||
|
// Show gradient on last day of month if it's within the selected range.
|
||||||
|
isLastDayOfMonth &&
|
||||||
|
isSelected &&
|
||||||
|
isBeforeEnd &&
|
||||||
|
isRangeCalendar &&
|
||||||
|
"after:absolute after:inset-0 after:translate-x-full after:bg-gradient-to-l after:from-transparent after:to-bg-active in-[[role=gridcell]:last-child]:after:hidden",
|
||||||
|
|
||||||
|
// Show gradient on first day of month if it's within the selected range.
|
||||||
|
isFirstDayOfMonth &&
|
||||||
|
isSelected &&
|
||||||
|
isAfterStart &&
|
||||||
|
isRangeCalendar &&
|
||||||
|
"after:absolute after:inset-0 after:-translate-x-full after:bg-gradient-to-r after:from-transparent after:to-bg-active in-[[role=gridcell]:first-child]:after:hidden",
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{({ isDisabled, isFocusVisible, isSelectionStart, isSelectionEnd, isSelected, formattedDate }) => {
|
||||||
|
const markedAsSelected = isSelectionStart || isSelectionEnd || (isSelected && !isDisabled && !isRangeCalendar);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cx(
|
||||||
|
"relative flex size-full items-center justify-center rounded-full text-sm",
|
||||||
|
// Disabled state.
|
||||||
|
isDisabled ? "text-disabled" : "text-secondary hover:text-secondary_hover",
|
||||||
|
// Focus ring, visible while the cell has keyboard focus.
|
||||||
|
isFocusVisible ? "outline-2 outline-offset-2 outline-focus-ring" : "",
|
||||||
|
// Hover state for cells in the middle of the range.
|
||||||
|
isSelected && !isDisabled && isRangeCalendar ? "font-medium" : "",
|
||||||
|
markedAsSelected && "bg-brand-solid font-medium text-white hover:bg-brand-solid_hover hover:text-white",
|
||||||
|
// Hover state for non-selected cells.
|
||||||
|
!isSelected && !isDisabled ? "hover:bg-primary_hover hover:font-medium!" : "",
|
||||||
|
!isSelected && isTodayDate ? "bg-active font-medium hover:bg-secondary_hover" : "",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{formattedDate}
|
||||||
|
|
||||||
|
{(isHighlighted || isTodayDate) && (
|
||||||
|
<div
|
||||||
|
className={cx(
|
||||||
|
"absolute bottom-1 left-1/2 size-1.25 -translate-x-1/2 rounded-full",
|
||||||
|
isDisabled ? "bg-fg-disabled" : markedAsSelected ? "bg-fg-white" : "bg-fg-brand-primary",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</AriaCalendarCell>
|
||||||
|
);
|
||||||
|
};
|
||||||
30
src/components/application/date-picker/date-input.tsx
Normal file
30
src/components/application/date-picker/date-input.tsx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import type { DateInputProps as AriaDateInputProps } from "react-aria-components";
|
||||||
|
import { DateInput as AriaDateInput, DateSegment as AriaDateSegment } from "react-aria-components";
|
||||||
|
import { cx } from "@/utils/cx";
|
||||||
|
|
||||||
|
interface DateInputProps extends Omit<AriaDateInputProps, "children"> {}
|
||||||
|
|
||||||
|
export const DateInput = (props: DateInputProps) => {
|
||||||
|
return (
|
||||||
|
<AriaDateInput
|
||||||
|
{...props}
|
||||||
|
className={cx(
|
||||||
|
"flex rounded-lg bg-primary px-2.5 py-2 text-md shadow-xs ring-1 ring-primary ring-inset focus-within:ring-2 focus-within:ring-brand",
|
||||||
|
typeof props.className === "string" && props.className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{(segment) => (
|
||||||
|
<AriaDateSegment
|
||||||
|
segment={segment}
|
||||||
|
className={cx(
|
||||||
|
"rounded px-0.5 text-primary tabular-nums caret-transparent focus:bg-brand-solid focus:font-medium focus:text-white focus:outline-hidden",
|
||||||
|
// The placeholder segment.
|
||||||
|
segment.isPlaceholder && "text-placeholder uppercase",
|
||||||
|
// The separator "/" segment.
|
||||||
|
segment.type === "literal" && "text-fg-quaternary",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</AriaDateInput>
|
||||||
|
);
|
||||||
|
};
|
||||||
84
src/components/application/date-picker/date-picker.tsx
Normal file
84
src/components/application/date-picker/date-picker.tsx
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import { getLocalTimeZone, today } from "@internationalized/date";
|
||||||
|
import { useControlledState } from "@react-stately/utils";
|
||||||
|
import { Calendar as CalendarIcon } from "@untitledui/icons";
|
||||||
|
import { useDateFormatter } from "react-aria";
|
||||||
|
import type { DatePickerProps as AriaDatePickerProps, DateValue } from "react-aria-components";
|
||||||
|
import { DatePicker as AriaDatePicker, Dialog as AriaDialog, Group as AriaGroup, Popover as AriaPopover } from "react-aria-components";
|
||||||
|
import { Button } from "@/components/base/buttons/button";
|
||||||
|
import { cx } from "@/utils/cx";
|
||||||
|
import { Calendar } from "./calendar";
|
||||||
|
|
||||||
|
const highlightedDates = [today(getLocalTimeZone())];
|
||||||
|
|
||||||
|
interface DatePickerProps extends AriaDatePickerProps<DateValue> {
|
||||||
|
/** The function to call when the apply button is clicked. */
|
||||||
|
onApply?: () => void;
|
||||||
|
/** The function to call when the cancel button is clicked. */
|
||||||
|
onCancel?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DatePicker = ({ value: valueProp, defaultValue, onChange, onApply, onCancel, ...props }: DatePickerProps) => {
|
||||||
|
const formatter = useDateFormatter({
|
||||||
|
month: "short",
|
||||||
|
day: "numeric",
|
||||||
|
year: "numeric",
|
||||||
|
});
|
||||||
|
const [value, setValue] = useControlledState(valueProp, defaultValue || null, onChange);
|
||||||
|
|
||||||
|
const formattedDate = value ? formatter.format(value.toDate(getLocalTimeZone())) : "Select date";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AriaDatePicker shouldCloseOnSelect={false} {...props} value={value} onChange={setValue}>
|
||||||
|
<AriaGroup>
|
||||||
|
<Button size="md" color="secondary" iconLeading={CalendarIcon}>
|
||||||
|
{formattedDate}
|
||||||
|
</Button>
|
||||||
|
</AriaGroup>
|
||||||
|
<AriaPopover
|
||||||
|
offset={8}
|
||||||
|
placement="bottom right"
|
||||||
|
className={({ isEntering, isExiting }) =>
|
||||||
|
cx(
|
||||||
|
"origin-(--trigger-anchor-point) will-change-transform",
|
||||||
|
isEntering &&
|
||||||
|
"duration-150 ease-out animate-in fade-in placement-right:slide-in-from-left-0.5 placement-top:slide-in-from-bottom-0.5 placement-bottom:slide-in-from-top-0.5",
|
||||||
|
isExiting &&
|
||||||
|
"duration-100 ease-in animate-out fade-out placement-right:slide-out-to-left-0.5 placement-top:slide-out-to-bottom-0.5 placement-bottom:slide-out-to-top-0.5",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<AriaDialog className="rounded-2xl bg-primary shadow-xl ring ring-secondary_alt">
|
||||||
|
{({ close }) => (
|
||||||
|
<>
|
||||||
|
<div className="flex px-6 py-5">
|
||||||
|
<Calendar highlightedDates={highlightedDates} />
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-3 border-t border-secondary p-4">
|
||||||
|
<Button
|
||||||
|
size="md"
|
||||||
|
color="secondary"
|
||||||
|
onClick={() => {
|
||||||
|
onCancel?.();
|
||||||
|
close();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="md"
|
||||||
|
color="primary"
|
||||||
|
onClick={() => {
|
||||||
|
onApply?.();
|
||||||
|
close();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Apply
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</AriaDialog>
|
||||||
|
</AriaPopover>
|
||||||
|
</AriaDatePicker>
|
||||||
|
);
|
||||||
|
};
|
||||||
161
src/components/application/date-picker/date-range-picker.tsx
Normal file
161
src/components/application/date-picker/date-range-picker.tsx
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
import { useMemo, useState } from "react";
|
||||||
|
import { endOfMonth, endOfWeek, getLocalTimeZone, startOfMonth, startOfWeek, today } from "@internationalized/date";
|
||||||
|
import { useControlledState } from "@react-stately/utils";
|
||||||
|
import { Calendar as CalendarIcon } from "@untitledui/icons";
|
||||||
|
import { useDateFormatter } from "react-aria";
|
||||||
|
import type { DateRangePickerProps as AriaDateRangePickerProps, DateValue } from "react-aria-components";
|
||||||
|
import { DateRangePicker as AriaDateRangePicker, Dialog as AriaDialog, Group as AriaGroup, Popover as AriaPopover, useLocale } from "react-aria-components";
|
||||||
|
import { Button } from "@/components/base/buttons/button";
|
||||||
|
import { cx } from "@/utils/cx";
|
||||||
|
import { DateInput } from "./date-input";
|
||||||
|
import { RangeCalendar } from "./range-calendar";
|
||||||
|
import { RangePresetButton } from "./range-preset";
|
||||||
|
|
||||||
|
const now = today(getLocalTimeZone());
|
||||||
|
|
||||||
|
const highlightedDates = [today(getLocalTimeZone())];
|
||||||
|
|
||||||
|
interface DateRangePickerProps extends AriaDateRangePickerProps<DateValue> {
|
||||||
|
/** The function to call when the apply button is clicked. */
|
||||||
|
onApply?: () => void;
|
||||||
|
/** The function to call when the cancel button is clicked. */
|
||||||
|
onCancel?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DateRangePicker = ({ value: valueProp, defaultValue, onChange, onApply, onCancel, ...props }: DateRangePickerProps) => {
|
||||||
|
const { locale } = useLocale();
|
||||||
|
const formatter = useDateFormatter({
|
||||||
|
month: "short",
|
||||||
|
day: "numeric",
|
||||||
|
year: "numeric",
|
||||||
|
});
|
||||||
|
const [value, setValue] = useControlledState(valueProp, defaultValue || null, onChange);
|
||||||
|
const [focusedValue, setFocusedValue] = useState<DateValue | null>(null);
|
||||||
|
|
||||||
|
const formattedStartDate = value?.start ? formatter.format(value.start.toDate(getLocalTimeZone())) : "Select date";
|
||||||
|
const formattedEndDate = value?.end ? formatter.format(value.end.toDate(getLocalTimeZone())) : "Select date";
|
||||||
|
|
||||||
|
const presets = useMemo(
|
||||||
|
() => ({
|
||||||
|
today: { label: "Today", value: { start: now, end: now } },
|
||||||
|
yesterday: { label: "Yesterday", value: { start: now.subtract({ days: 1 }), end: now.subtract({ days: 1 }) } },
|
||||||
|
thisWeek: { label: "This week", value: { start: startOfWeek(now, locale), end: endOfWeek(now, locale) } },
|
||||||
|
lastWeek: {
|
||||||
|
label: "Last week",
|
||||||
|
value: {
|
||||||
|
start: startOfWeek(now, locale).subtract({ weeks: 1 }),
|
||||||
|
end: endOfWeek(now, locale).subtract({ weeks: 1 }),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
thisMonth: { label: "This month", value: { start: startOfMonth(now), end: endOfMonth(now) } },
|
||||||
|
lastMonth: {
|
||||||
|
label: "Last month",
|
||||||
|
value: {
|
||||||
|
start: startOfMonth(now).subtract({ months: 1 }),
|
||||||
|
end: endOfMonth(now).subtract({ months: 1 }),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
thisYear: { label: "This year", value: { start: startOfMonth(now.set({ month: 1 })), end: endOfMonth(now.set({ month: 12 })) } },
|
||||||
|
lastYear: {
|
||||||
|
label: "Last year",
|
||||||
|
value: {
|
||||||
|
start: startOfMonth(now.set({ month: 1 }).subtract({ years: 1 })),
|
||||||
|
end: endOfMonth(now.set({ month: 12 }).subtract({ years: 1 })),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
allTime: {
|
||||||
|
label: "All time",
|
||||||
|
value: {
|
||||||
|
start: now.set({ year: 2000, month: 1, day: 1 }),
|
||||||
|
end: now,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[locale],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AriaDateRangePicker aria-label="Date range picker" shouldCloseOnSelect={false} {...props} value={value} onChange={setValue}>
|
||||||
|
<AriaGroup>
|
||||||
|
<Button size="md" color="secondary" iconLeading={CalendarIcon}>
|
||||||
|
{!value ? <span className="text-placeholder">Select dates</span> : `${formattedStartDate} – ${formattedEndDate}`}
|
||||||
|
</Button>
|
||||||
|
</AriaGroup>
|
||||||
|
<AriaPopover
|
||||||
|
placement="bottom right"
|
||||||
|
offset={8}
|
||||||
|
className={({ isEntering, isExiting }) =>
|
||||||
|
cx(
|
||||||
|
"origin-(--trigger-anchor-point) will-change-transform",
|
||||||
|
isEntering &&
|
||||||
|
"duration-150 ease-out animate-in fade-in placement-right:slide-in-from-left-0.5 placement-top:slide-in-from-bottom-0.5 placement-bottom:slide-in-from-top-0.5",
|
||||||
|
isExiting &&
|
||||||
|
"duration-100 ease-in animate-out fade-out placement-right:slide-out-to-left-0.5 placement-top:slide-out-to-bottom-0.5 placement-bottom:slide-out-to-top-0.5",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<AriaDialog className="flex rounded-2xl bg-primary shadow-xl ring ring-secondary_alt focus:outline-hidden">
|
||||||
|
{({ close }) => (
|
||||||
|
<>
|
||||||
|
<div className="hidden w-38 flex-col gap-0.5 border-r border-solid border-secondary p-3 lg:flex">
|
||||||
|
{Object.values(presets).map((preset) => (
|
||||||
|
<RangePresetButton
|
||||||
|
key={preset.label}
|
||||||
|
value={preset.value}
|
||||||
|
onClick={() => {
|
||||||
|
setValue(preset.value);
|
||||||
|
setFocusedValue(preset.value.start);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{preset.label}
|
||||||
|
</RangePresetButton>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<RangeCalendar
|
||||||
|
focusedValue={focusedValue}
|
||||||
|
onFocusChange={setFocusedValue}
|
||||||
|
highlightedDates={highlightedDates}
|
||||||
|
presets={{
|
||||||
|
lastWeek: presets.lastWeek,
|
||||||
|
lastMonth: presets.lastMonth,
|
||||||
|
lastYear: presets.lastYear,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="flex justify-between gap-3 border-t border-secondary p-4">
|
||||||
|
<div className="hidden items-center gap-3 md:flex">
|
||||||
|
<DateInput slot="start" className="w-36" />
|
||||||
|
<div className="text-md text-quaternary">–</div>
|
||||||
|
<DateInput slot="end" className="w-36" />
|
||||||
|
</div>
|
||||||
|
<div className="grid w-full grid-cols-2 gap-3 md:flex md:w-auto">
|
||||||
|
<Button
|
||||||
|
size="md"
|
||||||
|
color="secondary"
|
||||||
|
onClick={() => {
|
||||||
|
onCancel?.();
|
||||||
|
close();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="md"
|
||||||
|
color="primary"
|
||||||
|
onClick={() => {
|
||||||
|
onApply?.();
|
||||||
|
close();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Apply
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</AriaDialog>
|
||||||
|
</AriaPopover>
|
||||||
|
</AriaDateRangePicker>
|
||||||
|
);
|
||||||
|
};
|
||||||
159
src/components/application/date-picker/range-calendar.tsx
Normal file
159
src/components/application/date-picker/range-calendar.tsx
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
import type { HTMLAttributes, PropsWithChildren } from "react";
|
||||||
|
import { Fragment, useContext, useState } from "react";
|
||||||
|
import type { CalendarDate } from "@internationalized/date";
|
||||||
|
import { ChevronLeft, ChevronRight } from "@untitledui/icons";
|
||||||
|
import { useDateFormatter } from "react-aria";
|
||||||
|
import type { RangeCalendarProps as AriaRangeCalendarProps, DateValue } from "react-aria-components";
|
||||||
|
import {
|
||||||
|
CalendarGrid as AriaCalendarGrid,
|
||||||
|
CalendarGridBody as AriaCalendarGridBody,
|
||||||
|
CalendarGridHeader as AriaCalendarGridHeader,
|
||||||
|
CalendarHeaderCell as AriaCalendarHeaderCell,
|
||||||
|
RangeCalendar as AriaRangeCalendar,
|
||||||
|
RangeCalendarContext,
|
||||||
|
RangeCalendarStateContext,
|
||||||
|
useSlottedContext,
|
||||||
|
} from "react-aria-components";
|
||||||
|
import { Button } from "@/components/base/buttons/button";
|
||||||
|
import { useBreakpoint } from "@/hooks/use-breakpoint";
|
||||||
|
import { CalendarCell } from "./cell";
|
||||||
|
import { DateInput } from "./date-input";
|
||||||
|
|
||||||
|
export const RangeCalendarContextProvider = ({ children }: PropsWithChildren) => {
|
||||||
|
const [value, onChange] = useState<{ start: DateValue; end: DateValue } | null>(null);
|
||||||
|
const [focusedValue, onFocusChange] = useState<DateValue | undefined>();
|
||||||
|
|
||||||
|
return <RangeCalendarContext.Provider value={{ value, onChange, focusedValue, onFocusChange }}>{children}</RangeCalendarContext.Provider>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const RangeCalendarTitle = ({ part }: { part: "start" | "end" }) => {
|
||||||
|
const context = useContext(RangeCalendarStateContext);
|
||||||
|
|
||||||
|
if (!context) {
|
||||||
|
throw new Error("<RangeCalendarTitle /> must be used within a <RangeCalendar /> component.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatter = useDateFormatter({
|
||||||
|
month: "long",
|
||||||
|
year: "numeric",
|
||||||
|
calendar: context.visibleRange.start.calendar.identifier,
|
||||||
|
timeZone: context.timeZone,
|
||||||
|
});
|
||||||
|
|
||||||
|
return part === "start"
|
||||||
|
? formatter.format(context.visibleRange.start.toDate(context.timeZone))
|
||||||
|
: formatter.format(context.visibleRange.end.toDate(context.timeZone));
|
||||||
|
};
|
||||||
|
|
||||||
|
const MobilePresetButton = ({ value, children, ...props }: HTMLAttributes<HTMLButtonElement> & { value: { start: DateValue; end: DateValue } }) => {
|
||||||
|
const context = useContext(RangeCalendarStateContext);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
{...props}
|
||||||
|
slot={null}
|
||||||
|
size="sm"
|
||||||
|
color="link-color"
|
||||||
|
onClick={() => {
|
||||||
|
context?.setValue(value);
|
||||||
|
context?.setFocusedDate(value.start as CalendarDate);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface RangeCalendarProps extends AriaRangeCalendarProps<DateValue> {
|
||||||
|
/** The dates to highlight. */
|
||||||
|
highlightedDates?: DateValue[];
|
||||||
|
/** The date presets to display. */
|
||||||
|
presets?: Record<string, { label: string; value: { start: DateValue; end: DateValue } }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RangeCalendar = ({ presets, ...props }: RangeCalendarProps) => {
|
||||||
|
const isDesktop = useBreakpoint("md");
|
||||||
|
const context = useSlottedContext(RangeCalendarContext);
|
||||||
|
|
||||||
|
const ContextWrapper = context ? Fragment : RangeCalendarContextProvider;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ContextWrapper>
|
||||||
|
<AriaRangeCalendar
|
||||||
|
className="flex items-start"
|
||||||
|
visibleDuration={{
|
||||||
|
months: isDesktop ? 2 : 1,
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-3 px-6 py-5">
|
||||||
|
<header className="relative flex items-center justify-between md:justify-start">
|
||||||
|
<Button slot="previous" iconLeading={ChevronLeft} size="sm" color="tertiary" className="size-8" />
|
||||||
|
|
||||||
|
<h2 className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-sm font-semibold text-fg-secondary">
|
||||||
|
<RangeCalendarTitle part="start" />
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<Button slot="next" iconLeading={ChevronRight} size="sm" color="tertiary" className="size-8 md:hidden" />
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{!isDesktop && (
|
||||||
|
<div className="flex items-center gap-2 md:hidden">
|
||||||
|
<DateInput slot="start" className="flex-1" />
|
||||||
|
<div className="text-md text-quaternary">–</div>
|
||||||
|
<DateInput slot="end" className="flex-1" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isDesktop && presets && (
|
||||||
|
<div className="mt-1 flex justify-between gap-3 px-2 md:hidden">
|
||||||
|
{Object.values(presets).map((preset) => (
|
||||||
|
<MobilePresetButton key={preset.label} value={preset.value}>
|
||||||
|
{preset.label}
|
||||||
|
</MobilePresetButton>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<AriaCalendarGrid weekdayStyle="short" className="w-max">
|
||||||
|
<AriaCalendarGridHeader>
|
||||||
|
{(day) => (
|
||||||
|
<AriaCalendarHeaderCell className="border-b-4 border-transparent p-0">
|
||||||
|
<div className="flex size-10 items-center justify-center text-sm font-medium text-secondary">{day.slice(0, 2)}</div>
|
||||||
|
</AriaCalendarHeaderCell>
|
||||||
|
)}
|
||||||
|
</AriaCalendarGridHeader>
|
||||||
|
<AriaCalendarGridBody className="[&_td]:p-0 [&_tr]:border-b-4 [&_tr]:border-transparent [&_tr:last-of-type]:border-none">
|
||||||
|
{(date) => <CalendarCell date={date} />}
|
||||||
|
</AriaCalendarGridBody>
|
||||||
|
</AriaCalendarGrid>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isDesktop && (
|
||||||
|
<div className="flex flex-col gap-3 border-l border-secondary px-6 py-5">
|
||||||
|
<header className="relative flex items-center justify-end">
|
||||||
|
<h2 className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-sm font-semibold text-fg-secondary">
|
||||||
|
<RangeCalendarTitle part="end" />
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<Button slot="next" iconLeading={ChevronRight} size="sm" color="tertiary" className="size-8" />
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<AriaCalendarGrid weekdayStyle="short" offset={{ months: 1 }} className="w-max">
|
||||||
|
<AriaCalendarGridHeader>
|
||||||
|
{(day) => (
|
||||||
|
<AriaCalendarHeaderCell className="border-b-4 border-transparent p-0">
|
||||||
|
<div className="flex size-10 items-center justify-center text-sm font-medium text-secondary">{day.slice(0, 2)}</div>
|
||||||
|
</AriaCalendarHeaderCell>
|
||||||
|
)}
|
||||||
|
</AriaCalendarGridHeader>
|
||||||
|
<AriaCalendarGridBody className="[&_td]:p-0 [&_tr]:border-b-4 [&_tr]:border-transparent [&_tr:last-of-type]:border-none">
|
||||||
|
{(date) => <CalendarCell date={date} />}
|
||||||
|
</AriaCalendarGridBody>
|
||||||
|
</AriaCalendarGrid>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</AriaRangeCalendar>
|
||||||
|
</ContextWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
26
src/components/application/date-picker/range-preset.tsx
Normal file
26
src/components/application/date-picker/range-preset.tsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { type HTMLAttributes } from "react";
|
||||||
|
import { type DateValue, RangeCalendarContext, useSlottedContext } from "react-aria-components";
|
||||||
|
import { cx } from "@/utils/cx";
|
||||||
|
|
||||||
|
interface RangePresetButtonProps extends HTMLAttributes<HTMLButtonElement> {
|
||||||
|
value: { start: DateValue; end: DateValue };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RangePresetButton = ({ value, className, children, ...props }: RangePresetButtonProps) => {
|
||||||
|
const context = useSlottedContext(RangeCalendarContext);
|
||||||
|
|
||||||
|
const isSelected = context?.value?.start?.compare(value.start) === 0 && context?.value?.end?.compare(value.end) === 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
{...props}
|
||||||
|
className={cx(
|
||||||
|
"cursor-pointer rounded-md px-3 py-2 text-left text-sm font-medium outline-focus-ring transition duration-100 ease-linear focus-visible:outline-2 focus-visible:outline-offset-2",
|
||||||
|
isSelected ? "bg-active text-secondary_hover hover:bg-secondary_hover" : "text-secondary hover:bg-primary_hover hover:text-secondary_hover",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
144
src/components/application/empty-state/empty-state.tsx
Normal file
144
src/components/application/empty-state/empty-state.tsx
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
import type { ComponentProps, ComponentPropsWithRef } from "react";
|
||||||
|
import { Children, createContext, isValidElement, useContext } from "react";
|
||||||
|
import { FileIcon } from "@untitledui/file-icons";
|
||||||
|
import { SearchLg } from "@untitledui/icons";
|
||||||
|
import { FeaturedIcon as FeaturedIconbase } from "@/components/foundations/featured-icon/featured-icon";
|
||||||
|
import type { BackgroundPatternProps } from "@/components/shared-assets/background-patterns";
|
||||||
|
import { BackgroundPattern } from "@/components/shared-assets/background-patterns";
|
||||||
|
import { Illustration as Illustrations } from "@/components/shared-assets/illustrations";
|
||||||
|
import { cx } from "@/utils/cx";
|
||||||
|
|
||||||
|
interface RootContextProps {
|
||||||
|
size?: "sm" | "md" | "lg";
|
||||||
|
}
|
||||||
|
|
||||||
|
const RootContext = createContext<RootContextProps>({ size: "lg" });
|
||||||
|
|
||||||
|
interface RootProps extends ComponentPropsWithRef<"div">, RootContextProps {}
|
||||||
|
|
||||||
|
const Root = ({ size = "lg", ...props }: RootProps) => {
|
||||||
|
return (
|
||||||
|
<RootContext.Provider value={{ size }}>
|
||||||
|
<div {...props} className={cx("mx-auto flex w-full max-w-lg flex-col items-center justify-center", props.className)} />
|
||||||
|
</RootContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const FeaturedIcon = ({ color = "gray", theme = "modern", icon = SearchLg, size = "lg", ...props }: ComponentPropsWithRef<typeof FeaturedIconbase>) => {
|
||||||
|
const { size: rootSize } = useContext(RootContext);
|
||||||
|
|
||||||
|
return <FeaturedIconbase {...props} {...{ color, theme, icon }} size={rootSize === "lg" ? "xl" : size} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Illustration = ({ type = "cloud", color = "gray", size = "lg", ...props }: ComponentPropsWithRef<typeof Illustrations>) => {
|
||||||
|
const { size: rootSize } = useContext(RootContext);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Illustrations
|
||||||
|
role="img"
|
||||||
|
{...props}
|
||||||
|
{...{ type, color }}
|
||||||
|
size={rootSize === "sm" ? "sm" : rootSize === "md" ? "md" : size}
|
||||||
|
className={cx("z-10", props.className)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface FileTypeIconProps extends ComponentPropsWithRef<"div"> {
|
||||||
|
type?: ComponentProps<typeof FileIcon>["type"];
|
||||||
|
theme?: ComponentProps<typeof FileIcon>["variant"];
|
||||||
|
}
|
||||||
|
|
||||||
|
const FileTypeIcon = ({ type = "folder", theme = "solid", ...props }: FileTypeIconProps) => {
|
||||||
|
return (
|
||||||
|
<div {...props} className={cx("relative z-10 flex rounded-full bg-linear-to-b from-gray-50 to-gray-200 p-8", props.className)}>
|
||||||
|
<FileIcon type={type} variant={theme} className="size-10 drop-shadow-sm" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface HeaderProps extends ComponentPropsWithRef<"div"> {
|
||||||
|
pattern?: "none" | BackgroundPatternProps["pattern"];
|
||||||
|
patternSize?: "sm" | "md" | "lg";
|
||||||
|
}
|
||||||
|
|
||||||
|
const Header = ({ pattern = "circle", patternSize = "md", ...props }: HeaderProps) => {
|
||||||
|
const { size } = useContext(RootContext);
|
||||||
|
// Whether we are passing `Illustration` component as children.
|
||||||
|
const hasIllustration = Children.toArray(props.children).some((headerChild) => isValidElement(headerChild) && headerChild.type === Illustration);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<header
|
||||||
|
{...props}
|
||||||
|
className={cx("relative mb-4", (size === "md" || size === "lg") && "mb-5", hasIllustration && size === "lg" && "mb-6!", props.className)}
|
||||||
|
>
|
||||||
|
{pattern !== "none" && (
|
||||||
|
<BackgroundPattern size={patternSize} pattern={pattern} className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2" />
|
||||||
|
)}
|
||||||
|
{props.children}
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Content = (props: ComponentPropsWithRef<"div">) => {
|
||||||
|
const { size } = useContext(RootContext);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main
|
||||||
|
{...props}
|
||||||
|
className={cx(
|
||||||
|
"z-10 mb-6 flex w-full max-w-88 flex-col items-center justify-center gap-1",
|
||||||
|
(size === "md" || size === "lg") && "mb-8 gap-2",
|
||||||
|
props.className,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Footer = (props: ComponentPropsWithRef<"div">) => {
|
||||||
|
return <footer {...props} className={cx("z-10 flex gap-3", props.className)} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Title = (props: ComponentPropsWithRef<"h1">) => {
|
||||||
|
const { size } = useContext(RootContext);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<h1
|
||||||
|
{...props}
|
||||||
|
className={cx(
|
||||||
|
"text-md font-semibold text-primary",
|
||||||
|
size === "md" && "text-lg font-semibold",
|
||||||
|
size === "lg" && "text-xl font-semibold",
|
||||||
|
props.className,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Description = (props: ComponentPropsWithRef<"p">) => {
|
||||||
|
const { size } = useContext(RootContext);
|
||||||
|
|
||||||
|
return <p {...props} className={cx("text-center text-sm text-tertiary", size === "lg" && "text-md", props.className)} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const EmptyState = Root as typeof Root & {
|
||||||
|
Title: typeof Title;
|
||||||
|
Header: typeof Header;
|
||||||
|
Footer: typeof Footer;
|
||||||
|
Content: typeof Content;
|
||||||
|
Description: typeof Description;
|
||||||
|
Illustration: typeof Illustration;
|
||||||
|
FeaturedIcon: typeof FeaturedIcon;
|
||||||
|
FileTypeIcon: typeof FileTypeIcon;
|
||||||
|
};
|
||||||
|
|
||||||
|
EmptyState.Title = Title;
|
||||||
|
EmptyState.Header = Header;
|
||||||
|
EmptyState.Footer = Footer;
|
||||||
|
EmptyState.Content = Content;
|
||||||
|
EmptyState.Description = Description;
|
||||||
|
EmptyState.Illustration = Illustration;
|
||||||
|
EmptyState.FeaturedIcon = FeaturedIcon;
|
||||||
|
EmptyState.FileTypeIcon = FileTypeIcon;
|
||||||
|
|
||||||
|
export { EmptyState };
|
||||||
394
src/components/application/file-upload/file-upload-base.tsx
Normal file
394
src/components/application/file-upload/file-upload-base.tsx
Normal file
@@ -0,0 +1,394 @@
|
|||||||
|
import type { ComponentProps, ComponentPropsWithRef } from "react";
|
||||||
|
import { useId, useRef, useState } from "react";
|
||||||
|
import type { FileIcon } from "@untitledui/file-icons";
|
||||||
|
import { FileIcon as FileTypeIcon } from "@untitledui/file-icons";
|
||||||
|
import { CheckCircle, Trash01, UploadCloud02, XCircle } from "@untitledui/icons";
|
||||||
|
import { AnimatePresence, motion } from "motion/react";
|
||||||
|
import { Button } from "@/components/base/buttons/button";
|
||||||
|
import { ButtonUtility } from "@/components/base/buttons/button-utility";
|
||||||
|
import { ProgressBar } from "@/components/base/progress-indicators/progress-indicators";
|
||||||
|
import { FeaturedIcon } from "@/components/foundations/featured-icon/featured-icon";
|
||||||
|
import { cx } from "@/utils/cx";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a human-readable file size.
|
||||||
|
* @param bytes - The size of the file in bytes.
|
||||||
|
* @returns A string representing the file size in a human-readable format.
|
||||||
|
*/
|
||||||
|
export const getReadableFileSize = (bytes: number) => {
|
||||||
|
if (bytes === 0) return "0 KB";
|
||||||
|
|
||||||
|
const suffixes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
|
||||||
|
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||||
|
|
||||||
|
return Math.floor(bytes / Math.pow(1024, i)) + " " + suffixes[i];
|
||||||
|
};
|
||||||
|
|
||||||
|
interface FileUploadDropZoneProps {
|
||||||
|
/** The class name of the drop zone. */
|
||||||
|
className?: string;
|
||||||
|
/**
|
||||||
|
* A hint text explaining what files can be dropped.
|
||||||
|
*/
|
||||||
|
hint?: string;
|
||||||
|
/**
|
||||||
|
* Disables dropping or uploading files.
|
||||||
|
*/
|
||||||
|
isDisabled?: boolean;
|
||||||
|
/**
|
||||||
|
* Specifies the types of files that the server accepts.
|
||||||
|
* Examples: "image/*", ".pdf,image/*", "image/*,video/mpeg,application/pdf"
|
||||||
|
*/
|
||||||
|
accept?: string;
|
||||||
|
/**
|
||||||
|
* Allows multiple file uploads.
|
||||||
|
*/
|
||||||
|
allowsMultiple?: boolean;
|
||||||
|
/**
|
||||||
|
* Maximum file size in bytes.
|
||||||
|
*/
|
||||||
|
maxSize?: number;
|
||||||
|
/**
|
||||||
|
* Callback function that is called with the list of dropped files
|
||||||
|
* when files are dropped on the drop zone.
|
||||||
|
*/
|
||||||
|
onDropFiles?: (files: FileList) => void;
|
||||||
|
/**
|
||||||
|
* Callback function that is called with the list of unaccepted files
|
||||||
|
* when files are dropped on the drop zone.
|
||||||
|
*/
|
||||||
|
onDropUnacceptedFiles?: (files: FileList) => void;
|
||||||
|
/**
|
||||||
|
* Callback function that is called with the list of files that exceed
|
||||||
|
* the size limit when files are dropped on the drop zone.
|
||||||
|
*/
|
||||||
|
onSizeLimitExceed?: (files: FileList) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FileUploadDropZone = ({
|
||||||
|
className,
|
||||||
|
hint,
|
||||||
|
isDisabled,
|
||||||
|
accept,
|
||||||
|
allowsMultiple = true,
|
||||||
|
maxSize,
|
||||||
|
onDropFiles,
|
||||||
|
onDropUnacceptedFiles,
|
||||||
|
onSizeLimitExceed,
|
||||||
|
}: FileUploadDropZoneProps) => {
|
||||||
|
const id = useId();
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const [isInvalid, setIsInvalid] = useState(false);
|
||||||
|
const [isDraggingOver, setIsDraggingOver] = useState(false);
|
||||||
|
|
||||||
|
const isFileTypeAccepted = (file: File): boolean => {
|
||||||
|
if (!accept) return true;
|
||||||
|
|
||||||
|
// Split the accept string into individual types
|
||||||
|
const acceptedTypes = accept.split(",").map((type) => type.trim());
|
||||||
|
|
||||||
|
return acceptedTypes.some((acceptedType) => {
|
||||||
|
// Handle file extensions (e.g., .pdf, .doc)
|
||||||
|
if (acceptedType.startsWith(".")) {
|
||||||
|
const extension = `.${file.name.split(".").pop()?.toLowerCase()}`;
|
||||||
|
return extension === acceptedType.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle wildcards (e.g., image/*)
|
||||||
|
if (acceptedType.endsWith("/*")) {
|
||||||
|
const typePrefix = acceptedType.split("/")[0];
|
||||||
|
return file.type.startsWith(`${typePrefix}/`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle exact MIME types (e.g., application/pdf)
|
||||||
|
return file.type === acceptedType;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDragIn = (event: React.DragEvent<HTMLDivElement>) => {
|
||||||
|
if (isDisabled) return;
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
setIsDraggingOver(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDragOut = (event: React.DragEvent<HTMLDivElement>) => {
|
||||||
|
if (isDisabled) return;
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
setIsDraggingOver(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const processFiles = (files: File[]): void => {
|
||||||
|
// Reset the invalid state when processing files.
|
||||||
|
setIsInvalid(false);
|
||||||
|
|
||||||
|
const acceptedFiles: File[] = [];
|
||||||
|
const unacceptedFiles: File[] = [];
|
||||||
|
const oversizedFiles: File[] = [];
|
||||||
|
|
||||||
|
// If multiple files are not allowed, only process the first file
|
||||||
|
const filesToProcess = allowsMultiple ? files : files.slice(0, 1);
|
||||||
|
|
||||||
|
filesToProcess.forEach((file) => {
|
||||||
|
// Check file size first
|
||||||
|
if (maxSize && file.size > maxSize) {
|
||||||
|
oversizedFiles.push(file);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then check file type
|
||||||
|
if (isFileTypeAccepted(file)) {
|
||||||
|
acceptedFiles.push(file);
|
||||||
|
} else {
|
||||||
|
unacceptedFiles.push(file);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle oversized files
|
||||||
|
if (oversizedFiles.length > 0 && typeof onSizeLimitExceed === "function") {
|
||||||
|
const dataTransfer = new DataTransfer();
|
||||||
|
oversizedFiles.forEach((file) => dataTransfer.items.add(file));
|
||||||
|
|
||||||
|
setIsInvalid(true);
|
||||||
|
onSizeLimitExceed(dataTransfer.files);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle accepted files
|
||||||
|
if (acceptedFiles.length > 0 && typeof onDropFiles === "function") {
|
||||||
|
const dataTransfer = new DataTransfer();
|
||||||
|
acceptedFiles.forEach((file) => dataTransfer.items.add(file));
|
||||||
|
onDropFiles(dataTransfer.files);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle unaccepted files
|
||||||
|
if (unacceptedFiles.length > 0 && typeof onDropUnacceptedFiles === "function") {
|
||||||
|
const unacceptedDataTransfer = new DataTransfer();
|
||||||
|
unacceptedFiles.forEach((file) => unacceptedDataTransfer.items.add(file));
|
||||||
|
|
||||||
|
setIsInvalid(true);
|
||||||
|
onDropUnacceptedFiles(unacceptedDataTransfer.files);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear the input value to ensure the same file can be selected again
|
||||||
|
if (inputRef.current) {
|
||||||
|
inputRef.current.value = "";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDrop = (event: React.DragEvent<HTMLDivElement>) => {
|
||||||
|
if (isDisabled) return;
|
||||||
|
|
||||||
|
handleDragOut(event);
|
||||||
|
processFiles(Array.from(event.dataTransfer.files));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInputFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
processFiles(Array.from(event.target.files || []));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-dropzone
|
||||||
|
onDragOver={handleDragIn}
|
||||||
|
onDragEnter={handleDragIn}
|
||||||
|
onDragLeave={handleDragOut}
|
||||||
|
onDragEnd={handleDragOut}
|
||||||
|
onDrop={handleDrop}
|
||||||
|
className={cx(
|
||||||
|
"relative flex flex-col items-center gap-3 rounded-xl bg-primary px-6 py-4 text-tertiary ring-1 ring-secondary transition duration-100 ease-linear ring-inset",
|
||||||
|
isDraggingOver && "ring-2 ring-brand",
|
||||||
|
isDisabled && "cursor-not-allowed bg-disabled_subtle ring-disabled_subtle",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<FeaturedIcon color="gray" theme="modern" size="md">
|
||||||
|
<UploadCloud02 className="size-5" />
|
||||||
|
</FeaturedIcon>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-1 text-center">
|
||||||
|
<div className="flex justify-center gap-1 text-center">
|
||||||
|
<input
|
||||||
|
ref={inputRef}
|
||||||
|
id={id}
|
||||||
|
type="file"
|
||||||
|
className="peer sr-only"
|
||||||
|
disabled={isDisabled}
|
||||||
|
accept={accept}
|
||||||
|
multiple={allowsMultiple}
|
||||||
|
onChange={handleInputFileChange}
|
||||||
|
/>
|
||||||
|
<label htmlFor={id} className="flex cursor-pointer">
|
||||||
|
<Button color="link-color" size="md" isDisabled={isDisabled} onClick={() => inputRef.current?.click()}>
|
||||||
|
Click to upload <span className="md:hidden">and attach files</span>
|
||||||
|
</Button>
|
||||||
|
</label>
|
||||||
|
<span className="text-sm max-md:hidden">or drag and drop</span>
|
||||||
|
</div>
|
||||||
|
<p className={cx("text-xs transition duration-100 ease-linear", isInvalid && "text-error-primary")}>
|
||||||
|
{hint || "SVG, PNG, JPG or GIF (max. 800x400px)"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface FileListItemProps {
|
||||||
|
/** The name of the file. */
|
||||||
|
name: string;
|
||||||
|
/** The size of the file. */
|
||||||
|
size: number;
|
||||||
|
/** The upload progress of the file. */
|
||||||
|
progress: number;
|
||||||
|
/** Whether the file failed to upload. */
|
||||||
|
failed?: boolean;
|
||||||
|
/** The type of the file. */
|
||||||
|
type?: ComponentProps<typeof FileIcon>["type"];
|
||||||
|
/** The class name of the file list item. */
|
||||||
|
className?: string;
|
||||||
|
/** The variant of the file icon. */
|
||||||
|
fileIconVariant?: ComponentProps<typeof FileTypeIcon>["variant"];
|
||||||
|
/** The function to call when the file is deleted. */
|
||||||
|
onDelete?: () => void;
|
||||||
|
/** The function to call when the file upload is retried. */
|
||||||
|
onRetry?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FileListItemProgressBar = ({ name, size, progress, failed, type, fileIconVariant, onDelete, onRetry, className }: FileListItemProps) => {
|
||||||
|
const isComplete = progress === 100;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.li
|
||||||
|
layout="position"
|
||||||
|
className={cx(
|
||||||
|
"relative flex gap-3 rounded-xl bg-primary p-4 ring-1 ring-secondary transition-shadow duration-100 ease-linear ring-inset",
|
||||||
|
failed && "ring-2 ring-error",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<FileTypeIcon className="size-10 shrink-0 dark:hidden" type={type ?? "empty"} theme="light" variant={fileIconVariant ?? "default"} />
|
||||||
|
<FileTypeIcon className="size-10 shrink-0 not-dark:hidden" type={type ?? "empty"} theme="dark" variant={fileIconVariant ?? "default"} />
|
||||||
|
|
||||||
|
<div className="flex min-w-0 flex-1 flex-col items-start">
|
||||||
|
<div className="flex w-full max-w-full min-w-0 flex-1">
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<p className="truncate text-sm font-medium text-secondary">{name}</p>
|
||||||
|
|
||||||
|
<div className="mt-0.5 flex items-center gap-2">
|
||||||
|
<p className="truncate text-sm whitespace-nowrap text-tertiary">{getReadableFileSize(size)}</p>
|
||||||
|
|
||||||
|
<hr className="h-3 w-px rounded-t-full rounded-b-full border-none bg-border-primary" />
|
||||||
|
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
{isComplete && <CheckCircle className="size-4 stroke-[2.5px] text-fg-success-primary" />}
|
||||||
|
{isComplete && <p className="text-sm font-medium text-success-primary">Complete</p>}
|
||||||
|
|
||||||
|
{!isComplete && !failed && <UploadCloud02 className="stroke-[2.5px size-4 text-fg-quaternary" />}
|
||||||
|
{!isComplete && !failed && <p className="text-sm font-medium text-quaternary">Uploading...</p>}
|
||||||
|
|
||||||
|
{failed && <XCircle className="size-4 text-fg-error-primary" />}
|
||||||
|
{failed && <p className="text-sm font-medium text-error-primary">Failed</p>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ButtonUtility color="tertiary" tooltip="Delete" icon={Trash01} size="xs" className="-mt-2 -mr-2 self-start" onClick={onDelete} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!failed && (
|
||||||
|
<div className="mt-1 w-full">
|
||||||
|
<ProgressBar labelPosition="right" max={100} min={0} value={progress} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{failed && (
|
||||||
|
<Button color="link-destructive" size="sm" onClick={onRetry} className="mt-1.5">
|
||||||
|
Try again
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</motion.li>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FileListItemProgressFill = ({ name, size, progress, failed, type, fileIconVariant, onDelete, onRetry, className }: FileListItemProps) => {
|
||||||
|
const isComplete = progress === 100;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.li layout="position" className={cx("relative flex gap-3 overflow-hidden rounded-xl bg-primary p-4", className)}>
|
||||||
|
{/* Progress fill. */}
|
||||||
|
<div
|
||||||
|
style={{ transform: `translateX(-${100 - progress}%)` }}
|
||||||
|
className={cx("absolute inset-0 size-full bg-secondary transition duration-75 ease-linear", isComplete && "opacity-0")}
|
||||||
|
role="progressbar"
|
||||||
|
aria-valuenow={progress}
|
||||||
|
aria-valuemin={0}
|
||||||
|
aria-valuemax={100}
|
||||||
|
/>
|
||||||
|
{/* Inner ring. */}
|
||||||
|
<div
|
||||||
|
className={cx(
|
||||||
|
"absolute inset-0 size-full rounded-[inherit] ring-1 ring-secondary transition duration-100 ease-linear ring-inset",
|
||||||
|
failed && "ring-2 ring-error",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FileTypeIcon className="relative size-10 shrink-0 dark:hidden" type={type ?? "empty"} theme="light" variant={fileIconVariant ?? "solid"} />
|
||||||
|
<FileTypeIcon className="relative size-10 shrink-0 not-dark:hidden" type={type ?? "empty"} theme="dark" variant={fileIconVariant ?? "solid"} />
|
||||||
|
|
||||||
|
<div className="relative flex min-w-0 flex-1">
|
||||||
|
<div className="relative flex min-w-0 flex-1 flex-col items-start">
|
||||||
|
<div className="w-full min-w-0 flex-1">
|
||||||
|
<p className="truncate text-sm font-medium text-secondary">{name}</p>
|
||||||
|
|
||||||
|
<div className="mt-0.5 flex items-center gap-2">
|
||||||
|
<p className="text-sm text-tertiary">{failed ? "Upload failed, please try again" : getReadableFileSize(size)}</p>
|
||||||
|
|
||||||
|
{!failed && (
|
||||||
|
<>
|
||||||
|
<hr className="h-3 w-px rounded-t-full rounded-b-full border-none bg-border-primary" />
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
{isComplete && <CheckCircle className="size-4 stroke-[2.5px] text-fg-success-primary" />}
|
||||||
|
{!isComplete && <UploadCloud02 className="size-4 stroke-[2.5px] text-fg-quaternary" />}
|
||||||
|
|
||||||
|
<p className="text-sm text-tertiary">{progress}%</p>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{failed && (
|
||||||
|
<Button color="link-destructive" size="sm" onClick={onRetry} className="mt-1.5">
|
||||||
|
Try again
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ButtonUtility color="tertiary" tooltip="Delete" icon={Trash01} size="xs" className="-mt-2 -mr-2 self-start" onClick={onDelete} />
|
||||||
|
</div>
|
||||||
|
</motion.li>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const FileUploadRoot = (props: ComponentPropsWithRef<"div">) => (
|
||||||
|
<div {...props} className={cx("flex flex-col gap-4", props.className)}>
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const FileUploadList = (props: ComponentPropsWithRef<"ul">) => (
|
||||||
|
<ul {...props} className={cx("flex flex-col gap-3", props.className)}>
|
||||||
|
<AnimatePresence initial={false}>{props.children}</AnimatePresence>
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const FileUpload = {
|
||||||
|
Root: FileUploadRoot,
|
||||||
|
List: FileUploadList,
|
||||||
|
DropZone: FileUploadDropZone,
|
||||||
|
ListItemProgressBar: FileListItemProgressBar,
|
||||||
|
ListItemProgressFill: FileListItemProgressFill,
|
||||||
|
};
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
import { cx } from "@/utils/cx";
|
||||||
|
|
||||||
|
const styles = {
|
||||||
|
sm: {
|
||||||
|
root: "gap-4",
|
||||||
|
label: "text-sm font-medium",
|
||||||
|
spinner: "size-8",
|
||||||
|
},
|
||||||
|
md: {
|
||||||
|
root: "gap-4",
|
||||||
|
label: "text-sm font-medium",
|
||||||
|
spinner: "size-12",
|
||||||
|
},
|
||||||
|
lg: {
|
||||||
|
root: "gap-4",
|
||||||
|
label: "text-lg font-medium",
|
||||||
|
spinner: "size-14",
|
||||||
|
},
|
||||||
|
xl: {
|
||||||
|
root: "gap-5",
|
||||||
|
label: "text-lg font-medium",
|
||||||
|
spinner: "size-16",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
interface LoadingIndicatorProps {
|
||||||
|
/**
|
||||||
|
* The visual style of the loading indicator.
|
||||||
|
* @default 'line-simple'
|
||||||
|
*/
|
||||||
|
type?: "line-simple" | "line-spinner" | "dot-circle";
|
||||||
|
/**
|
||||||
|
* The size of the loading indicator.
|
||||||
|
* @default 'sm'
|
||||||
|
*/
|
||||||
|
size?: "sm" | "md" | "lg" | "xl";
|
||||||
|
/**
|
||||||
|
* Optional text label displayed below the indicator.
|
||||||
|
*/
|
||||||
|
label?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LoadingIndicator = ({ type = "line-simple", size = "sm", label }: LoadingIndicatorProps) => {
|
||||||
|
const renderSpinner = () => {
|
||||||
|
if (type === "line-spinner") {
|
||||||
|
return (
|
||||||
|
<svg className={cx("animate-spin", styles[size].spinner)} viewBox="0 0 32 32" fill="none">
|
||||||
|
<circle
|
||||||
|
className="stroke-fg-brand-primary"
|
||||||
|
cx="16"
|
||||||
|
cy="16"
|
||||||
|
r="14"
|
||||||
|
fill="none"
|
||||||
|
strokeWidth="4"
|
||||||
|
strokeDashoffset="40"
|
||||||
|
strokeDasharray="100"
|
||||||
|
strokeLinecap="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === "dot-circle") {
|
||||||
|
return (
|
||||||
|
<svg className={cx("animate-spin text-fg-brand-primary", styles[size].spinner)} viewBox="0 0 36 36" fill="none">
|
||||||
|
<path
|
||||||
|
d="M34 18C34 15.8989 33.5861 13.8183 32.7821 11.8771C31.978 9.93586 30.7994 8.17203 29.3137 6.68629C27.828 5.20055 26.0641 4.022 24.1229 3.21793C22.1817 2.41385 20.1011 2 18 2C15.8988 2 13.8183 2.41385 11.8771 3.21793C9.93585 4.022 8.17203 5.20055 6.68629 6.68629C5.20055 8.17203 4.022 9.93586 3.21793 11.8771C2.41385 13.8183 2 15.8989 2 18"
|
||||||
|
stroke="url(#paint0)"
|
||||||
|
strokeWidth="4"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeDasharray="0.1 8"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M3.21793 24.1229C4.022 26.0641 5.20055 27.828 6.68629 29.3137C8.17203 30.7994 9.93585 31.978 11.8771 32.7821C13.8183 33.5861 15.8988 34 18 34C20.1011 34 22.1817 33.5861 24.1229 32.7821C26.0641 31.978 27.828 30.7994 29.3137 29.3137C30.7994 27.828 31.978 26.0641 32.7821 24.1229"
|
||||||
|
stroke="url(#paint1)"
|
||||||
|
strokeWidth="4"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeDasharray="0.1 8"
|
||||||
|
/>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="paint0" x1="34" y1="18" x2="2" y2="18" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stopColor="currentColor" />
|
||||||
|
<stop offset="1" stopColor="currentColor" stopOpacity="0.5" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="paint1" x1="33" y1="23.5" x2="3" y2="24" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stopOpacity="0" stopColor="currentColor" />
|
||||||
|
<stop offset="1" stopColor="currentColor" stopOpacity="0.48" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default case: type === "line-simple"
|
||||||
|
return (
|
||||||
|
<svg className={cx("animate-spin", styles[size].spinner)} viewBox="0 0 32 32" fill="none">
|
||||||
|
<circle className="text-bg-tertiary" cx="16" cy="16" r="14" stroke="currentColor" strokeWidth="4" />
|
||||||
|
<circle
|
||||||
|
className="stroke-fg-brand-primary"
|
||||||
|
cx="16"
|
||||||
|
cy="16"
|
||||||
|
r="14"
|
||||||
|
fill="none"
|
||||||
|
strokeWidth="4"
|
||||||
|
strokeDashoffset="75"
|
||||||
|
strokeDasharray="100"
|
||||||
|
strokeLinecap="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cx("flex flex-col items-center justify-center", styles[size].root)}>
|
||||||
|
{renderSpinner()}
|
||||||
|
{label && <span className={cx("text-secondary", styles[size].label)}>{label}</span>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
39
src/components/application/modals/modal.tsx
Normal file
39
src/components/application/modals/modal.tsx
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import type { DialogProps as AriaDialogProps, ModalOverlayProps as AriaModalOverlayProps } from "react-aria-components";
|
||||||
|
import { Dialog as AriaDialog, DialogTrigger as AriaDialogTrigger, Modal as AriaModal, ModalOverlay as AriaModalOverlay } from "react-aria-components";
|
||||||
|
import { cx } from "@/utils/cx";
|
||||||
|
|
||||||
|
export const DialogTrigger = AriaDialogTrigger;
|
||||||
|
|
||||||
|
export const ModalOverlay = (props: AriaModalOverlayProps) => {
|
||||||
|
return (
|
||||||
|
<AriaModalOverlay
|
||||||
|
{...props}
|
||||||
|
className={(state) =>
|
||||||
|
cx(
|
||||||
|
"fixed inset-0 z-50 flex min-h-dvh w-full items-end justify-center overflow-y-auto bg-overlay/70 px-4 pt-4 pb-[clamp(16px,8vh,64px)] outline-hidden backdrop-blur-[6px] sm:items-center sm:justify-center sm:p-8",
|
||||||
|
state.isEntering && "duration-300 ease-out animate-in fade-in",
|
||||||
|
state.isExiting && "duration-200 ease-in animate-out fade-out",
|
||||||
|
typeof props.className === "function" ? props.className(state) : props.className,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Modal = (props: AriaModalOverlayProps) => (
|
||||||
|
<AriaModal
|
||||||
|
{...props}
|
||||||
|
className={(state) =>
|
||||||
|
cx(
|
||||||
|
"max-h-full w-full align-middle outline-hidden max-sm:overflow-y-auto max-sm:rounded-xl",
|
||||||
|
state.isEntering && "duration-300 ease-out animate-in zoom-in-95",
|
||||||
|
state.isExiting && "duration-200 ease-in animate-out zoom-out-95",
|
||||||
|
typeof props.className === "function" ? props.className(state) : props.className,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const Dialog = (props: AriaDialogProps) => (
|
||||||
|
<AriaDialog {...props} className={cx("flex w-full items-center justify-center outline-hidden", props.className)} />
|
||||||
|
);
|
||||||
376
src/components/application/pagination/pagination-base.tsx
Normal file
376
src/components/application/pagination/pagination-base.tsx
Normal file
@@ -0,0 +1,376 @@
|
|||||||
|
import type { CSSProperties, FC, HTMLAttributes, ReactNode } from "react";
|
||||||
|
import React, { cloneElement, createContext, isValidElement, useCallback, useContext, useEffect, useState } from "react";
|
||||||
|
|
||||||
|
type PaginationPage = {
|
||||||
|
/** The type of the pagination item. */
|
||||||
|
type: "page";
|
||||||
|
/** The value of the pagination item. */
|
||||||
|
value: number;
|
||||||
|
/** Whether the pagination item is the current page. */
|
||||||
|
isCurrent: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type PaginationEllipsisType = {
|
||||||
|
type: "ellipsis";
|
||||||
|
key: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type PaginationItemType = PaginationPage | PaginationEllipsisType;
|
||||||
|
|
||||||
|
interface PaginationContextType {
|
||||||
|
/** The pages of the pagination. */
|
||||||
|
pages: PaginationItemType[];
|
||||||
|
/** The current page of the pagination. */
|
||||||
|
currentPage: number;
|
||||||
|
/** The total number of pages. */
|
||||||
|
total: number;
|
||||||
|
/** The function to call when the page changes. */
|
||||||
|
onPageChange: (page: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PaginationContext = createContext<PaginationContextType | undefined>(undefined);
|
||||||
|
|
||||||
|
export interface PaginationRootProps {
|
||||||
|
/** Number of sibling pages to show on each side of the current page */
|
||||||
|
siblingCount?: number;
|
||||||
|
/** Current active page number */
|
||||||
|
page: number;
|
||||||
|
/** Total number of pages */
|
||||||
|
total: number;
|
||||||
|
children: ReactNode;
|
||||||
|
/** The style of the pagination root. */
|
||||||
|
style?: CSSProperties;
|
||||||
|
/** The class name of the pagination root. */
|
||||||
|
className?: string;
|
||||||
|
/** Callback function that's called when the page changes with the new page number. */
|
||||||
|
onPageChange?: (page: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PaginationRoot = ({ total, siblingCount = 1, page, onPageChange, children, style, className }: PaginationRootProps) => {
|
||||||
|
const [pages, setPages] = useState<PaginationItemType[]>([]);
|
||||||
|
|
||||||
|
const createPaginationItems = useCallback((): PaginationItemType[] => {
|
||||||
|
const items: PaginationItemType[] = [];
|
||||||
|
// Calculate the maximum number of pagination elements (pages, potential ellipsis, first and last) to show
|
||||||
|
const totalPageNumbers = siblingCount * 2 + 5;
|
||||||
|
|
||||||
|
// If the total number of items to show is greater than or equal to the total pages,
|
||||||
|
// we can simply list all pages without needing to collapse with ellipsis
|
||||||
|
if (totalPageNumbers >= total) {
|
||||||
|
for (let i = 1; i <= total; i++) {
|
||||||
|
items.push({
|
||||||
|
type: "page",
|
||||||
|
value: i,
|
||||||
|
isCurrent: i === page,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Calculate left and right sibling boundaries around the current page
|
||||||
|
const leftSiblingIndex = Math.max(page - siblingCount, 1);
|
||||||
|
const rightSiblingIndex = Math.min(page + siblingCount, total);
|
||||||
|
|
||||||
|
// Determine if we need to show ellipsis on either side
|
||||||
|
const showLeftEllipsis = leftSiblingIndex > 2;
|
||||||
|
const showRightEllipsis = rightSiblingIndex < total - 1;
|
||||||
|
|
||||||
|
// Case 1: No left ellipsis, but right ellipsis is needed
|
||||||
|
if (!showLeftEllipsis && showRightEllipsis) {
|
||||||
|
// Calculate how many page numbers to show starting from the beginning
|
||||||
|
const leftItemCount = siblingCount * 2 + 3;
|
||||||
|
const leftRange = range(1, leftItemCount);
|
||||||
|
|
||||||
|
leftRange.forEach((pageNum) =>
|
||||||
|
items.push({
|
||||||
|
type: "page",
|
||||||
|
value: pageNum,
|
||||||
|
isCurrent: pageNum === page,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Insert ellipsis after the left range and add the last page
|
||||||
|
items.push({ type: "ellipsis", key: leftItemCount + 1 });
|
||||||
|
items.push({
|
||||||
|
type: "page",
|
||||||
|
value: total,
|
||||||
|
isCurrent: total === page,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Case 2: Left ellipsis needed, but right ellipsis is not needed
|
||||||
|
else if (showLeftEllipsis && !showRightEllipsis) {
|
||||||
|
// Determine how many items from the end should be shown
|
||||||
|
const rightItemCount = siblingCount * 2 + 3;
|
||||||
|
const rightRange = range(total - rightItemCount + 1, total);
|
||||||
|
|
||||||
|
// Always show the first page, then add an ellipsis to indicate skipped pages
|
||||||
|
items.push({
|
||||||
|
type: "page",
|
||||||
|
value: 1,
|
||||||
|
isCurrent: page === 1,
|
||||||
|
});
|
||||||
|
items.push({ type: "ellipsis", key: total - rightItemCount });
|
||||||
|
rightRange.forEach((pageNum) =>
|
||||||
|
items.push({
|
||||||
|
type: "page",
|
||||||
|
value: pageNum,
|
||||||
|
isCurrent: pageNum === page,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Case 3: Both left and right ellipsis are needed
|
||||||
|
else if (showLeftEllipsis && showRightEllipsis) {
|
||||||
|
// Always show the first page
|
||||||
|
items.push({
|
||||||
|
type: "page",
|
||||||
|
value: 1,
|
||||||
|
isCurrent: page === 1,
|
||||||
|
});
|
||||||
|
// Insert left ellipsis after the first page
|
||||||
|
items.push({ type: "ellipsis", key: leftSiblingIndex - 1 });
|
||||||
|
|
||||||
|
// Show a range of pages around the current page
|
||||||
|
const middleRange = range(leftSiblingIndex, rightSiblingIndex);
|
||||||
|
middleRange.forEach((pageNum) =>
|
||||||
|
items.push({
|
||||||
|
type: "page",
|
||||||
|
value: pageNum,
|
||||||
|
isCurrent: pageNum === page,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Insert right ellipsis and finally the last page
|
||||||
|
items.push({ type: "ellipsis", key: rightSiblingIndex + 1 });
|
||||||
|
items.push({
|
||||||
|
type: "page",
|
||||||
|
value: total,
|
||||||
|
isCurrent: total === page,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}, [total, siblingCount, page]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const paginationItems = createPaginationItems();
|
||||||
|
setPages(paginationItems);
|
||||||
|
}, [createPaginationItems]);
|
||||||
|
|
||||||
|
const onPageChangeHandler = (newPage: number) => {
|
||||||
|
onPageChange?.(newPage);
|
||||||
|
};
|
||||||
|
|
||||||
|
const paginationContextValue: PaginationContextType = {
|
||||||
|
pages,
|
||||||
|
currentPage: page,
|
||||||
|
total,
|
||||||
|
onPageChange: onPageChangeHandler,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PaginationContext.Provider value={paginationContextValue}>
|
||||||
|
<nav aria-label="Pagination Navigation" style={style} className={className}>
|
||||||
|
{children}
|
||||||
|
</nav>
|
||||||
|
</PaginationContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an array of numbers from start to end.
|
||||||
|
* @param start - The start number.
|
||||||
|
* @param end - The end number.
|
||||||
|
* @returns An array of numbers from start to end.
|
||||||
|
*/
|
||||||
|
const range = (start: number, end: number): number[] => {
|
||||||
|
const length = end - start + 1;
|
||||||
|
|
||||||
|
return Array.from({ length }, (_, index) => index + start);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface TriggerRenderProps {
|
||||||
|
isDisabled: boolean;
|
||||||
|
onClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TriggerProps {
|
||||||
|
/** The children of the trigger. Can be a render prop or a valid element. */
|
||||||
|
children: ReactNode | ((props: TriggerRenderProps) => ReactNode);
|
||||||
|
/** The style of the trigger. */
|
||||||
|
style?: CSSProperties;
|
||||||
|
/** The class name of the trigger. */
|
||||||
|
className?: string | ((args: { isDisabled: boolean }) => string);
|
||||||
|
/** If true, the child element will be cloned and passed down the prop of the trigger. */
|
||||||
|
asChild?: boolean;
|
||||||
|
/** The direction of the trigger. */
|
||||||
|
direction: "prev" | "next";
|
||||||
|
/** The aria label of the trigger. */
|
||||||
|
ariaLabel?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Trigger: FC<TriggerProps> = ({ children, style, className, asChild = false, direction, ariaLabel }) => {
|
||||||
|
const context = useContext(PaginationContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error("Pagination components must be used within a Pagination.Root");
|
||||||
|
}
|
||||||
|
|
||||||
|
const { currentPage, total, onPageChange } = context;
|
||||||
|
|
||||||
|
const isDisabled = direction === "prev" ? currentPage <= 1 : currentPage >= total;
|
||||||
|
|
||||||
|
const handleClick = () => {
|
||||||
|
if (isDisabled) return;
|
||||||
|
|
||||||
|
const newPage = direction === "prev" ? currentPage - 1 : currentPage + 1;
|
||||||
|
onPageChange?.(newPage);
|
||||||
|
};
|
||||||
|
|
||||||
|
const computedClassName = typeof className === "function" ? className({ isDisabled }) : className;
|
||||||
|
|
||||||
|
const defaultAriaLabel = direction === "prev" ? "Previous Page" : "Next Page";
|
||||||
|
|
||||||
|
// If the children is a render prop, we need to pass the isDisabled and onClick to the render prop.
|
||||||
|
if (typeof children === "function") {
|
||||||
|
return <>{children({ isDisabled, onClick: handleClick })}</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the children is a valid element, we need to clone it and pass the isDisabled and onClick to the cloned element.
|
||||||
|
if (asChild && isValidElement(children)) {
|
||||||
|
return cloneElement(children, {
|
||||||
|
onClick: handleClick,
|
||||||
|
disabled: isDisabled,
|
||||||
|
isDisabled,
|
||||||
|
"aria-label": ariaLabel || defaultAriaLabel,
|
||||||
|
style: { ...(children.props as HTMLAttributes<HTMLElement>).style, ...style },
|
||||||
|
className: [computedClassName, (children.props as HTMLAttributes<HTMLElement>).className].filter(Boolean).join(" ") || undefined,
|
||||||
|
} as HTMLAttributes<HTMLElement>);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button aria-label={ariaLabel || defaultAriaLabel} onClick={handleClick} disabled={isDisabled} style={style} className={computedClassName}>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const PaginationPrevTrigger: FC<Omit<TriggerProps, "direction">> = (props) => <Trigger {...props} direction="prev" />;
|
||||||
|
|
||||||
|
const PaginationNextTrigger: FC<Omit<TriggerProps, "direction">> = (props) => <Trigger {...props} direction="next" />;
|
||||||
|
|
||||||
|
interface PaginationItemRenderProps {
|
||||||
|
isSelected: boolean;
|
||||||
|
onClick: () => void;
|
||||||
|
value: number;
|
||||||
|
"aria-current"?: "page";
|
||||||
|
"aria-label"?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PaginationItemProps {
|
||||||
|
/** The value of the pagination item. */
|
||||||
|
value: number;
|
||||||
|
/** Whether the pagination item is the current page. */
|
||||||
|
isCurrent: boolean;
|
||||||
|
/** The children of the pagination item. Can be a render prop or a valid element. */
|
||||||
|
children?: ReactNode | ((props: PaginationItemRenderProps) => ReactNode);
|
||||||
|
/** The style object of the pagination item. */
|
||||||
|
style?: CSSProperties;
|
||||||
|
/** The class name of the pagination item. */
|
||||||
|
className?: string | ((args: { isSelected: boolean }) => string);
|
||||||
|
/** The aria label of the pagination item. */
|
||||||
|
ariaLabel?: string;
|
||||||
|
/** If true, the child element will be cloned and passed down the prop of the item. */
|
||||||
|
asChild?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PaginationItem = ({ value, isCurrent, children, style, className, ariaLabel, asChild = false }: PaginationItemProps) => {
|
||||||
|
const context = useContext(PaginationContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error("Pagination components must be used within a <Pagination.Root />");
|
||||||
|
}
|
||||||
|
|
||||||
|
const { onPageChange } = context;
|
||||||
|
|
||||||
|
const isSelected = isCurrent;
|
||||||
|
|
||||||
|
const handleClick = () => {
|
||||||
|
onPageChange?.(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const computedClassName = typeof className === "function" ? className({ isSelected }) : className;
|
||||||
|
|
||||||
|
// If the children is a render prop, we need to pass the necessary props to the render prop.
|
||||||
|
if (typeof children === "function") {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{children({
|
||||||
|
isSelected,
|
||||||
|
onClick: handleClick,
|
||||||
|
value,
|
||||||
|
"aria-current": isCurrent ? "page" : undefined,
|
||||||
|
"aria-label": ariaLabel || `Page ${value}`,
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the children is a valid element, we need to clone it and pass the necessary props to the cloned element.
|
||||||
|
if (asChild && isValidElement(children)) {
|
||||||
|
return cloneElement(children, {
|
||||||
|
onClick: handleClick,
|
||||||
|
"aria-current": isCurrent ? "page" : undefined,
|
||||||
|
"aria-label": ariaLabel || `Page ${value}`,
|
||||||
|
style: { ...(children.props as HTMLAttributes<HTMLElement>).style, ...style },
|
||||||
|
className: [computedClassName, (children.props as HTMLAttributes<HTMLElement>).className].filter(Boolean).join(" ") || undefined,
|
||||||
|
} as HTMLAttributes<HTMLElement>);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={handleClick}
|
||||||
|
style={style}
|
||||||
|
className={computedClassName}
|
||||||
|
aria-current={isCurrent ? "page" : undefined}
|
||||||
|
aria-label={ariaLabel || `Page ${value}`}
|
||||||
|
role="listitem"
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
interface PaginationEllipsisProps {
|
||||||
|
key: number;
|
||||||
|
children?: ReactNode;
|
||||||
|
style?: CSSProperties;
|
||||||
|
className?: string | (() => string);
|
||||||
|
}
|
||||||
|
|
||||||
|
const PaginationEllipsis: FC<PaginationEllipsisProps> = ({ children, style, className }) => {
|
||||||
|
const computedClassName = typeof className === "function" ? className() : className;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span style={style} className={computedClassName} aria-hidden="true">
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface PaginationContextComponentProps {
|
||||||
|
children: (pagination: PaginationContextType) => ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PaginationContextComponent: FC<PaginationContextComponentProps> = ({ children }) => {
|
||||||
|
const context = useContext(PaginationContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error("Pagination components must be used within a Pagination.Root");
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>{children(context)}</>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Pagination = {
|
||||||
|
Root: PaginationRoot,
|
||||||
|
PrevTrigger: PaginationPrevTrigger,
|
||||||
|
NextTrigger: PaginationNextTrigger,
|
||||||
|
Item: PaginationItem,
|
||||||
|
Ellipsis: PaginationEllipsis,
|
||||||
|
Context: PaginationContextComponent,
|
||||||
|
};
|
||||||
52
src/components/application/pagination/pagination-dot.tsx
Normal file
52
src/components/application/pagination/pagination-dot.tsx
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import { cx } from "@/utils/cx";
|
||||||
|
import type { PaginationRootProps } from "./pagination-base";
|
||||||
|
import { Pagination } from "./pagination-base";
|
||||||
|
|
||||||
|
interface PaginationDotProps extends Omit<PaginationRootProps, "children"> {
|
||||||
|
/** The size of the pagination dot. */
|
||||||
|
size?: "md" | "lg";
|
||||||
|
/** Whether the pagination uses brand colors. */
|
||||||
|
isBrand?: boolean;
|
||||||
|
/** Whether the pagination is displayed in a card. */
|
||||||
|
framed?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PaginationDot = ({ framed, className, size = "md", isBrand, ...props }: PaginationDotProps) => {
|
||||||
|
const sizes = {
|
||||||
|
md: {
|
||||||
|
root: cx("gap-3", framed && "p-2"),
|
||||||
|
button: "h-2 w-2 after:-inset-x-1.5 after:-inset-y-2",
|
||||||
|
},
|
||||||
|
lg: {
|
||||||
|
root: cx("gap-4", framed && "p-3"),
|
||||||
|
button: "h-2.5 w-2.5 after:-inset-x-2 after:-inset-y-3",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Pagination.Root {...props} className={cx("flex h-max w-max", sizes[size].root, framed && "rounded-full bg-alpha-white/90 backdrop-blur", className)}>
|
||||||
|
<Pagination.Context>
|
||||||
|
{({ pages }) =>
|
||||||
|
pages.map((page, index) =>
|
||||||
|
page.type === "page" ? (
|
||||||
|
<Pagination.Item
|
||||||
|
{...page}
|
||||||
|
asChild
|
||||||
|
key={index}
|
||||||
|
className={cx(
|
||||||
|
"relative cursor-pointer rounded-full bg-quaternary outline-focus-ring after:absolute focus-visible:outline-2 focus-visible:outline-offset-2",
|
||||||
|
sizes[size].button,
|
||||||
|
page.isCurrent && "bg-fg-brand-primary_alt",
|
||||||
|
isBrand && "bg-fg-brand-secondary",
|
||||||
|
isBrand && page.isCurrent && "bg-fg-white",
|
||||||
|
)}
|
||||||
|
></Pagination.Item>
|
||||||
|
) : (
|
||||||
|
<Pagination.Ellipsis {...page} key={index} />
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</Pagination.Context>
|
||||||
|
</Pagination.Root>
|
||||||
|
);
|
||||||
|
};
|
||||||
48
src/components/application/pagination/pagination-line.tsx
Normal file
48
src/components/application/pagination/pagination-line.tsx
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { cx } from "@/utils/cx";
|
||||||
|
import type { PaginationRootProps } from "./pagination-base";
|
||||||
|
import { Pagination } from "./pagination-base";
|
||||||
|
|
||||||
|
interface PaginationLineProps extends Omit<PaginationRootProps, "children"> {
|
||||||
|
/** The size of the pagination line. */
|
||||||
|
size?: "md" | "lg";
|
||||||
|
/** Whether the pagination is displayed in a card. */
|
||||||
|
framed?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PaginationLine = ({ framed, className, size = "md", ...props }: PaginationLineProps) => {
|
||||||
|
const sizes = {
|
||||||
|
md: {
|
||||||
|
root: cx("gap-2", framed && "p-2"),
|
||||||
|
button: "h-1.5 w-full after:-inset-x-1.5 after:-inset-y-2",
|
||||||
|
},
|
||||||
|
lg: {
|
||||||
|
root: cx("gap-3", framed && "p-3"),
|
||||||
|
button: "h-2 w-full after:-inset-x-2 after:-inset-y-3",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Pagination.Root {...props} className={cx("flex h-max w-max", sizes[size].root, framed && "rounded-full bg-alpha-white/90 backdrop-blur", className)}>
|
||||||
|
<Pagination.Context>
|
||||||
|
{({ pages }) =>
|
||||||
|
pages.map((page, index) =>
|
||||||
|
page.type === "page" ? (
|
||||||
|
<Pagination.Item
|
||||||
|
{...page}
|
||||||
|
asChild
|
||||||
|
key={index}
|
||||||
|
className={cx(
|
||||||
|
"relative cursor-pointer rounded-full bg-quaternary outline-focus-ring after:absolute focus-visible:outline-2 focus-visible:outline-offset-2",
|
||||||
|
sizes[size].button,
|
||||||
|
page.isCurrent && "bg-fg-brand-primary_alt",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Pagination.Ellipsis {...page} key={index} />
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</Pagination.Context>
|
||||||
|
</Pagination.Root>
|
||||||
|
);
|
||||||
|
};
|
||||||
328
src/components/application/pagination/pagination.tsx
Normal file
328
src/components/application/pagination/pagination.tsx
Normal file
@@ -0,0 +1,328 @@
|
|||||||
|
import { ArrowLeft, ArrowRight } from "@untitledui/icons";
|
||||||
|
import { ButtonGroup, ButtonGroupItem } from "@/components/base/button-group/button-group";
|
||||||
|
import { Button } from "@/components/base/buttons/button";
|
||||||
|
import { useBreakpoint } from "@/hooks/use-breakpoint";
|
||||||
|
import { cx } from "@/utils/cx";
|
||||||
|
import type { PaginationRootProps } from "./pagination-base";
|
||||||
|
import { Pagination } from "./pagination-base";
|
||||||
|
|
||||||
|
interface PaginationProps extends Partial<Omit<PaginationRootProps, "children">> {
|
||||||
|
/** Whether the pagination buttons are rounded. */
|
||||||
|
rounded?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PaginationItem = ({ value, rounded, isCurrent }: { value: number; rounded?: boolean; isCurrent: boolean }) => {
|
||||||
|
return (
|
||||||
|
<Pagination.Item
|
||||||
|
value={value}
|
||||||
|
isCurrent={isCurrent}
|
||||||
|
className={({ isSelected }) =>
|
||||||
|
cx(
|
||||||
|
"flex size-10 cursor-pointer items-center justify-center p-3 text-sm font-medium text-quaternary outline-focus-ring transition duration-100 ease-linear hover:bg-primary_hover hover:text-secondary focus-visible:z-10 focus-visible:bg-primary_hover focus-visible:outline-2 focus-visible:outline-offset-2",
|
||||||
|
rounded ? "rounded-full" : "rounded-lg",
|
||||||
|
isSelected && "bg-primary_hover text-secondary",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</Pagination.Item>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface MobilePaginationProps {
|
||||||
|
/** The current page. */
|
||||||
|
page?: number;
|
||||||
|
/** The total number of pages. */
|
||||||
|
total?: number;
|
||||||
|
/** The class name of the pagination component. */
|
||||||
|
className?: string;
|
||||||
|
/** The function to call when the page changes. */
|
||||||
|
onPageChange?: (page: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MobilePagination = ({ page = 1, total = 10, className, onPageChange }: MobilePaginationProps) => {
|
||||||
|
return (
|
||||||
|
<nav aria-label="Pagination" className={cx("flex items-center justify-between md:hidden", className)}>
|
||||||
|
<Button
|
||||||
|
aria-label="Go to previous page"
|
||||||
|
iconLeading={ArrowLeft}
|
||||||
|
color="secondary"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => onPageChange?.(Math.max(0, page - 1))}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<span className="text-sm text-fg-secondary">
|
||||||
|
Page <span className="font-medium">{page}</span> of <span className="font-medium">{total}</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
aria-label="Go to next page"
|
||||||
|
iconLeading={ArrowRight}
|
||||||
|
color="secondary"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => onPageChange?.(Math.min(total, page + 1))}
|
||||||
|
/>
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PaginationPageDefault = ({ rounded, page = 1, total = 10, className, ...props }: PaginationProps) => {
|
||||||
|
const isDesktop = useBreakpoint("md");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Pagination.Root
|
||||||
|
{...props}
|
||||||
|
page={page}
|
||||||
|
total={total}
|
||||||
|
className={cx("flex w-full items-center justify-between gap-3 border-t border-secondary pt-4 md:pt-5", className)}
|
||||||
|
>
|
||||||
|
<div className="hidden flex-1 justify-start md:flex">
|
||||||
|
<Pagination.PrevTrigger asChild>
|
||||||
|
<Button iconLeading={ArrowLeft} color="link-gray" size="sm">
|
||||||
|
{isDesktop ? "Previous" : undefined}{" "}
|
||||||
|
</Button>
|
||||||
|
</Pagination.PrevTrigger>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Pagination.PrevTrigger asChild className="md:hidden">
|
||||||
|
<Button iconLeading={ArrowLeft} color="secondary" size="sm">
|
||||||
|
{isDesktop ? "Previous" : undefined}
|
||||||
|
</Button>
|
||||||
|
</Pagination.PrevTrigger>
|
||||||
|
|
||||||
|
<Pagination.Context>
|
||||||
|
{({ pages, currentPage, total }) => (
|
||||||
|
<>
|
||||||
|
<div className="hidden justify-center gap-0.5 md:flex">
|
||||||
|
{pages.map((page, index) =>
|
||||||
|
page.type === "page" ? (
|
||||||
|
<PaginationItem key={index} rounded={rounded} {...page} />
|
||||||
|
) : (
|
||||||
|
<Pagination.Ellipsis key={index} className="flex size-10 shrink-0 items-center justify-center text-tertiary">
|
||||||
|
…
|
||||||
|
</Pagination.Ellipsis>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-center text-sm whitespace-pre text-fg-secondary md:hidden">
|
||||||
|
Page <span className="font-medium">{currentPage}</span> of <span className="font-medium">{total}</span>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Pagination.Context>
|
||||||
|
|
||||||
|
<div className="hidden flex-1 justify-end md:flex">
|
||||||
|
<Pagination.NextTrigger asChild>
|
||||||
|
<Button iconTrailing={ArrowRight} color="link-gray" size="sm">
|
||||||
|
{isDesktop ? "Next" : undefined}
|
||||||
|
</Button>
|
||||||
|
</Pagination.NextTrigger>
|
||||||
|
</div>
|
||||||
|
<Pagination.NextTrigger asChild className="md:hidden">
|
||||||
|
<Button iconTrailing={ArrowRight} color="secondary" size="sm">
|
||||||
|
{isDesktop ? "Next" : undefined}
|
||||||
|
</Button>
|
||||||
|
</Pagination.NextTrigger>
|
||||||
|
</Pagination.Root>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PaginationPageMinimalCenter = ({ rounded, page = 1, total = 10, className, ...props }: PaginationProps) => {
|
||||||
|
const isDesktop = useBreakpoint("md");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Pagination.Root
|
||||||
|
{...props}
|
||||||
|
page={page}
|
||||||
|
total={total}
|
||||||
|
className={cx("flex w-full items-center justify-between gap-3 border-t border-secondary pt-4 md:pt-5", className)}
|
||||||
|
>
|
||||||
|
<div className="flex flex-1 justify-start">
|
||||||
|
<Pagination.PrevTrigger asChild>
|
||||||
|
<Button iconLeading={ArrowLeft} color="secondary" size="sm">
|
||||||
|
{isDesktop ? "Previous" : undefined}
|
||||||
|
</Button>
|
||||||
|
</Pagination.PrevTrigger>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Pagination.Context>
|
||||||
|
{({ pages, currentPage, total }) => (
|
||||||
|
<>
|
||||||
|
<div className="hidden justify-center gap-0.5 md:flex">
|
||||||
|
{pages.map((page, index) =>
|
||||||
|
page.type === "page" ? (
|
||||||
|
<PaginationItem key={index} rounded={rounded} {...page} />
|
||||||
|
) : (
|
||||||
|
<Pagination.Ellipsis key={index} className="flex size-10 shrink-0 items-center justify-center text-tertiary">
|
||||||
|
…
|
||||||
|
</Pagination.Ellipsis>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-center text-sm whitespace-pre text-fg-secondary md:hidden">
|
||||||
|
Page <span className="font-medium">{currentPage}</span> of <span className="font-medium">{total}</span>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Pagination.Context>
|
||||||
|
|
||||||
|
<div className="flex flex-1 justify-end">
|
||||||
|
<Pagination.NextTrigger asChild>
|
||||||
|
<Button iconTrailing={ArrowRight} color="secondary" size="sm">
|
||||||
|
{isDesktop ? "Next" : undefined}
|
||||||
|
</Button>
|
||||||
|
</Pagination.NextTrigger>
|
||||||
|
</div>
|
||||||
|
</Pagination.Root>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PaginationCardDefault = ({ rounded, page = 1, total = 10, ...props }: PaginationProps) => {
|
||||||
|
const isDesktop = useBreakpoint("md");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Pagination.Root
|
||||||
|
{...props}
|
||||||
|
page={page}
|
||||||
|
total={total}
|
||||||
|
className="flex w-full items-center justify-between gap-3 border-t border-secondary px-4 py-3 md:px-6 md:pt-3 md:pb-4"
|
||||||
|
>
|
||||||
|
<div className="flex flex-1 justify-start">
|
||||||
|
<Pagination.PrevTrigger asChild>
|
||||||
|
<Button iconLeading={ArrowLeft} color="secondary" size="sm">
|
||||||
|
{isDesktop ? "Previous" : undefined}
|
||||||
|
</Button>
|
||||||
|
</Pagination.PrevTrigger>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Pagination.Context>
|
||||||
|
{({ pages, currentPage, total }) => (
|
||||||
|
<>
|
||||||
|
<div className="hidden justify-center gap-0.5 md:flex">
|
||||||
|
{pages.map((page, index) =>
|
||||||
|
page.type === "page" ? (
|
||||||
|
<PaginationItem key={index} rounded={rounded} {...page} />
|
||||||
|
) : (
|
||||||
|
<Pagination.Ellipsis key={index} className="flex size-10 shrink-0 items-center justify-center text-tertiary">
|
||||||
|
…
|
||||||
|
</Pagination.Ellipsis>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-center text-sm whitespace-pre text-fg-secondary md:hidden">
|
||||||
|
Page <span className="font-medium">{currentPage}</span> of <span className="font-medium">{total}</span>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Pagination.Context>
|
||||||
|
|
||||||
|
<div className="flex flex-1 justify-end">
|
||||||
|
<Pagination.NextTrigger asChild>
|
||||||
|
<Button iconTrailing={ArrowRight} color="secondary" size="sm">
|
||||||
|
{isDesktop ? "Next" : undefined}
|
||||||
|
</Button>
|
||||||
|
</Pagination.NextTrigger>
|
||||||
|
</div>
|
||||||
|
</Pagination.Root>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface PaginationCardMinimalProps {
|
||||||
|
/** The current page. */
|
||||||
|
page?: number;
|
||||||
|
/** The total number of pages. */
|
||||||
|
total?: number;
|
||||||
|
/** The alignment of the pagination. */
|
||||||
|
align?: "left" | "center" | "right";
|
||||||
|
/** The class name of the pagination component. */
|
||||||
|
className?: string;
|
||||||
|
/** The function to call when the page changes. */
|
||||||
|
onPageChange?: (page: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PaginationCardMinimal = ({ page = 1, total = 10, align = "left", onPageChange, className }: PaginationCardMinimalProps) => {
|
||||||
|
return (
|
||||||
|
<div className={cx("border-t border-secondary px-4 py-3 md:px-6 md:pt-3 md:pb-4", className)}>
|
||||||
|
<MobilePagination page={page} total={total} onPageChange={onPageChange} />
|
||||||
|
|
||||||
|
<nav aria-label="Pagination" className={cx("hidden items-center gap-3 md:flex", align === "center" && "justify-between")}>
|
||||||
|
<div className={cx(align === "center" && "flex flex-1 justify-start")}>
|
||||||
|
<Button isDisabled={page === 1} color="secondary" size="sm" onClick={() => onPageChange?.(Math.max(0, page - 1))}>
|
||||||
|
Previous
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span
|
||||||
|
className={cx(
|
||||||
|
"text-sm font-medium text-fg-secondary",
|
||||||
|
align === "right" && "order-first mr-auto",
|
||||||
|
align === "left" && "order-last ml-auto",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Page {page} of {total}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div className={cx(align === "center" && "flex flex-1 justify-end")}>
|
||||||
|
<Button isDisabled={page === total} color="secondary" size="sm" onClick={() => onPageChange?.(Math.min(total, page + 1))}>
|
||||||
|
Next
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface PaginationButtonGroupProps extends Partial<Omit<PaginationRootProps, "children">> {
|
||||||
|
/** The alignment of the pagination. */
|
||||||
|
align?: "left" | "center" | "right";
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PaginationButtonGroup = ({ align = "left", page = 1, total = 10, ...props }: PaginationButtonGroupProps) => {
|
||||||
|
const isDesktop = useBreakpoint("md");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cx(
|
||||||
|
"flex border-t border-secondary px-4 py-3 md:px-6 md:pt-3 md:pb-4",
|
||||||
|
align === "left" && "justify-start",
|
||||||
|
align === "center" && "justify-center",
|
||||||
|
align === "right" && "justify-end",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Pagination.Root {...props} page={page} total={total}>
|
||||||
|
<Pagination.Context>
|
||||||
|
{({ pages }) => (
|
||||||
|
<ButtonGroup size="md">
|
||||||
|
<Pagination.PrevTrigger asChild>
|
||||||
|
<ButtonGroupItem iconLeading={ArrowLeft}>{isDesktop ? "Previous" : undefined}</ButtonGroupItem>
|
||||||
|
</Pagination.PrevTrigger>
|
||||||
|
|
||||||
|
{pages.map((page, index) =>
|
||||||
|
page.type === "page" ? (
|
||||||
|
<Pagination.Item key={index} {...page} asChild>
|
||||||
|
<ButtonGroupItem isSelected={page.isCurrent} className="size-10 items-center justify-center">
|
||||||
|
{page.value}
|
||||||
|
</ButtonGroupItem>
|
||||||
|
</Pagination.Item>
|
||||||
|
) : (
|
||||||
|
<Pagination.Ellipsis key={index}>
|
||||||
|
<ButtonGroupItem className="pointer-events-none size-10 items-center justify-center rounded-none!">
|
||||||
|
…
|
||||||
|
</ButtonGroupItem>
|
||||||
|
</Pagination.Ellipsis>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Pagination.NextTrigger asChild>
|
||||||
|
<ButtonGroupItem iconTrailing={ArrowRight}>{isDesktop ? "Next" : undefined}</ButtonGroupItem>
|
||||||
|
</Pagination.NextTrigger>
|
||||||
|
</ButtonGroup>
|
||||||
|
)}
|
||||||
|
</Pagination.Context>
|
||||||
|
</Pagination.Root>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
120
src/components/application/slideout-menus/slideout-menu.tsx
Normal file
120
src/components/application/slideout-menus/slideout-menu.tsx
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
import { type ComponentPropsWithRef, type ReactNode, type RefAttributes } from "react";
|
||||||
|
import type {
|
||||||
|
DialogProps as AriaDialogProps,
|
||||||
|
ModalOverlayProps as AriaModalOverlayProps,
|
||||||
|
ModalRenderProps as AriaModalRenderProps,
|
||||||
|
} from "react-aria-components";
|
||||||
|
import { Dialog as AriaDialog, DialogTrigger as AriaDialogTrigger, Modal as AriaModal, ModalOverlay as AriaModalOverlay } from "react-aria-components";
|
||||||
|
import { CloseButton } from "@/components/base/buttons/close-button";
|
||||||
|
import { cx } from "@/utils/cx";
|
||||||
|
|
||||||
|
interface ModalOverlayProps extends AriaModalOverlayProps, RefAttributes<HTMLDivElement> {}
|
||||||
|
|
||||||
|
export const ModalOverlay = (props: ModalOverlayProps) => {
|
||||||
|
return (
|
||||||
|
<AriaModalOverlay
|
||||||
|
{...props}
|
||||||
|
className={(state) =>
|
||||||
|
cx(
|
||||||
|
"fixed inset-0 flex min-h-dvh w-full items-center justify-end bg-overlay/70 pl-6 outline-hidden ease-linear md:pl-10",
|
||||||
|
state.isEntering && "duration-300 animate-in fade-in",
|
||||||
|
state.isExiting && "duration-500 animate-out fade-out",
|
||||||
|
typeof props.className === "function" ? props.className(state) : props.className,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
ModalOverlay.displayName = "ModalOverlay";
|
||||||
|
|
||||||
|
interface ModalProps extends AriaModalOverlayProps, RefAttributes<HTMLDivElement> {}
|
||||||
|
|
||||||
|
export const Modal = (props: ModalProps) => (
|
||||||
|
<AriaModal
|
||||||
|
{...props}
|
||||||
|
className={(state) =>
|
||||||
|
cx(
|
||||||
|
"inset-y-0 right-0 h-full w-full max-w-100 shadow-xl transition",
|
||||||
|
state.isEntering && "duration-300 animate-in slide-in-from-right",
|
||||||
|
state.isExiting && "duration-500 animate-out slide-out-to-right",
|
||||||
|
typeof props.className === "function" ? props.className(state) : props.className,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
Modal.displayName = "Modal";
|
||||||
|
|
||||||
|
interface DialogProps extends AriaDialogProps, RefAttributes<HTMLElement> {}
|
||||||
|
|
||||||
|
export const Dialog = (props: DialogProps) => (
|
||||||
|
<AriaDialog
|
||||||
|
role="dialog"
|
||||||
|
{...props}
|
||||||
|
className={cx(
|
||||||
|
"relative flex size-full flex-col items-start gap-6 overflow-y-auto bg-primary ring-1 ring-secondary_alt outline-hidden",
|
||||||
|
props.className,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
Dialog.displayName = "Dialog";
|
||||||
|
|
||||||
|
interface SlideoutMenuProps extends Omit<AriaModalOverlayProps, "children">, RefAttributes<HTMLDivElement> {
|
||||||
|
children: ReactNode | ((children: AriaModalRenderProps & { close: () => void }) => ReactNode);
|
||||||
|
dialogClassName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Menu = ({ children, dialogClassName, ...props }: SlideoutMenuProps) => {
|
||||||
|
return (
|
||||||
|
<ModalOverlay {...props}>
|
||||||
|
<Modal className={(state) => cx(typeof props.className === "function" ? props.className(state) : props.className)}>
|
||||||
|
{(state) => (
|
||||||
|
<Dialog className={dialogClassName}>
|
||||||
|
{({ close }) => {
|
||||||
|
return typeof children === "function" ? children({ ...state, close }) : children;
|
||||||
|
}}
|
||||||
|
</Dialog>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
</ModalOverlay>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
Menu.displayName = "SlideoutMenu";
|
||||||
|
|
||||||
|
const Content = ({ role = "main", ...props }: ComponentPropsWithRef<"div">) => {
|
||||||
|
return <div role={role} {...props} className={cx("flex size-full flex-col gap-6 overflow-y-auto overscroll-auto px-4 md:px-6", props.className)} />;
|
||||||
|
};
|
||||||
|
Content.displayName = "SlideoutContent";
|
||||||
|
|
||||||
|
interface SlideoutHeaderProps extends ComponentPropsWithRef<"header"> {
|
||||||
|
onClose?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Header = ({ className, children, onClose, ...props }: SlideoutHeaderProps) => {
|
||||||
|
return (
|
||||||
|
<header {...props} className={cx("relative z-1 w-full px-4 pt-6 md:px-6", className)}>
|
||||||
|
{children}
|
||||||
|
<CloseButton size="md" className="absolute top-3 right-3 shrink-0" onClick={onClose} />
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
Header.displayName = "SlideoutHeader";
|
||||||
|
|
||||||
|
const Footer = (props: ComponentPropsWithRef<"footer">) => {
|
||||||
|
return <footer {...props} className={cx("w-full p-4 shadow-[inset_0px_1px_0px_0px] shadow-border-secondary md:px-6", props.className)} />;
|
||||||
|
};
|
||||||
|
Footer.displayName = "SlideoutFooter";
|
||||||
|
|
||||||
|
const SlideoutMenu = Menu as typeof Menu & {
|
||||||
|
Trigger: typeof AriaDialogTrigger;
|
||||||
|
Content: typeof Content;
|
||||||
|
Header: typeof Header;
|
||||||
|
Footer: typeof Footer;
|
||||||
|
};
|
||||||
|
SlideoutMenu.displayName = "SlideoutMenu";
|
||||||
|
|
||||||
|
SlideoutMenu.Trigger = AriaDialogTrigger;
|
||||||
|
SlideoutMenu.Content = Content;
|
||||||
|
SlideoutMenu.Header = Header;
|
||||||
|
SlideoutMenu.Footer = Footer;
|
||||||
|
|
||||||
|
export { SlideoutMenu };
|
||||||
298
src/components/application/table/table.tsx
Normal file
298
src/components/application/table/table.tsx
Normal file
@@ -0,0 +1,298 @@
|
|||||||
|
import type { ComponentPropsWithRef, HTMLAttributes, ReactNode, Ref, TdHTMLAttributes, ThHTMLAttributes } from "react";
|
||||||
|
import { createContext, isValidElement, useContext } from "react";
|
||||||
|
import { ArrowDown, ChevronSelectorVertical, Copy01, Edit01, HelpCircle, Trash01 } from "@untitledui/icons";
|
||||||
|
import type {
|
||||||
|
CellProps as AriaCellProps,
|
||||||
|
ColumnProps as AriaColumnProps,
|
||||||
|
RowProps as AriaRowProps,
|
||||||
|
TableHeaderProps as AriaTableHeaderProps,
|
||||||
|
TableProps as AriaTableProps,
|
||||||
|
} from "react-aria-components";
|
||||||
|
import {
|
||||||
|
Cell as AriaCell,
|
||||||
|
Collection as AriaCollection,
|
||||||
|
Column as AriaColumn,
|
||||||
|
Group as AriaGroup,
|
||||||
|
Row as AriaRow,
|
||||||
|
Table as AriaTable,
|
||||||
|
TableBody as AriaTableBody,
|
||||||
|
TableHeader as AriaTableHeader,
|
||||||
|
useTableOptions,
|
||||||
|
} from "react-aria-components";
|
||||||
|
import { Badge } from "@/components/base/badges/badges";
|
||||||
|
import { Checkbox } from "@/components/base/checkbox/checkbox";
|
||||||
|
import { Dropdown } from "@/components/base/dropdown/dropdown";
|
||||||
|
import { Tooltip, TooltipTrigger } from "@/components/base/tooltip/tooltip";
|
||||||
|
import { cx } from "@/utils/cx";
|
||||||
|
|
||||||
|
export const TableRowActionsDropdown = () => (
|
||||||
|
<Dropdown.Root>
|
||||||
|
<Dropdown.DotsButton />
|
||||||
|
|
||||||
|
<Dropdown.Popover className="w-min">
|
||||||
|
<Dropdown.Menu>
|
||||||
|
<Dropdown.Item icon={Edit01}>
|
||||||
|
<span className="pr-4">Edit</span>
|
||||||
|
</Dropdown.Item>
|
||||||
|
<Dropdown.Item icon={Copy01}>
|
||||||
|
<span className="pr-4">Copy link</span>
|
||||||
|
</Dropdown.Item>
|
||||||
|
<Dropdown.Item icon={Trash01}>
|
||||||
|
<span className="pr-4">Delete</span>
|
||||||
|
</Dropdown.Item>
|
||||||
|
</Dropdown.Menu>
|
||||||
|
</Dropdown.Popover>
|
||||||
|
</Dropdown.Root>
|
||||||
|
);
|
||||||
|
|
||||||
|
const TableContext = createContext<{ size: "sm" | "md" }>({ size: "md" });
|
||||||
|
|
||||||
|
const TableCardRoot = ({ children, className, size = "md", ...props }: HTMLAttributes<HTMLDivElement> & { size?: "sm" | "md" }) => {
|
||||||
|
return (
|
||||||
|
<TableContext.Provider value={{ size }}>
|
||||||
|
<div {...props} className={cx("overflow-hidden rounded-xl bg-primary shadow-xs ring-1 ring-secondary", className)}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</TableContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface TableCardHeaderProps {
|
||||||
|
/** The title of the table card header. */
|
||||||
|
title: string;
|
||||||
|
/** The badge displayed next to the title. */
|
||||||
|
badge?: ReactNode;
|
||||||
|
/** The description of the table card header. */
|
||||||
|
description?: string;
|
||||||
|
/** The content displayed after the title and badge. */
|
||||||
|
contentTrailing?: ReactNode;
|
||||||
|
/** The class name of the table card header. */
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TableCardHeader = ({ title, badge, description, contentTrailing, className }: TableCardHeaderProps) => {
|
||||||
|
const { size } = useContext(TableContext);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cx(
|
||||||
|
"relative flex flex-col items-start gap-4 border-b border-secondary bg-primary px-4 md:flex-row",
|
||||||
|
size === "sm" ? "py-4 md:px-5" : "py-5 md:px-6",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex flex-1 flex-col gap-0.5">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<h2 className={cx("font-semibold text-primary", size === "sm" ? "text-md" : "text-lg")}>{title}</h2>
|
||||||
|
{badge ? (
|
||||||
|
isValidElement(badge) ? (
|
||||||
|
badge
|
||||||
|
) : (
|
||||||
|
<Badge color="brand" size="sm">
|
||||||
|
{badge}
|
||||||
|
</Badge>
|
||||||
|
)
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
{description && <p className="text-sm text-tertiary">{description}</p>}
|
||||||
|
</div>
|
||||||
|
{contentTrailing}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface TableRootProps extends AriaTableProps, Omit<ComponentPropsWithRef<"table">, "className" | "slot" | "style"> {
|
||||||
|
size?: "sm" | "md";
|
||||||
|
}
|
||||||
|
|
||||||
|
const TableRoot = ({ className, size = "md", ...props }: TableRootProps) => {
|
||||||
|
const context = useContext(TableContext);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableContext.Provider value={{ size: context?.size ?? size }}>
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<AriaTable className={(state) => cx("w-full overflow-x-hidden", typeof className === "function" ? className(state) : className)} {...props} />
|
||||||
|
</div>
|
||||||
|
</TableContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
TableRoot.displayName = "Table";
|
||||||
|
|
||||||
|
interface TableHeaderProps<T extends object>
|
||||||
|
extends AriaTableHeaderProps<T>,
|
||||||
|
Omit<ComponentPropsWithRef<"thead">, "children" | "className" | "slot" | "style"> {
|
||||||
|
bordered?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TableHeader = <T extends object>({ columns, children, bordered = true, className, ...props }: TableHeaderProps<T>) => {
|
||||||
|
const { size } = useContext(TableContext);
|
||||||
|
const { selectionBehavior, selectionMode } = useTableOptions();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AriaTableHeader
|
||||||
|
{...props}
|
||||||
|
className={(state) =>
|
||||||
|
cx(
|
||||||
|
"relative bg-secondary",
|
||||||
|
size === "sm" ? "h-9" : "h-11",
|
||||||
|
|
||||||
|
// Row border—using an "after" pseudo-element to avoid the border taking up space.
|
||||||
|
bordered &&
|
||||||
|
"[&>tr>th]:after:pointer-events-none [&>tr>th]:after:absolute [&>tr>th]:after:inset-x-0 [&>tr>th]:after:bottom-0 [&>tr>th]:after:h-px [&>tr>th]:after:bg-border-secondary [&>tr>th]:focus-visible:after:bg-transparent",
|
||||||
|
|
||||||
|
typeof className === "function" ? className(state) : className,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{selectionBehavior === "toggle" && (
|
||||||
|
<AriaColumn className={cx("relative py-2 pr-0 pl-4", size === "sm" ? "w-9 md:pl-5" : "w-11 md:pl-6")}>
|
||||||
|
{selectionMode === "multiple" && (
|
||||||
|
<div className="flex items-start">
|
||||||
|
<Checkbox slot="selection" size={size} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</AriaColumn>
|
||||||
|
)}
|
||||||
|
<AriaCollection items={columns}>{children}</AriaCollection>
|
||||||
|
</AriaTableHeader>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
TableHeader.displayName = "TableHeader";
|
||||||
|
|
||||||
|
interface TableHeadProps extends AriaColumnProps, Omit<ThHTMLAttributes<HTMLTableCellElement>, "children" | "className" | "style" | "id"> {
|
||||||
|
label?: string;
|
||||||
|
tooltip?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TableHead = ({ className, tooltip, label, children, ...props }: TableHeadProps) => {
|
||||||
|
const { selectionBehavior } = useTableOptions();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AriaColumn
|
||||||
|
{...props}
|
||||||
|
className={(state) =>
|
||||||
|
cx(
|
||||||
|
"relative p-0 px-6 py-2 outline-hidden focus-visible:z-1 focus-visible:ring-2 focus-visible:ring-focus-ring focus-visible:ring-offset-bg-primary focus-visible:ring-inset",
|
||||||
|
selectionBehavior === "toggle" && "nth-2:pl-3",
|
||||||
|
state.allowsSorting && "cursor-pointer",
|
||||||
|
typeof className === "function" ? className(state) : className,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{(state) => (
|
||||||
|
<AriaGroup className="flex items-center gap-1">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
{label && <span className="text-xs font-semibold whitespace-nowrap text-quaternary">{label}</span>}
|
||||||
|
{typeof children === "function" ? children(state) : children}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{tooltip && (
|
||||||
|
<Tooltip title={tooltip} placement="top">
|
||||||
|
<TooltipTrigger className="cursor-pointer text-fg-quaternary transition duration-100 ease-linear hover:text-fg-quaternary_hover focus:text-fg-quaternary_hover">
|
||||||
|
<HelpCircle className="size-4" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{state.allowsSorting &&
|
||||||
|
(state.sortDirection ? (
|
||||||
|
<ArrowDown className={cx("size-3 stroke-[3px] text-fg-quaternary", state.sortDirection === "ascending" && "rotate-180")} />
|
||||||
|
) : (
|
||||||
|
<ChevronSelectorVertical size={12} strokeWidth={3} className="text-fg-quaternary" />
|
||||||
|
))}
|
||||||
|
</AriaGroup>
|
||||||
|
)}
|
||||||
|
</AriaColumn>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
TableHead.displayName = "TableHead";
|
||||||
|
|
||||||
|
interface TableRowProps<T extends object>
|
||||||
|
extends AriaRowProps<T>,
|
||||||
|
Omit<ComponentPropsWithRef<"tr">, "children" | "className" | "onClick" | "slot" | "style" | "id"> {
|
||||||
|
highlightSelectedRow?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TableRow = <T extends object>({ columns, children, className, highlightSelectedRow = true, ...props }: TableRowProps<T>) => {
|
||||||
|
const { size } = useContext(TableContext);
|
||||||
|
const { selectionBehavior } = useTableOptions();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AriaRow
|
||||||
|
{...props}
|
||||||
|
className={(state) =>
|
||||||
|
cx(
|
||||||
|
"relative outline-focus-ring transition-colors after:pointer-events-none hover:bg-secondary focus-visible:outline-2 focus-visible:-outline-offset-2",
|
||||||
|
size === "sm" ? "h-14" : "h-18",
|
||||||
|
highlightSelectedRow && "selected:bg-secondary",
|
||||||
|
|
||||||
|
// Row border—using an "after" pseudo-element to avoid the border taking up space.
|
||||||
|
"[&>td]:after:absolute [&>td]:after:inset-x-0 [&>td]:after:bottom-0 [&>td]:after:h-px [&>td]:after:w-full [&>td]:after:bg-border-secondary last:[&>td]:after:hidden [&>td]:focus-visible:after:opacity-0 focus-visible:[&>td]:after:opacity-0",
|
||||||
|
|
||||||
|
typeof className === "function" ? className(state) : className,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{selectionBehavior === "toggle" && (
|
||||||
|
<AriaCell className={cx("relative py-2 pr-0 pl-4", size === "sm" ? "md:pl-5" : "md:pl-6")}>
|
||||||
|
<div className="flex items-end">
|
||||||
|
<Checkbox slot="selection" size={size} />
|
||||||
|
</div>
|
||||||
|
</AriaCell>
|
||||||
|
)}
|
||||||
|
<AriaCollection items={columns}>{children}</AriaCollection>
|
||||||
|
</AriaRow>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
TableRow.displayName = "TableRow";
|
||||||
|
|
||||||
|
interface TableCellProps extends AriaCellProps, Omit<TdHTMLAttributes<HTMLTableCellElement>, "children" | "className" | "style" | "id"> {
|
||||||
|
ref?: Ref<HTMLTableCellElement>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TableCell = ({ className, children, ...props }: TableCellProps) => {
|
||||||
|
const { size } = useContext(TableContext);
|
||||||
|
const { selectionBehavior } = useTableOptions();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AriaCell
|
||||||
|
{...props}
|
||||||
|
className={(state) =>
|
||||||
|
cx(
|
||||||
|
"relative text-sm text-tertiary outline-focus-ring focus-visible:z-1 focus-visible:outline-2 focus-visible:-outline-offset-2",
|
||||||
|
size === "sm" && "px-5 py-3",
|
||||||
|
size === "md" && "px-6 py-4",
|
||||||
|
|
||||||
|
selectionBehavior === "toggle" && "nth-2:pl-3",
|
||||||
|
|
||||||
|
typeof className === "function" ? className(state) : className,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</AriaCell>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
TableCell.displayName = "TableCell";
|
||||||
|
|
||||||
|
const TableCard = {
|
||||||
|
Root: TableCardRoot,
|
||||||
|
Header: TableCardHeader,
|
||||||
|
};
|
||||||
|
|
||||||
|
const Table = TableRoot as typeof TableRoot & {
|
||||||
|
Body: typeof AriaTableBody;
|
||||||
|
Cell: typeof TableCell;
|
||||||
|
Head: typeof TableHead;
|
||||||
|
Header: typeof TableHeader;
|
||||||
|
Row: typeof TableRow;
|
||||||
|
};
|
||||||
|
Table.Body = AriaTableBody;
|
||||||
|
Table.Cell = TableCell;
|
||||||
|
Table.Head = TableHead;
|
||||||
|
Table.Header = TableHeader;
|
||||||
|
Table.Row = TableRow;
|
||||||
|
|
||||||
|
export { Table, TableCard };
|
||||||
223
src/components/application/tabs/tabs.tsx
Normal file
223
src/components/application/tabs/tabs.tsx
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
import type { ComponentPropsWithRef, ReactNode } from "react";
|
||||||
|
import { Fragment, createContext, useContext } from "react";
|
||||||
|
import type { TabListProps as AriaTabListProps, TabProps as AriaTabProps, TabRenderProps as AriaTabRenderProps } from "react-aria-components";
|
||||||
|
import { Tab as AriaTab, TabList as AriaTabList, TabPanel as AriaTabPanel, Tabs as AriaTabs, TabsContext, useSlottedContext } from "react-aria-components";
|
||||||
|
import type { BadgeColors } from "@/components/base/badges/badge-types";
|
||||||
|
import { Badge } from "@/components/base/badges/badges";
|
||||||
|
import { cx } from "@/utils/cx";
|
||||||
|
|
||||||
|
type Orientation = "horizontal" | "vertical";
|
||||||
|
|
||||||
|
// Types for different orientations
|
||||||
|
type HorizontalTypes = "button-brand" | "button-gray" | "button-border" | "button-minimal" | "underline";
|
||||||
|
type VerticalTypes = "button-brand" | "button-gray" | "button-border" | "button-minimal" | "line";
|
||||||
|
type TabTypeColors<T> = T extends "horizontal" ? HorizontalTypes : VerticalTypes;
|
||||||
|
|
||||||
|
// Styles for different types of tab
|
||||||
|
const getTabStyles = ({ isFocusVisible, isSelected, isHovered }: AriaTabRenderProps) => ({
|
||||||
|
"button-brand": cx(
|
||||||
|
"outline-focus-ring",
|
||||||
|
isFocusVisible && "outline-2 -outline-offset-2",
|
||||||
|
(isSelected || isHovered) && "bg-brand-primary_alt text-brand-secondary",
|
||||||
|
),
|
||||||
|
"button-gray": cx(
|
||||||
|
"outline-focus-ring",
|
||||||
|
isHovered && "bg-primary_hover text-secondary",
|
||||||
|
isFocusVisible && "outline-2 -outline-offset-2",
|
||||||
|
isSelected && "bg-active text-secondary",
|
||||||
|
),
|
||||||
|
"button-border": cx(
|
||||||
|
"outline-focus-ring",
|
||||||
|
(isSelected || isHovered) && "bg-primary_alt text-secondary shadow-sm",
|
||||||
|
isFocusVisible && "outline-2 -outline-offset-2",
|
||||||
|
),
|
||||||
|
"button-minimal": cx(
|
||||||
|
"rounded-lg outline-focus-ring",
|
||||||
|
isHovered && "text-secondary",
|
||||||
|
isFocusVisible && "outline-2 -outline-offset-2",
|
||||||
|
isSelected && "bg-primary_alt text-secondary shadow-xs ring-1 ring-primary ring-inset",
|
||||||
|
),
|
||||||
|
underline: cx(
|
||||||
|
"rounded-none border-b-2 border-transparent outline-focus-ring",
|
||||||
|
(isSelected || isHovered) && "border-fg-brand-primary_alt text-brand-secondary",
|
||||||
|
isFocusVisible && "outline-2 -outline-offset-2",
|
||||||
|
),
|
||||||
|
line: cx(
|
||||||
|
"rounded-none border-l-2 border-transparent outline-focus-ring",
|
||||||
|
(isSelected || isHovered) && "border-fg-brand-primary_alt text-brand-secondary",
|
||||||
|
isFocusVisible && "outline-2 -outline-offset-2",
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
const sizes = {
|
||||||
|
sm: {
|
||||||
|
"button-brand": "text-sm font-semibold py-2 px-3",
|
||||||
|
"button-gray": "text-sm font-semibold py-2 px-3",
|
||||||
|
"button-border": "text-sm font-semibold py-2 px-3",
|
||||||
|
"button-minimal": "text-sm font-semibold py-2 px-3",
|
||||||
|
underline: "text-sm font-semibold px-1 pb-2.5 pt-0",
|
||||||
|
line: "text-sm font-semibold pl-2.5 pr-3 py-0.5",
|
||||||
|
},
|
||||||
|
md: {
|
||||||
|
"button-brand": "text-md font-semibold py-2.5 px-3",
|
||||||
|
"button-gray": "text-md font-semibold py-2.5 px-3",
|
||||||
|
"button-border": "text-md font-semibold py-2.5 px-3",
|
||||||
|
"button-minimal": "text-md font-semibold py-2.5 px-3",
|
||||||
|
underline: "text-md font-semibold px-1 pb-2.5 pt-0",
|
||||||
|
line: "text-md font-semibold pr-3.5 pl-3 py-1",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Styles for different types of horizontal tabs
|
||||||
|
const getHorizontalStyles = ({ size, fullWidth }: { size?: "sm" | "md"; fullWidth?: boolean }) => ({
|
||||||
|
"button-brand": "gap-1",
|
||||||
|
"button-gray": "gap-1",
|
||||||
|
"button-border": cx("gap-1 rounded-[10px] bg-secondary_alt p-1 ring-1 ring-secondary ring-inset", size === "md" && "rounded-xl p-1.5"),
|
||||||
|
"button-minimal": "gap-0.5 rounded-lg bg-secondary_alt ring-1 ring-inset ring-secondary",
|
||||||
|
underline: cx("gap-3", fullWidth && "w-full gap-4"),
|
||||||
|
line: "gap-2",
|
||||||
|
});
|
||||||
|
|
||||||
|
const getColorStyles = ({ isSelected, isHovered }: Partial<AriaTabRenderProps>) => ({
|
||||||
|
"button-brand": isSelected || isHovered ? "brand" : "gray",
|
||||||
|
"button-gray": "gray",
|
||||||
|
"button-border": "gray",
|
||||||
|
"button-minimal": "gray",
|
||||||
|
underline: isSelected || isHovered ? "brand" : "gray",
|
||||||
|
line: isSelected || isHovered ? "brand" : "gray",
|
||||||
|
});
|
||||||
|
|
||||||
|
interface TabListComponentProps<T extends object, K extends Orientation> extends AriaTabListProps<T> {
|
||||||
|
/** The size of the tab list. */
|
||||||
|
size?: keyof typeof sizes;
|
||||||
|
/** The type of the tab list. */
|
||||||
|
type?: TabTypeColors<K>;
|
||||||
|
/** The orientation of the tab list. */
|
||||||
|
orientation?: K;
|
||||||
|
/** The items of the tab list. */
|
||||||
|
items: T[];
|
||||||
|
/** Whether the tab list is full width. */
|
||||||
|
fullWidth?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TabListContext = createContext<Omit<TabListComponentProps<TabComponentProps, Orientation>, "items">>({
|
||||||
|
size: "sm",
|
||||||
|
type: "button-brand",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const TabList = <T extends Orientation>({
|
||||||
|
size = "sm",
|
||||||
|
type = "button-brand",
|
||||||
|
orientation: orientationProp,
|
||||||
|
fullWidth,
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...otherProps
|
||||||
|
}: TabListComponentProps<TabComponentProps, T>) => {
|
||||||
|
const context = useSlottedContext(TabsContext);
|
||||||
|
|
||||||
|
const orientation = orientationProp ?? context?.orientation ?? "horizontal";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TabListContext.Provider value={{ size, type, orientation, fullWidth }}>
|
||||||
|
<AriaTabList
|
||||||
|
{...otherProps}
|
||||||
|
className={(state) =>
|
||||||
|
cx(
|
||||||
|
"group flex",
|
||||||
|
|
||||||
|
getHorizontalStyles({
|
||||||
|
size,
|
||||||
|
fullWidth,
|
||||||
|
})[type as HorizontalTypes],
|
||||||
|
|
||||||
|
orientation === "vertical" && "w-max flex-col",
|
||||||
|
|
||||||
|
// Only horizontal tabs with underline type have bottom border
|
||||||
|
orientation === "horizontal" &&
|
||||||
|
type === "underline" &&
|
||||||
|
"relative before:absolute before:inset-x-0 before:bottom-0 before:h-px before:bg-border-secondary",
|
||||||
|
|
||||||
|
typeof className === "function" ? className(state) : className,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{children ?? ((item) => <Tab {...item}>{item.children}</Tab>)}
|
||||||
|
</AriaTabList>
|
||||||
|
</TabListContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TabPanel = (props: ComponentPropsWithRef<typeof AriaTabPanel>) => {
|
||||||
|
return (
|
||||||
|
<AriaTabPanel
|
||||||
|
{...props}
|
||||||
|
className={(state) =>
|
||||||
|
cx(
|
||||||
|
"outline-focus-ring focus-visible:outline-2 focus-visible:outline-offset-2",
|
||||||
|
typeof props.className === "function" ? props.className(state) : props.className,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface TabComponentProps extends AriaTabProps {
|
||||||
|
/** The label of the tab. */
|
||||||
|
label?: ReactNode;
|
||||||
|
/** The children of the tab. */
|
||||||
|
children?: ReactNode | ((props: AriaTabRenderProps) => ReactNode);
|
||||||
|
/** The badge displayed next to the label. */
|
||||||
|
badge?: number | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Tab = (props: TabComponentProps) => {
|
||||||
|
const { label, children, badge, ...otherProps } = props;
|
||||||
|
const { size = "sm", type = "button-brand", fullWidth } = useContext(TabListContext);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AriaTab
|
||||||
|
{...otherProps}
|
||||||
|
className={(prop) =>
|
||||||
|
cx(
|
||||||
|
"z-10 flex h-max cursor-pointer items-center justify-center gap-2 rounded-md whitespace-nowrap text-quaternary transition duration-100 ease-linear",
|
||||||
|
"group-orientation-vertical:justify-start",
|
||||||
|
fullWidth && "w-full flex-1",
|
||||||
|
sizes[size][type],
|
||||||
|
getTabStyles(prop)[type],
|
||||||
|
typeof props.className === "function" ? props.className(prop) : props.className,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{(state) => (
|
||||||
|
<Fragment>
|
||||||
|
{typeof children === "function" ? children(state) : children || label}
|
||||||
|
{badge && (
|
||||||
|
<Badge
|
||||||
|
size={size}
|
||||||
|
type="pill-color"
|
||||||
|
color={getColorStyles(state)[type] as BadgeColors}
|
||||||
|
className={cx("hidden transition-inherit-all md:flex", size === "sm" && "-my-px")}
|
||||||
|
>
|
||||||
|
{badge}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</Fragment>
|
||||||
|
)}
|
||||||
|
</AriaTab>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Tabs = ({ className, ...props }: ComponentPropsWithRef<typeof AriaTabs>) => {
|
||||||
|
return (
|
||||||
|
<AriaTabs
|
||||||
|
keyboardActivation="manual"
|
||||||
|
{...props}
|
||||||
|
className={(state) => cx("flex w-full flex-col", typeof className === "function" ? className(state) : className)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Tabs.Panel = TabPanel;
|
||||||
|
Tabs.List = TabList;
|
||||||
|
Tabs.Item = Tab;
|
||||||
28
src/components/base/avatar/avatar-label-group.tsx
Normal file
28
src/components/base/avatar/avatar-label-group.tsx
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { type ReactNode } from "react";
|
||||||
|
import { cx } from "@/utils/cx";
|
||||||
|
import { Avatar, type AvatarProps } from "./avatar";
|
||||||
|
|
||||||
|
const styles = {
|
||||||
|
sm: { root: "gap-2", title: "text-sm font-semibold", subtitle: "text-xs" },
|
||||||
|
md: { root: "gap-2", title: "text-sm font-semibold", subtitle: "text-sm" },
|
||||||
|
lg: { root: "gap-3", title: "text-md font-semibold", subtitle: "text-md" },
|
||||||
|
xl: { root: "gap-4", title: "text-lg font-semibold", subtitle: "text-md" },
|
||||||
|
};
|
||||||
|
|
||||||
|
interface AvatarLabelGroupProps extends AvatarProps {
|
||||||
|
size: "sm" | "md" | "lg" | "xl";
|
||||||
|
title: string | ReactNode;
|
||||||
|
subtitle: string | ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AvatarLabelGroup = ({ title, subtitle, className, ...props }: AvatarLabelGroupProps) => {
|
||||||
|
return (
|
||||||
|
<figure className={cx("group flex min-w-0 flex-1 items-center", styles[props.size].root, className)}>
|
||||||
|
<Avatar {...props} />
|
||||||
|
<figcaption className="min-w-0 flex-1">
|
||||||
|
<p className={cx("text-primary", styles[props.size].title)}>{title}</p>
|
||||||
|
<p className={cx("truncate text-tertiary", styles[props.size].subtitle)}>{subtitle}</p>
|
||||||
|
</figcaption>
|
||||||
|
</figure>
|
||||||
|
);
|
||||||
|
};
|
||||||
123
src/components/base/avatar/avatar-profile-photo.tsx
Normal file
123
src/components/base/avatar/avatar-profile-photo.tsx
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { User01 } from "@untitledui/icons";
|
||||||
|
import { cx } from "@/utils/cx";
|
||||||
|
import { type AvatarProps } from "./avatar";
|
||||||
|
import { AvatarOnlineIndicator, VerifiedTick } from "./base-components";
|
||||||
|
|
||||||
|
const styles = {
|
||||||
|
sm: {
|
||||||
|
root: "size-18 p-0.75",
|
||||||
|
rootWithPlaceholder: "p-1",
|
||||||
|
content: "",
|
||||||
|
icon: "size-9",
|
||||||
|
initials: "text-display-sm font-semibold",
|
||||||
|
badge: "bottom-0.5 right-0.5",
|
||||||
|
},
|
||||||
|
md: {
|
||||||
|
root: "size-24 p-1",
|
||||||
|
rootWithPlaceholder: "p-1.25",
|
||||||
|
content: "shadow-xl",
|
||||||
|
icon: "size-12",
|
||||||
|
initials: "text-display-md font-semibold",
|
||||||
|
badge: "bottom-1 right-1",
|
||||||
|
},
|
||||||
|
lg: {
|
||||||
|
root: "size-40 p-1.5",
|
||||||
|
rootWithPlaceholder: "p-1.75",
|
||||||
|
content: "shadow-2xl",
|
||||||
|
icon: "size-20",
|
||||||
|
initials: "text-display-xl font-semibold",
|
||||||
|
badge: "bottom-2 right-2",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const tickSizeMap = {
|
||||||
|
sm: "2xl",
|
||||||
|
md: "3xl",
|
||||||
|
lg: "4xl",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
interface AvatarProfilePhotoProps extends AvatarProps {
|
||||||
|
size: "sm" | "md" | "lg";
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AvatarProfilePhoto = ({
|
||||||
|
contrastBorder = true,
|
||||||
|
size = "md",
|
||||||
|
src,
|
||||||
|
alt,
|
||||||
|
initials,
|
||||||
|
placeholder,
|
||||||
|
placeholderIcon: PlaceholderIcon,
|
||||||
|
verified,
|
||||||
|
badge,
|
||||||
|
status,
|
||||||
|
className,
|
||||||
|
}: AvatarProfilePhotoProps) => {
|
||||||
|
const [isFailed, setIsFailed] = useState(false);
|
||||||
|
|
||||||
|
const renderMainContent = () => {
|
||||||
|
if (src && !isFailed) {
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
src={src}
|
||||||
|
alt={alt}
|
||||||
|
onError={() => setIsFailed(true)}
|
||||||
|
className={cx(
|
||||||
|
"size-full rounded-full object-cover",
|
||||||
|
contrastBorder && "outline-1 -outline-offset-1 outline-avatar-contrast-border",
|
||||||
|
styles[size].content,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (initials) {
|
||||||
|
return (
|
||||||
|
<div className={cx("flex size-full items-center justify-center rounded-full bg-tertiary ring-1 ring-secondary_alt", styles[size].content)}>
|
||||||
|
<span className={cx("text-quaternary", styles[size].initials)}>{initials}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (PlaceholderIcon) {
|
||||||
|
return (
|
||||||
|
<div className={cx("flex size-full items-center justify-center rounded-full bg-tertiary ring-1 ring-secondary_alt", styles[size].content)}>
|
||||||
|
<PlaceholderIcon className={cx("text-fg-quaternary", styles[size].icon)} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cx("flex size-full items-center justify-center rounded-full bg-tertiary ring-1 ring-secondary_alt", styles[size].content)}>
|
||||||
|
{placeholder || <User01 className={cx("text-fg-quaternary", styles[size].icon)} />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderBadgeContent = () => {
|
||||||
|
if (status) {
|
||||||
|
return <AvatarOnlineIndicator status={status} size={tickSizeMap[size]} className={styles[size].badge} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (verified) {
|
||||||
|
return <VerifiedTick size={tickSizeMap[size]} className={cx("absolute", styles[size].badge)} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return badge;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cx(
|
||||||
|
"relative flex shrink-0 items-center justify-center rounded-full bg-primary ring-1 ring-secondary_alt",
|
||||||
|
styles[size].root,
|
||||||
|
(!src || isFailed) && styles[size].rootWithPlaceholder,
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{renderMainContent()}
|
||||||
|
{renderBadgeContent()}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
129
src/components/base/avatar/avatar.tsx
Normal file
129
src/components/base/avatar/avatar.tsx
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
import { type FC, type ReactNode, useState } from "react";
|
||||||
|
import { User01 } from "@untitledui/icons";
|
||||||
|
import { cx } from "@/utils/cx";
|
||||||
|
import { AvatarOnlineIndicator, VerifiedTick } from "./base-components";
|
||||||
|
|
||||||
|
type AvatarSize = "xxs" | "xs" | "sm" | "md" | "lg" | "xl" | "2xl";
|
||||||
|
|
||||||
|
export interface AvatarProps {
|
||||||
|
size?: AvatarSize;
|
||||||
|
className?: string;
|
||||||
|
src?: string | null;
|
||||||
|
alt?: string;
|
||||||
|
/**
|
||||||
|
* Display a contrast border around the avatar.
|
||||||
|
*/
|
||||||
|
contrastBorder?: boolean;
|
||||||
|
/**
|
||||||
|
* Display a badge (i.e. company logo).
|
||||||
|
*/
|
||||||
|
badge?: ReactNode;
|
||||||
|
/**
|
||||||
|
* Display a status indicator.
|
||||||
|
*/
|
||||||
|
status?: "online" | "offline";
|
||||||
|
/**
|
||||||
|
* Display a verified tick icon.
|
||||||
|
*
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
verified?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The initials of the user to display if no image is available.
|
||||||
|
*/
|
||||||
|
initials?: string;
|
||||||
|
/**
|
||||||
|
* An icon to display if no image is available.
|
||||||
|
*/
|
||||||
|
placeholderIcon?: FC<{ className?: string }>;
|
||||||
|
/**
|
||||||
|
* A placeholder to display if no image is available.
|
||||||
|
*/
|
||||||
|
placeholder?: ReactNode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the avatar should show a focus ring when the parent group is in focus.
|
||||||
|
* For example, when the avatar is wrapped inside a link.
|
||||||
|
*
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
focusable?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = {
|
||||||
|
xxs: { root: "size-4 outline-[0.5px] -outline-offset-[0.5px]", initials: "text-xs font-semibold", icon: "size-3" },
|
||||||
|
xs: { root: "size-6 outline-[0.5px] -outline-offset-[0.5px]", initials: "text-xs font-semibold", icon: "size-4" },
|
||||||
|
sm: { root: "size-8 outline-[0.75px] -outline-offset-[0.75px]", initials: "text-sm font-semibold", icon: "size-5" },
|
||||||
|
md: { root: "size-10 outline-1 -outline-offset-1", initials: "text-md font-semibold", icon: "size-6" },
|
||||||
|
lg: { root: "size-12 outline-1 -outline-offset-1", initials: "text-lg font-semibold", icon: "size-7" },
|
||||||
|
xl: { root: "size-14 outline-1 -outline-offset-1", initials: "text-xl font-semibold", icon: "size-8" },
|
||||||
|
"2xl": { root: "size-16 outline-1 -outline-offset-1", initials: "text-display-xs font-semibold", icon: "size-8" },
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Avatar = ({
|
||||||
|
contrastBorder = true,
|
||||||
|
size = "md",
|
||||||
|
src,
|
||||||
|
alt,
|
||||||
|
initials,
|
||||||
|
placeholder,
|
||||||
|
placeholderIcon: PlaceholderIcon,
|
||||||
|
badge,
|
||||||
|
status,
|
||||||
|
verified,
|
||||||
|
focusable = false,
|
||||||
|
className,
|
||||||
|
}: AvatarProps) => {
|
||||||
|
const [isFailed, setIsFailed] = useState(false);
|
||||||
|
|
||||||
|
const renderMainContent = () => {
|
||||||
|
if (src && !isFailed) {
|
||||||
|
return <img data-avatar-img className="size-full rounded-full object-cover" src={src} alt={alt} onError={() => setIsFailed(true)} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (initials) {
|
||||||
|
return <span className={cx("text-quaternary", styles[size].initials)}>{initials}</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (PlaceholderIcon) {
|
||||||
|
return <PlaceholderIcon className={cx("text-fg-quaternary", styles[size].icon)} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return placeholder || <User01 className={cx("text-fg-quaternary", styles[size].icon)} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderBadgeContent = () => {
|
||||||
|
if (status) {
|
||||||
|
return <AvatarOnlineIndicator status={status} size={size === "xxs" ? "xs" : size} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (verified) {
|
||||||
|
return (
|
||||||
|
<VerifiedTick
|
||||||
|
size={size === "xxs" ? "xs" : size}
|
||||||
|
className={cx("absolute right-0 bottom-0", (size === "xxs" || size === "xs") && "-right-px -bottom-px")}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return badge;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-avatar
|
||||||
|
className={cx(
|
||||||
|
"relative inline-flex shrink-0 items-center justify-center rounded-full bg-avatar-bg outline-transparent",
|
||||||
|
// Focus styles
|
||||||
|
focusable && "group-outline-focus-ring group-focus-visible:outline-2 group-focus-visible:outline-offset-2",
|
||||||
|
contrastBorder && "outline outline-avatar-contrast-border",
|
||||||
|
styles[size].root,
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{renderMainContent()}
|
||||||
|
{renderBadgeContent()}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { Plus } from "@untitledui/icons";
|
||||||
|
import type { ButtonProps as AriaButtonProps } from "react-aria-components";
|
||||||
|
import { Tooltip as AriaTooltip, TooltipTrigger as AriaTooltipTrigger } from "@/components/base/tooltip/tooltip";
|
||||||
|
import { cx } from "@/utils/cx";
|
||||||
|
|
||||||
|
const sizes = {
|
||||||
|
xs: { root: "size-6", icon: "size-4" },
|
||||||
|
sm: { root: "size-8", icon: "size-4" },
|
||||||
|
md: { root: "size-10", icon: "size-5" },
|
||||||
|
};
|
||||||
|
|
||||||
|
interface AvatarAddButtonProps extends AriaButtonProps {
|
||||||
|
size: "xs" | "sm" | "md";
|
||||||
|
title?: string;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AvatarAddButton = ({ size, className, title = "Add user", ...props }: AvatarAddButtonProps) => (
|
||||||
|
<AriaTooltip title={title}>
|
||||||
|
<AriaTooltipTrigger
|
||||||
|
{...props}
|
||||||
|
aria-label={title}
|
||||||
|
className={cx(
|
||||||
|
"flex cursor-pointer items-center justify-center rounded-full border border-dashed border-primary bg-primary text-fg-quaternary outline-focus-ring transition duration-100 ease-linear hover:bg-primary_hover hover:text-fg-quaternary_hover focus-visible:outline-2 focus-visible:outline-offset-2 disabled:border-gray-200 disabled:bg-secondary disabled:text-gray-200",
|
||||||
|
sizes[size].root,
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Plus className={cx("text-current transition-inherit-all", sizes[size].icon)} />
|
||||||
|
</AriaTooltipTrigger>
|
||||||
|
</AriaTooltip>
|
||||||
|
);
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { cx } from "@/utils/cx";
|
||||||
|
|
||||||
|
const sizes = {
|
||||||
|
xs: "size-2",
|
||||||
|
sm: "size-3",
|
||||||
|
md: "size-3.5",
|
||||||
|
lg: "size-4",
|
||||||
|
xl: "size-4.5",
|
||||||
|
"2xl": "size-5 ring-[1.67px]",
|
||||||
|
};
|
||||||
|
|
||||||
|
interface AvatarCompanyIconProps {
|
||||||
|
size: "xs" | "sm" | "md" | "lg" | "xl" | "2xl";
|
||||||
|
src: string;
|
||||||
|
alt?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AvatarCompanyIcon = ({ size, src, alt }: AvatarCompanyIconProps) => (
|
||||||
|
<img
|
||||||
|
src={src}
|
||||||
|
alt={alt}
|
||||||
|
className={cx("bg-primary-25 absolute -right-0.5 -bottom-0.5 rounded-full object-cover ring-[1.5px] ring-bg-primary", sizes[size])}
|
||||||
|
/>
|
||||||
|
);
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { cx } from "@/utils/cx";
|
||||||
|
|
||||||
|
const sizes = {
|
||||||
|
xs: "size-1.5",
|
||||||
|
sm: "size-2",
|
||||||
|
md: "size-2.5",
|
||||||
|
lg: "size-3",
|
||||||
|
xl: "size-3.5",
|
||||||
|
"2xl": "size-4",
|
||||||
|
"3xl": "size-4.5",
|
||||||
|
"4xl": "size-5",
|
||||||
|
};
|
||||||
|
|
||||||
|
interface AvatarOnlineIndicatorProps {
|
||||||
|
size: "xs" | "sm" | "md" | "lg" | "xl" | "2xl" | "3xl" | "4xl";
|
||||||
|
status: "online" | "offline";
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AvatarOnlineIndicator = ({ size, status, className }: AvatarOnlineIndicatorProps) => (
|
||||||
|
<span
|
||||||
|
className={cx(
|
||||||
|
"absolute right-0 bottom-0 rounded-full ring-[1.5px] ring-bg-primary",
|
||||||
|
status === "online" ? "bg-fg-success-secondary" : "bg-fg-disabled_subtle",
|
||||||
|
sizes[size],
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
4
src/components/base/avatar/base-components/index.tsx
Normal file
4
src/components/base/avatar/base-components/index.tsx
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export * from "./avatar-add-button";
|
||||||
|
export * from "./avatar-company-icon";
|
||||||
|
export * from "./avatar-online-indicator";
|
||||||
|
export * from "./verified-tick";
|
||||||
32
src/components/base/avatar/base-components/verified-tick.tsx
Normal file
32
src/components/base/avatar/base-components/verified-tick.tsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { cx } from "@/utils/cx";
|
||||||
|
|
||||||
|
const sizes = {
|
||||||
|
xs: { root: "size-2.5", tick: "size-[4.38px" },
|
||||||
|
sm: { root: "size-3", tick: "size-[5.25px]" },
|
||||||
|
md: { root: "size-3.5", tick: "size-[6.13px]" },
|
||||||
|
lg: { root: "size-4", tick: "size-[7px]" },
|
||||||
|
xl: { root: "size-4.5", tick: "size-[7.88px]" },
|
||||||
|
"2xl": { root: "size-5", tick: "size-[8.75px]" },
|
||||||
|
"3xl": { root: "size-6", tick: "size-[10.5px]" },
|
||||||
|
"4xl": { root: "size-8", tick: "size-[14px]" },
|
||||||
|
};
|
||||||
|
|
||||||
|
interface VerifiedTickProps {
|
||||||
|
size: "xs" | "sm" | "md" | "lg" | "xl" | "2xl" | "3xl" | "4xl";
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const VerifiedTick = ({ size, className }: VerifiedTickProps) => (
|
||||||
|
<svg data-verified className={cx("z-10 text-utility-blue-500", sizes[size].root, className)} viewBox="0 0 10 10" fill="none">
|
||||||
|
<path
|
||||||
|
d="M7.72237 1.77098C7.81734 2.00068 7.99965 2.18326 8.2292 2.27858L9.03413 2.61199C9.26384 2.70714 9.44635 2.88965 9.5415 3.11936C9.63665 3.34908 9.63665 3.60718 9.5415 3.83689L9.20833 4.64125C9.11313 4.87106 9.113 5.12943 9.20863 5.35913L9.54122 6.16325C9.58839 6.27702 9.61268 6.39897 9.6127 6.52214C9.61272 6.6453 9.58847 6.76726 9.54134 6.88105C9.4942 6.99484 9.42511 7.09823 9.33801 7.18531C9.2509 7.27238 9.14749 7.34144 9.03369 7.38854L8.22934 7.72171C7.99964 7.81669 7.81706 7.99899 7.72174 8.22855L7.38833 9.03348C7.29318 9.26319 7.11067 9.4457 6.88096 9.54085C6.65124 9.636 6.39314 9.636 6.16343 9.54085L5.35907 9.20767C5.12935 9.11276 4.87134 9.11295 4.64177 9.20821L3.83684 9.54115C3.60725 9.63608 3.34937 9.636 3.11984 9.54092C2.89032 9.44585 2.70791 9.26356 2.6127 9.03409L2.27918 8.22892C2.18421 7.99923 2.0019 7.81665 1.77235 7.72133L0.967421 7.38792C0.737807 7.29281 0.555355 7.11041 0.460169 6.88083C0.364983 6.65125 0.364854 6.39327 0.45981 6.16359L0.792984 5.35924C0.8879 5.12952 0.887707 4.87151 0.792445 4.64193L0.459749 3.83642C0.41258 3.72265 0.388291 3.60069 0.388272 3.47753C0.388252 3.35436 0.412501 3.2324 0.459634 3.11861C0.506767 3.00482 0.57586 2.90144 0.662965 2.81436C0.75007 2.72728 0.853479 2.65822 0.967283 2.61113L1.77164 2.27795C2.00113 2.18306 2.1836 2.00099 2.27899 1.7717L2.6124 0.966768C2.70755 0.737054 2.89006 0.554547 3.11978 0.459397C3.34949 0.364246 3.60759 0.364246 3.83731 0.459397L4.64166 0.792571C4.87138 0.887487 5.12939 0.887293 5.35897 0.792031L6.16424 0.459913C6.39392 0.364816 6.65197 0.364836 6.88164 0.459968C7.11131 0.555099 7.29379 0.737554 7.38895 0.967208L7.72247 1.77238L7.72237 1.77098Z"
|
||||||
|
className="fill-current"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M6.95829 3.68932C7.02509 3.58439 7.04747 3.45723 7.02051 3.3358C6.99356 3.21437 6.91946 3.10862 6.81454 3.04182C6.70961 2.97502 6.58245 2.95264 6.46102 2.97959C6.33959 3.00655 6.23384 3.08064 6.16704 3.18557L4.33141 6.06995L3.49141 5.01995C3.41375 4.92281 3.30069 4.8605 3.17709 4.84673C3.05349 4.83296 2.92949 4.86885 2.83235 4.94651C2.73522 5.02417 2.67291 5.13723 2.65914 5.26083C2.64536 5.38443 2.68125 5.50843 2.75891 5.60557L4.00891 7.16807C4.0555 7.22638 4.11533 7.27271 4.18344 7.30323C4.25154 7.33375 4.32595 7.34757 4.40047 7.34353C4.47499 7.3395 4.54747 7.31773 4.61188 7.28004C4.67629 7.24234 4.73077 7.18981 4.77079 7.12682L6.95829 3.68932Z"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
12
src/components/base/avatar/utils.ts
Normal file
12
src/components/base/avatar/utils.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
/**
|
||||||
|
* Extracts the initials from a full name.
|
||||||
|
*
|
||||||
|
* @param name - The full name from which to extract initials.
|
||||||
|
* @returns The initials of the provided name. If the name contains only one word,
|
||||||
|
* it returns the first character of that word. If the name contains two words,
|
||||||
|
* it returns the first character of each word.
|
||||||
|
*/
|
||||||
|
export const getInitials = (name: string) => {
|
||||||
|
const [firstName, lastName] = name.split(" ");
|
||||||
|
return firstName.charAt(0) + (lastName ? lastName.charAt(0) : "");
|
||||||
|
};
|
||||||
174
src/components/base/badges/badge-groups.tsx
Normal file
174
src/components/base/badges/badge-groups.tsx
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
import type { FC, ReactNode } from "react";
|
||||||
|
import { isValidElement } from "react";
|
||||||
|
import { ArrowRight } from "@untitledui/icons";
|
||||||
|
import { cx, sortCx } from "@/utils/cx";
|
||||||
|
import { isReactComponent } from "@/utils/is-react-component";
|
||||||
|
|
||||||
|
type Size = "md" | "lg";
|
||||||
|
type Color = "brand" | "warning" | "error" | "gray" | "success";
|
||||||
|
type Theme = "light" | "modern";
|
||||||
|
type Align = "leading" | "trailing";
|
||||||
|
|
||||||
|
const baseClasses: Record<Theme, { root?: string; addon?: string; icon?: string }> = {
|
||||||
|
light: {
|
||||||
|
root: "rounded-full ring-1 ring-inset",
|
||||||
|
addon: "rounded-full ring-1 ring-inset",
|
||||||
|
},
|
||||||
|
modern: {
|
||||||
|
root: "rounded-[10px] bg-primary text-secondary shadow-xs ring-1 ring-inset ring-primary hover:bg-secondary",
|
||||||
|
addon: "flex items-center rounded-md bg-primary shadow-xs ring-1 ring-inset ring-primary",
|
||||||
|
icon: "text-utility-gray-500",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSizeClasses = (
|
||||||
|
theme?: Theme,
|
||||||
|
text?: boolean,
|
||||||
|
icon?: boolean,
|
||||||
|
): Record<Align, Record<Size, { root?: string; addon?: string; icon?: string; dot?: string }>> => ({
|
||||||
|
leading: {
|
||||||
|
md: {
|
||||||
|
root: cx("py-1 pr-2 pl-1 text-xs font-medium", !text && !icon && "pr-1"),
|
||||||
|
addon: cx("px-2 py-0.5", theme === "modern" && "gap-1 px-1.5", text && "mr-2"),
|
||||||
|
icon: "ml-1 size-4",
|
||||||
|
},
|
||||||
|
lg: {
|
||||||
|
root: cx("py-1 pr-2 pl-1 text-sm font-medium", !text && !icon && "pr-1"),
|
||||||
|
addon: cx("px-2.5 py-0.5", theme === "modern" && "gap-1.5 px-2", text && "mr-2"),
|
||||||
|
icon: "ml-1 size-4",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
trailing: {
|
||||||
|
md: {
|
||||||
|
root: cx("py-1 pr-1 pl-3 text-xs font-medium", theme === "modern" && "pl-2.5"),
|
||||||
|
addon: cx("py-0.5 pr-1.5 pl-2", theme === "modern" && "pr-1.5 pl-2", text && "ml-2"),
|
||||||
|
icon: "ml-0.5 size-3 stroke-[3px]",
|
||||||
|
dot: "mr-1.5",
|
||||||
|
},
|
||||||
|
lg: {
|
||||||
|
root: "py-1 pr-1 pl-3 text-sm font-medium",
|
||||||
|
addon: cx("py-0.5 pr-2 pl-2.5", theme === "modern" && "pr-1.5 pl-2", text && "ml-2"),
|
||||||
|
icon: "ml-1 size-3 stroke-[3px]",
|
||||||
|
dot: "mr-2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const colorClasses: Record<Theme, Record<Color, { root?: string; addon?: string; icon?: string; dot?: string }>> = sortCx({
|
||||||
|
light: {
|
||||||
|
brand: {
|
||||||
|
root: "bg-utility-brand-50 text-utility-brand-700 ring-utility-brand-200 hover:bg-utility-brand-100",
|
||||||
|
addon: "bg-primary text-current ring-utility-brand-200",
|
||||||
|
icon: "text-utility-brand-500",
|
||||||
|
},
|
||||||
|
gray: {
|
||||||
|
root: "bg-utility-gray-50 text-utility-gray-700 ring-utility-gray-200 hover:bg-utility-gray-100",
|
||||||
|
addon: "bg-primary text-current ring-utility-gray-200",
|
||||||
|
icon: "text-utility-gray-500",
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
root: "bg-utility-error-50 text-utility-error-700 ring-utility-error-200 hover:bg-utility-error-100",
|
||||||
|
addon: "bg-primary text-current ring-utility-error-200",
|
||||||
|
icon: "text-utility-error-500",
|
||||||
|
},
|
||||||
|
warning: {
|
||||||
|
root: "bg-utility-warning-50 text-utility-warning-700 ring-utility-warning-200 hover:bg-utility-warning-100",
|
||||||
|
addon: "bg-primary text-current ring-utility-warning-200",
|
||||||
|
icon: "text-utility-warning-500",
|
||||||
|
},
|
||||||
|
success: {
|
||||||
|
root: "bg-utility-success-50 text-utility-success-700 ring-utility-success-200 hover:bg-utility-success-100",
|
||||||
|
addon: "bg-primary text-current ring-utility-success-200",
|
||||||
|
icon: "text-utility-success-500",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
modern: {
|
||||||
|
brand: {
|
||||||
|
dot: "bg-utility-brand-500 outline-3 -outline-offset-1 outline-utility-brand-100",
|
||||||
|
},
|
||||||
|
gray: {
|
||||||
|
dot: "bg-utility-gray-500 outline-3 -outline-offset-1 outline-utility-gray-100",
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
dot: "bg-utility-error-500 outline-3 -outline-offset-1 outline-utility-error-100",
|
||||||
|
},
|
||||||
|
warning: {
|
||||||
|
dot: "bg-utility-warning-500 outline-3 -outline-offset-1 outline-utility-warning-100",
|
||||||
|
},
|
||||||
|
success: {
|
||||||
|
dot: "bg-utility-success-500 outline-3 -outline-offset-1 outline-utility-success-100",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
interface BadgeGroupProps {
|
||||||
|
children?: string | ReactNode;
|
||||||
|
addonText: string;
|
||||||
|
size?: Size;
|
||||||
|
color: Color;
|
||||||
|
theme?: Theme;
|
||||||
|
/**
|
||||||
|
* Alignment of the badge addon element.
|
||||||
|
*/
|
||||||
|
align?: Align;
|
||||||
|
iconTrailing?: FC<{ className?: string }> | ReactNode;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BadgeGroup = ({
|
||||||
|
children,
|
||||||
|
addonText,
|
||||||
|
size = "md",
|
||||||
|
color = "brand",
|
||||||
|
theme = "light",
|
||||||
|
align = "leading",
|
||||||
|
className,
|
||||||
|
iconTrailing: IconTrailing = ArrowRight,
|
||||||
|
}: BadgeGroupProps) => {
|
||||||
|
const colors = colorClasses[theme][color];
|
||||||
|
const sizes = getSizeClasses(theme, !!children, !!IconTrailing)[align][size];
|
||||||
|
|
||||||
|
const rootClasses = cx(
|
||||||
|
"inline-flex w-max cursor-pointer items-center transition duration-100 ease-linear",
|
||||||
|
baseClasses[theme].root,
|
||||||
|
sizes.root,
|
||||||
|
colors.root,
|
||||||
|
className,
|
||||||
|
);
|
||||||
|
const addonClasses = cx("inline-flex items-center", baseClasses[theme].addon, sizes.addon, colors.addon);
|
||||||
|
const dotClasses = cx("inline-block size-2 shrink-0 rounded-full", sizes.dot, colors.dot);
|
||||||
|
const iconClasses = cx(baseClasses[theme].icon, sizes.icon, colors.icon);
|
||||||
|
|
||||||
|
if (align === "trailing") {
|
||||||
|
return (
|
||||||
|
<div className={rootClasses}>
|
||||||
|
{theme === "modern" && <span className={dotClasses} />}
|
||||||
|
|
||||||
|
{children}
|
||||||
|
|
||||||
|
<span className={addonClasses}>
|
||||||
|
{addonText}
|
||||||
|
|
||||||
|
{/* Trailing icon */}
|
||||||
|
{isReactComponent(IconTrailing) && <IconTrailing className={iconClasses} />}
|
||||||
|
{isValidElement(IconTrailing) && IconTrailing}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={rootClasses}>
|
||||||
|
<span className={addonClasses}>
|
||||||
|
{theme === "modern" && <span className={dotClasses} />}
|
||||||
|
{addonText}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{children}
|
||||||
|
|
||||||
|
{/* Trailing icon */}
|
||||||
|
{isReactComponent(IconTrailing) && <IconTrailing className={iconClasses} />}
|
||||||
|
{isValidElement(IconTrailing) && IconTrailing}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
264
src/components/base/badges/badge-types.ts
Normal file
264
src/components/base/badges/badge-types.ts
Normal file
@@ -0,0 +1,264 @@
|
|||||||
|
export type IconComponentType = React.FunctionComponent<{ className?: string; strokeWidth?: string | number }>;
|
||||||
|
|
||||||
|
export type Sizes = "sm" | "md" | "lg";
|
||||||
|
|
||||||
|
export type BadgeColors = "gray" | "brand" | "error" | "warning" | "success" | "gray-blue" | "blue-light" | "blue" | "indigo" | "purple" | "pink" | "orange";
|
||||||
|
|
||||||
|
export type FlagTypes =
|
||||||
|
| "AD"
|
||||||
|
| "AE"
|
||||||
|
| "AF"
|
||||||
|
| "AG"
|
||||||
|
| "AI"
|
||||||
|
| "AL"
|
||||||
|
| "AM"
|
||||||
|
| "AO"
|
||||||
|
| "AR"
|
||||||
|
| "AS"
|
||||||
|
| "AT"
|
||||||
|
| "AU"
|
||||||
|
| "AW"
|
||||||
|
| "AX"
|
||||||
|
| "AZ"
|
||||||
|
| "BA"
|
||||||
|
| "BB"
|
||||||
|
| "BD"
|
||||||
|
| "BE"
|
||||||
|
| "BF"
|
||||||
|
| "BG"
|
||||||
|
| "BH"
|
||||||
|
| "BI"
|
||||||
|
| "BJ"
|
||||||
|
| "BL"
|
||||||
|
| "BM"
|
||||||
|
| "BN"
|
||||||
|
| "BO"
|
||||||
|
| "BQ-1"
|
||||||
|
| "BQ-2"
|
||||||
|
| "BQ"
|
||||||
|
| "BR"
|
||||||
|
| "BS"
|
||||||
|
| "BT"
|
||||||
|
| "BW"
|
||||||
|
| "BY"
|
||||||
|
| "BZ"
|
||||||
|
| "CA"
|
||||||
|
| "CC"
|
||||||
|
| "CD-1"
|
||||||
|
| "CD"
|
||||||
|
| "CF"
|
||||||
|
| "CH"
|
||||||
|
| "CK"
|
||||||
|
| "CL"
|
||||||
|
| "CM"
|
||||||
|
| "CN"
|
||||||
|
| "CO"
|
||||||
|
| "CR"
|
||||||
|
| "CU"
|
||||||
|
| "CW"
|
||||||
|
| "CX"
|
||||||
|
| "CY"
|
||||||
|
| "CZ"
|
||||||
|
| "DE"
|
||||||
|
| "DJ"
|
||||||
|
| "DK"
|
||||||
|
| "DM"
|
||||||
|
| "DO"
|
||||||
|
| "DS"
|
||||||
|
| "DZ"
|
||||||
|
| "earth"
|
||||||
|
| "EC"
|
||||||
|
| "EE"
|
||||||
|
| "EG"
|
||||||
|
| "EH"
|
||||||
|
| "ER"
|
||||||
|
| "ES"
|
||||||
|
| "ET"
|
||||||
|
| "FI"
|
||||||
|
| "FJ"
|
||||||
|
| "FK"
|
||||||
|
| "FM"
|
||||||
|
| "FO"
|
||||||
|
| "FR"
|
||||||
|
| "GA"
|
||||||
|
| "GB-2"
|
||||||
|
| "GB"
|
||||||
|
| "GD"
|
||||||
|
| "GE"
|
||||||
|
| "GG"
|
||||||
|
| "GH"
|
||||||
|
| "GI"
|
||||||
|
| "GL"
|
||||||
|
| "GM"
|
||||||
|
| "GN"
|
||||||
|
| "GQ"
|
||||||
|
| "GR"
|
||||||
|
| "GT"
|
||||||
|
| "GU"
|
||||||
|
| "GW"
|
||||||
|
| "GY"
|
||||||
|
| "HK"
|
||||||
|
| "HN"
|
||||||
|
| "HR"
|
||||||
|
| "HT"
|
||||||
|
| "HU"
|
||||||
|
| "ID"
|
||||||
|
| "IE"
|
||||||
|
| "IL"
|
||||||
|
| "IM"
|
||||||
|
| "IN"
|
||||||
|
| "IO"
|
||||||
|
| "IQ"
|
||||||
|
| "IR"
|
||||||
|
| "IS"
|
||||||
|
| "IT"
|
||||||
|
| "JE"
|
||||||
|
| "JM"
|
||||||
|
| "JO"
|
||||||
|
| "JP"
|
||||||
|
| "KE"
|
||||||
|
| "KG"
|
||||||
|
| "KH"
|
||||||
|
| "KI"
|
||||||
|
| "KM"
|
||||||
|
| "KN"
|
||||||
|
| "KP"
|
||||||
|
| "KR"
|
||||||
|
| "KW"
|
||||||
|
| "KY"
|
||||||
|
| "KZ"
|
||||||
|
| "LA"
|
||||||
|
| "LB"
|
||||||
|
| "LC"
|
||||||
|
| "LI"
|
||||||
|
| "LK"
|
||||||
|
| "LR"
|
||||||
|
| "LS"
|
||||||
|
| "LT"
|
||||||
|
| "LU"
|
||||||
|
| "LV"
|
||||||
|
| "LY"
|
||||||
|
| "MA"
|
||||||
|
| "MC"
|
||||||
|
| "MD"
|
||||||
|
| "ME"
|
||||||
|
| "MG"
|
||||||
|
| "MH"
|
||||||
|
| "MK"
|
||||||
|
| "ML"
|
||||||
|
| "MM"
|
||||||
|
| "MN"
|
||||||
|
| "MO"
|
||||||
|
| "MP"
|
||||||
|
| "MQ"
|
||||||
|
| "MR"
|
||||||
|
| "MS"
|
||||||
|
| "MT"
|
||||||
|
| "MU"
|
||||||
|
| "MV"
|
||||||
|
| "MW"
|
||||||
|
| "MX"
|
||||||
|
| "MY"
|
||||||
|
| "MZ"
|
||||||
|
| "NA"
|
||||||
|
| "NE"
|
||||||
|
| "NF"
|
||||||
|
| "NG"
|
||||||
|
| "NI"
|
||||||
|
| "NL"
|
||||||
|
| "NO"
|
||||||
|
| "NP"
|
||||||
|
| "NR"
|
||||||
|
| "NU"
|
||||||
|
| "NZ"
|
||||||
|
| "OM"
|
||||||
|
| "PA"
|
||||||
|
| "PE"
|
||||||
|
| "PF"
|
||||||
|
| "PG"
|
||||||
|
| "PH"
|
||||||
|
| "PK"
|
||||||
|
| "PL"
|
||||||
|
| "PM"
|
||||||
|
| "PN"
|
||||||
|
| "PR"
|
||||||
|
| "PT"
|
||||||
|
| "PW"
|
||||||
|
| "PY"
|
||||||
|
| "QA"
|
||||||
|
| "RE"
|
||||||
|
| "RO"
|
||||||
|
| "RS"
|
||||||
|
| "RU"
|
||||||
|
| "RW"
|
||||||
|
| "SA"
|
||||||
|
| "SB"
|
||||||
|
| "SC"
|
||||||
|
| "SD"
|
||||||
|
| "SE"
|
||||||
|
| "SG"
|
||||||
|
| "SH"
|
||||||
|
| "SI"
|
||||||
|
| "SJ"
|
||||||
|
| "SK"
|
||||||
|
| "SL"
|
||||||
|
| "SM"
|
||||||
|
| "SN"
|
||||||
|
| "SO"
|
||||||
|
| "SR"
|
||||||
|
| "SS"
|
||||||
|
| "ST"
|
||||||
|
| "SV"
|
||||||
|
| "SX"
|
||||||
|
| "SY"
|
||||||
|
| "SZ"
|
||||||
|
| "TC"
|
||||||
|
| "TD"
|
||||||
|
| "TF"
|
||||||
|
| "TG"
|
||||||
|
| "TH"
|
||||||
|
| "TJ"
|
||||||
|
| "TK"
|
||||||
|
| "TL"
|
||||||
|
| "TM"
|
||||||
|
| "TN"
|
||||||
|
| "TO"
|
||||||
|
| "TR"
|
||||||
|
| "TT"
|
||||||
|
| "TV"
|
||||||
|
| "TZ"
|
||||||
|
| "UA"
|
||||||
|
| "UG"
|
||||||
|
| "UM"
|
||||||
|
| "US"
|
||||||
|
| "UY"
|
||||||
|
| "UZ"
|
||||||
|
| "VA"
|
||||||
|
| "VC"
|
||||||
|
| "VE"
|
||||||
|
| "VG"
|
||||||
|
| "VI"
|
||||||
|
| "VN"
|
||||||
|
| "VU"
|
||||||
|
| "WF"
|
||||||
|
| "WS"
|
||||||
|
| "YE"
|
||||||
|
| "YT"
|
||||||
|
| "ZA"
|
||||||
|
| "ZM"
|
||||||
|
| "ZW";
|
||||||
|
|
||||||
|
export type ExtractColorKeys<T> = T extends { styles: infer C } ? keyof C : never;
|
||||||
|
export type ExtractBadgeKeys<T> = keyof T;
|
||||||
|
export type BadgeTypeToColorMap<T> = {
|
||||||
|
[K in ExtractBadgeKeys<T>]: ExtractColorKeys<T[K]>;
|
||||||
|
};
|
||||||
|
export type BadgeTypeColors<T> = ExtractColorKeys<T[keyof T]>;
|
||||||
|
|
||||||
|
export const badgeTypes = {
|
||||||
|
pillColor: "pill-color",
|
||||||
|
badgeColor: "color",
|
||||||
|
badgeModern: "modern",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type BadgeTypes = (typeof badgeTypes)[keyof typeof badgeTypes];
|
||||||
415
src/components/base/badges/badges.tsx
Normal file
415
src/components/base/badges/badges.tsx
Normal file
@@ -0,0 +1,415 @@
|
|||||||
|
import type { MouseEventHandler, ReactNode } from "react";
|
||||||
|
import { X as CloseX } from "@untitledui/icons";
|
||||||
|
import { Dot } from "@/components/foundations/dot-icon";
|
||||||
|
import { cx } from "@/utils/cx";
|
||||||
|
import type { BadgeColors, BadgeTypeToColorMap, BadgeTypes, FlagTypes, IconComponentType, Sizes } from "./badge-types";
|
||||||
|
import { badgeTypes } from "./badge-types";
|
||||||
|
|
||||||
|
export const filledColors: Record<BadgeColors, { root: string; addon: string; addonButton: string }> = {
|
||||||
|
gray: {
|
||||||
|
root: "bg-utility-gray-50 text-utility-gray-700 ring-utility-gray-200",
|
||||||
|
addon: "text-utility-gray-500",
|
||||||
|
addonButton: "hover:bg-utility-gray-100 text-utility-gray-400 hover:text-utility-gray-500",
|
||||||
|
},
|
||||||
|
brand: {
|
||||||
|
root: "bg-utility-brand-50 text-utility-brand-700 ring-utility-brand-200",
|
||||||
|
addon: "text-utility-brand-500",
|
||||||
|
addonButton: "hover:bg-utility-brand-100 text-utility-brand-400 hover:text-utility-brand-500",
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
root: "bg-utility-error-50 text-utility-error-700 ring-utility-error-200",
|
||||||
|
addon: "text-utility-error-500",
|
||||||
|
addonButton: "hover:bg-utility-error-100 text-utility-error-400 hover:text-utility-error-500",
|
||||||
|
},
|
||||||
|
warning: {
|
||||||
|
root: "bg-utility-warning-50 text-utility-warning-700 ring-utility-warning-200",
|
||||||
|
addon: "text-utility-warning-500",
|
||||||
|
addonButton: "hover:bg-utility-warning-100 text-utility-warning-400 hover:text-utility-warning-500",
|
||||||
|
},
|
||||||
|
success: {
|
||||||
|
root: "bg-utility-success-50 text-utility-success-700 ring-utility-success-200",
|
||||||
|
addon: "text-utility-success-500",
|
||||||
|
addonButton: "hover:bg-utility-success-100 text-utility-success-400 hover:text-utility-success-500",
|
||||||
|
},
|
||||||
|
"gray-blue": {
|
||||||
|
root: "bg-utility-gray-blue-50 text-utility-gray-blue-700 ring-utility-gray-blue-200",
|
||||||
|
addon: "text-utility-gray-blue-500",
|
||||||
|
addonButton: "hover:bg-utility-gray-blue-100 text-utility-gray-blue-400 hover:text-utility-gray-blue-500",
|
||||||
|
},
|
||||||
|
"blue-light": {
|
||||||
|
root: "bg-utility-blue-light-50 text-utility-blue-light-700 ring-utility-blue-light-200",
|
||||||
|
addon: "text-utility-blue-light-500",
|
||||||
|
addonButton: "hover:bg-utility-blue-light-100 text-utility-blue-light-400 hover:text-utility-blue-light-500",
|
||||||
|
},
|
||||||
|
blue: {
|
||||||
|
root: "bg-utility-blue-50 text-utility-blue-700 ring-utility-blue-200",
|
||||||
|
addon: "text-utility-blue-500",
|
||||||
|
addonButton: "hover:bg-utility-blue-100 text-utility-blue-400 hover:text-utility-blue-500",
|
||||||
|
},
|
||||||
|
indigo: {
|
||||||
|
root: "bg-utility-indigo-50 text-utility-indigo-700 ring-utility-indigo-200",
|
||||||
|
addon: "text-utility-indigo-500",
|
||||||
|
addonButton: "hover:bg-utility-indigo-100 text-utility-indigo-400 hover:text-utility-indigo-500",
|
||||||
|
},
|
||||||
|
purple: {
|
||||||
|
root: "bg-utility-purple-50 text-utility-purple-700 ring-utility-purple-200",
|
||||||
|
addon: "text-utility-purple-500",
|
||||||
|
addonButton: "hover:bg-utility-purple-100 text-utility-purple-400 hover:text-utility-purple-500",
|
||||||
|
},
|
||||||
|
pink: {
|
||||||
|
root: "bg-utility-pink-50 text-utility-pink-700 ring-utility-pink-200",
|
||||||
|
addon: "text-utility-pink-500",
|
||||||
|
addonButton: "hover:bg-utility-pink-100 text-utility-pink-400 hover:text-utility-pink-500",
|
||||||
|
},
|
||||||
|
orange: {
|
||||||
|
root: "bg-utility-orange-50 text-utility-orange-700 ring-utility-orange-200",
|
||||||
|
addon: "text-utility-orange-500",
|
||||||
|
addonButton: "hover:bg-utility-orange-100 text-utility-orange-400 hover:text-utility-orange-500",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const addonOnlyColors = Object.fromEntries(Object.entries(filledColors).map(([key, value]) => [key, { root: "", addon: value.addon }])) as Record<
|
||||||
|
BadgeColors,
|
||||||
|
{ root: string; addon: string }
|
||||||
|
>;
|
||||||
|
|
||||||
|
const withPillTypes = {
|
||||||
|
[badgeTypes.pillColor]: {
|
||||||
|
common: "size-max flex items-center whitespace-nowrap rounded-full ring-1 ring-inset",
|
||||||
|
styles: filledColors,
|
||||||
|
},
|
||||||
|
[badgeTypes.badgeColor]: {
|
||||||
|
common: "size-max flex items-center whitespace-nowrap rounded-md ring-1 ring-inset",
|
||||||
|
styles: filledColors,
|
||||||
|
},
|
||||||
|
[badgeTypes.badgeModern]: {
|
||||||
|
common: "size-max flex items-center whitespace-nowrap rounded-md ring-1 ring-inset shadow-xs",
|
||||||
|
styles: {
|
||||||
|
gray: {
|
||||||
|
root: "bg-primary text-secondary ring-primary",
|
||||||
|
addon: "text-gray-500",
|
||||||
|
addonButton: "hover:bg-utility-gray-100 text-utility-gray-400 hover:text-utility-gray-500",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const withBadgeTypes = {
|
||||||
|
[badgeTypes.pillColor]: {
|
||||||
|
common: "size-max flex items-center whitespace-nowrap rounded-full ring-1 ring-inset",
|
||||||
|
styles: filledColors,
|
||||||
|
},
|
||||||
|
[badgeTypes.badgeColor]: {
|
||||||
|
common: "size-max flex items-center whitespace-nowrap rounded-md ring-1 ring-inset",
|
||||||
|
styles: filledColors,
|
||||||
|
},
|
||||||
|
[badgeTypes.badgeModern]: {
|
||||||
|
common: "size-max flex items-center whitespace-nowrap rounded-md ring-1 ring-inset bg-primary text-secondary ring-primary shadow-xs",
|
||||||
|
styles: addonOnlyColors,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BadgeColor<T extends BadgeTypes> = BadgeTypeToColorMap<typeof withPillTypes>[T];
|
||||||
|
|
||||||
|
interface BadgeProps<T extends BadgeTypes> {
|
||||||
|
type?: T;
|
||||||
|
size?: Sizes;
|
||||||
|
color?: BadgeColor<T>;
|
||||||
|
children: ReactNode;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Badge = <T extends BadgeTypes>(props: BadgeProps<T>) => {
|
||||||
|
const { type = "pill-color", size = "md", color = "gray", children } = props;
|
||||||
|
const colors = withPillTypes[type];
|
||||||
|
|
||||||
|
const pillSizes = {
|
||||||
|
sm: "py-0.5 px-2 text-xs font-medium",
|
||||||
|
md: "py-0.5 px-2.5 text-sm font-medium",
|
||||||
|
lg: "py-1 px-3 text-sm font-medium",
|
||||||
|
};
|
||||||
|
const badgeSizes = {
|
||||||
|
sm: "py-0.5 px-1.5 text-xs font-medium",
|
||||||
|
md: "py-0.5 px-2 text-sm font-medium",
|
||||||
|
lg: "py-1 px-2.5 text-sm font-medium rounded-lg",
|
||||||
|
};
|
||||||
|
|
||||||
|
const sizes = {
|
||||||
|
[badgeTypes.pillColor]: pillSizes,
|
||||||
|
[badgeTypes.badgeColor]: badgeSizes,
|
||||||
|
[badgeTypes.badgeModern]: badgeSizes,
|
||||||
|
};
|
||||||
|
|
||||||
|
return <span className={cx(colors.common, sizes[type][size], colors.styles[color].root, props.className)}>{children}</span>;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface BadgeWithDotProps<T extends BadgeTypes> {
|
||||||
|
type?: T;
|
||||||
|
size?: Sizes;
|
||||||
|
color?: BadgeTypeToColorMap<typeof withBadgeTypes>[T];
|
||||||
|
className?: string;
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BadgeWithDot = <T extends BadgeTypes>(props: BadgeWithDotProps<T>) => {
|
||||||
|
const { size = "md", color = "gray", type = "pill-color", className, children } = props;
|
||||||
|
|
||||||
|
const colors = withBadgeTypes[type];
|
||||||
|
|
||||||
|
const pillSizes = {
|
||||||
|
sm: "gap-1 py-0.5 pl-1.5 pr-2 text-xs font-medium",
|
||||||
|
md: "gap-1.5 py-0.5 pl-2 pr-2.5 text-sm font-medium",
|
||||||
|
lg: "gap-1.5 py-1 pl-2.5 pr-3 text-sm font-medium",
|
||||||
|
};
|
||||||
|
|
||||||
|
const badgeSizes = {
|
||||||
|
sm: "gap-1 py-0.5 px-1.5 text-xs font-medium",
|
||||||
|
md: "gap-1.5 py-0.5 px-2 text-sm font-medium",
|
||||||
|
lg: "gap-1.5 py-1 px-2.5 text-sm font-medium rounded-lg",
|
||||||
|
};
|
||||||
|
|
||||||
|
const sizes = {
|
||||||
|
[badgeTypes.pillColor]: pillSizes,
|
||||||
|
[badgeTypes.badgeColor]: badgeSizes,
|
||||||
|
[badgeTypes.badgeModern]: badgeSizes,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className={cx(colors.common, sizes[type][size], colors.styles[color].root, className)}>
|
||||||
|
<Dot className={colors.styles[color].addon} size="sm" />
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface BadgeWithIconProps<T extends BadgeTypes> {
|
||||||
|
type?: T;
|
||||||
|
size?: Sizes;
|
||||||
|
color?: BadgeTypeToColorMap<typeof withBadgeTypes>[T];
|
||||||
|
iconLeading?: IconComponentType;
|
||||||
|
iconTrailing?: IconComponentType;
|
||||||
|
children: ReactNode;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BadgeWithIcon = <T extends BadgeTypes>(props: BadgeWithIconProps<T>) => {
|
||||||
|
const { size = "md", color = "gray", type = "pill-color", iconLeading: IconLeading, iconTrailing: IconTrailing, children, className } = props;
|
||||||
|
|
||||||
|
const colors = withBadgeTypes[type];
|
||||||
|
|
||||||
|
const icon = IconLeading ? "leading" : "trailing";
|
||||||
|
|
||||||
|
const pillSizes = {
|
||||||
|
sm: {
|
||||||
|
trailing: "gap-0.5 py-0.5 pl-2 pr-1.5 text-xs font-medium",
|
||||||
|
leading: "gap-0.5 py-0.5 pr-2 pl-1.5 text-xs font-medium",
|
||||||
|
},
|
||||||
|
md: {
|
||||||
|
trailing: "gap-1 py-0.5 pl-2.5 pr-2 text-sm font-medium",
|
||||||
|
leading: "gap-1 py-0.5 pr-2.5 pl-2 text-sm font-medium",
|
||||||
|
},
|
||||||
|
lg: {
|
||||||
|
trailing: "gap-1 py-1 pl-3 pr-2.5 text-sm font-medium",
|
||||||
|
leading: "gap-1 py-1 pr-3 pl-2.5 text-sm font-medium",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const badgeSizes = {
|
||||||
|
sm: {
|
||||||
|
trailing: "gap-0.5 py-0.5 pl-2 pr-1.5 text-xs font-medium",
|
||||||
|
leading: "gap-0.5 py-0.5 pr-2 pl-1.5 text-xs font-medium",
|
||||||
|
},
|
||||||
|
md: {
|
||||||
|
trailing: "gap-1 py-0.5 pl-2 pr-1.5 text-sm font-medium",
|
||||||
|
leading: "gap-1 py-0.5 pr-2 pl-1.5 text-sm font-medium",
|
||||||
|
},
|
||||||
|
lg: {
|
||||||
|
trailing: "gap-1 py-1 pl-2.5 pr-2 text-sm font-medium rounded-lg",
|
||||||
|
leading: "gap-1 py-1 pr-2.5 pl-2 text-sm font-medium rounded-lg",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const sizes = {
|
||||||
|
[badgeTypes.pillColor]: pillSizes,
|
||||||
|
[badgeTypes.badgeColor]: badgeSizes,
|
||||||
|
[badgeTypes.badgeModern]: badgeSizes,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className={cx(colors.common, sizes[type][size][icon], colors.styles[color].root, className)}>
|
||||||
|
{IconLeading && <IconLeading className={cx(colors.styles[color].addon, "size-3 stroke-3")} />}
|
||||||
|
{children}
|
||||||
|
{IconTrailing && <IconTrailing className={cx(colors.styles[color].addon, "size-3 stroke-3")} />}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface BadgeWithFlagProps<T extends BadgeTypes> {
|
||||||
|
type?: T;
|
||||||
|
size?: Sizes;
|
||||||
|
flag?: FlagTypes;
|
||||||
|
color?: BadgeTypeToColorMap<typeof withPillTypes>[T];
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BadgeWithFlag = <T extends BadgeTypes>(props: BadgeWithFlagProps<T>) => {
|
||||||
|
const { size = "md", color = "gray", flag = "AU", type = "pill-color", children } = props;
|
||||||
|
|
||||||
|
const colors = withPillTypes[type];
|
||||||
|
|
||||||
|
const pillSizes = {
|
||||||
|
sm: "gap-1 py-0.5 pl-0.75 pr-2 text-xs font-medium",
|
||||||
|
md: "gap-1.5 py-0.5 pl-1 pr-2.5 text-sm font-medium",
|
||||||
|
lg: "gap-1.5 py-1 pl-1.5 pr-3 text-sm font-medium",
|
||||||
|
};
|
||||||
|
const badgeSizes = {
|
||||||
|
sm: "gap-1 py-0.5 pl-1 pr-1.5 text-xs font-medium",
|
||||||
|
md: "gap-1.5 py-0.5 pl-1.5 pr-2 text-sm font-medium",
|
||||||
|
lg: "gap-1.5 py-1 pl-2 pr-2.5 text-sm font-medium rounded-lg",
|
||||||
|
};
|
||||||
|
|
||||||
|
const sizes = {
|
||||||
|
[badgeTypes.pillColor]: pillSizes,
|
||||||
|
[badgeTypes.badgeColor]: badgeSizes,
|
||||||
|
[badgeTypes.badgeModern]: badgeSizes,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className={cx(colors.common, sizes[type][size], colors.styles[color].root)}>
|
||||||
|
<img src={`https://www.untitledui.com/images/flags/${flag}.svg`} className="size-4 max-w-none rounded-full" alt={`${flag} flag`} />
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface BadgeWithImageProps<T extends BadgeTypes> {
|
||||||
|
type?: T;
|
||||||
|
size?: Sizes;
|
||||||
|
imgSrc: string;
|
||||||
|
color?: BadgeTypeToColorMap<typeof withPillTypes>[T];
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BadgeWithImage = <T extends BadgeTypes>(props: BadgeWithImageProps<T>) => {
|
||||||
|
const { size = "md", color = "gray", type = "pill-color", imgSrc, children } = props;
|
||||||
|
|
||||||
|
const colors = withPillTypes[type];
|
||||||
|
|
||||||
|
const pillSizes = {
|
||||||
|
sm: "gap-1 py-0.5 pl-0.75 pr-2 text-xs font-medium",
|
||||||
|
md: "gap-1.5 py-0.5 pl-1 pr-2.5 text-sm font-medium",
|
||||||
|
lg: "gap-1.5 py-1 pl-1.5 pr-3 text-sm font-medium",
|
||||||
|
};
|
||||||
|
const badgeSizes = {
|
||||||
|
sm: "gap-1 py-0.5 pl-1 pr-1.5 text-xs font-medium",
|
||||||
|
md: "gap-1.5 py-0.5 pl-1.5 pr-2 text-sm font-medium",
|
||||||
|
lg: "gap-1.5 py-1 pl-2 pr-2.5 text-sm font-medium rounded-lg",
|
||||||
|
};
|
||||||
|
|
||||||
|
const sizes = {
|
||||||
|
[badgeTypes.pillColor]: pillSizes,
|
||||||
|
[badgeTypes.badgeColor]: badgeSizes,
|
||||||
|
[badgeTypes.badgeModern]: badgeSizes,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className={cx(colors.common, sizes[type][size], colors.styles[color].root)}>
|
||||||
|
<img src={imgSrc} className="size-4 max-w-none rounded-full" alt="Badge image" />
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface BadgeWithButtonProps<T extends BadgeTypes> {
|
||||||
|
type?: T;
|
||||||
|
size?: Sizes;
|
||||||
|
icon?: IconComponentType;
|
||||||
|
color?: BadgeTypeToColorMap<typeof withPillTypes>[T];
|
||||||
|
children: ReactNode;
|
||||||
|
/**
|
||||||
|
* The label for the button.
|
||||||
|
*/
|
||||||
|
buttonLabel?: string;
|
||||||
|
/**
|
||||||
|
* The click event handler for the button.
|
||||||
|
*/
|
||||||
|
onButtonClick?: MouseEventHandler<HTMLButtonElement>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BadgeWithButton = <T extends BadgeTypes>(props: BadgeWithButtonProps<T>) => {
|
||||||
|
const { size = "md", color = "gray", type = "pill-color", icon: Icon = CloseX, buttonLabel, children } = props;
|
||||||
|
|
||||||
|
const colors = withPillTypes[type];
|
||||||
|
|
||||||
|
const pillSizes = {
|
||||||
|
sm: "gap-0.5 py-0.5 pl-2 pr-0.75 text-xs font-medium",
|
||||||
|
md: "gap-0.5 py-0.5 pl-2.5 pr-1 text-sm font-medium",
|
||||||
|
lg: "gap-0.5 py-1 pl-3 pr-1.5 text-sm font-medium",
|
||||||
|
};
|
||||||
|
const badgeSizes = {
|
||||||
|
sm: "gap-0.5 py-0.5 pl-1.5 pr-0.75 text-xs font-medium",
|
||||||
|
md: "gap-0.5 py-0.5 pl-2 pr-1 text-sm font-medium",
|
||||||
|
lg: "gap-0.5 py-1 pl-2.5 pr-1.5 text-sm font-medium rounded-lg",
|
||||||
|
};
|
||||||
|
|
||||||
|
const sizes = {
|
||||||
|
[badgeTypes.pillColor]: pillSizes,
|
||||||
|
[badgeTypes.badgeColor]: badgeSizes,
|
||||||
|
[badgeTypes.badgeModern]: badgeSizes,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className={cx(colors.common, sizes[type][size], colors.styles[color].root)}>
|
||||||
|
{children}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
aria-label={buttonLabel}
|
||||||
|
onClick={props.onButtonClick}
|
||||||
|
className={cx(
|
||||||
|
"flex cursor-pointer items-center justify-center p-0.5 outline-focus-ring transition duration-100 ease-linear focus-visible:outline-2",
|
||||||
|
colors.styles[color].addonButton,
|
||||||
|
type === "pill-color" ? "rounded-full" : "rounded-[3px]",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Icon className="size-3 stroke-[3px] transition-inherit-all" />
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface BadgeIconProps<T extends BadgeTypes> {
|
||||||
|
type?: T;
|
||||||
|
size?: Sizes;
|
||||||
|
icon: IconComponentType;
|
||||||
|
color?: BadgeTypeToColorMap<typeof withPillTypes>[T];
|
||||||
|
children?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BadgeIcon = <T extends BadgeTypes>(props: BadgeIconProps<T>) => {
|
||||||
|
const { size = "md", color = "gray", type = "pill-color", icon: Icon } = props;
|
||||||
|
|
||||||
|
const colors = withPillTypes[type];
|
||||||
|
|
||||||
|
const pillSizes = {
|
||||||
|
sm: "p-1.25",
|
||||||
|
md: "p-1.5",
|
||||||
|
lg: "p-2",
|
||||||
|
};
|
||||||
|
|
||||||
|
const badgeSizes = {
|
||||||
|
sm: "p-1.25",
|
||||||
|
md: "p-1.5",
|
||||||
|
lg: "p-2 rounded-lg",
|
||||||
|
};
|
||||||
|
|
||||||
|
const sizes = {
|
||||||
|
[badgeTypes.pillColor]: pillSizes,
|
||||||
|
[badgeTypes.badgeColor]: badgeSizes,
|
||||||
|
[badgeTypes.badgeModern]: badgeSizes,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className={cx(colors.common, sizes[type][size], colors.styles[color].root)}>
|
||||||
|
<Icon className={cx("size-3 stroke-[3px]", colors.styles[color].addon)} />
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
104
src/components/base/button-group/button-group.tsx
Normal file
104
src/components/base/button-group/button-group.tsx
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
import { type FC, type PropsWithChildren, type ReactNode, type RefAttributes, createContext, isValidElement, useContext } from "react";
|
||||||
|
import {
|
||||||
|
ToggleButton as AriaToggleButton,
|
||||||
|
ToggleButtonGroup as AriaToggleButtonGroup,
|
||||||
|
type ToggleButtonGroupProps,
|
||||||
|
type ToggleButtonProps,
|
||||||
|
} from "react-aria-components";
|
||||||
|
import { cx, sortCx } from "@/utils/cx";
|
||||||
|
import { isReactComponent } from "@/utils/is-react-component";
|
||||||
|
|
||||||
|
export const styles = sortCx({
|
||||||
|
common: {
|
||||||
|
root: [
|
||||||
|
"group/button-group inline-flex h-max cursor-pointer items-center bg-primary font-semibold whitespace-nowrap text-secondary shadow-skeumorphic ring-1 ring-primary outline-brand transition duration-100 ease-linear ring-inset",
|
||||||
|
// Hover and focus styles
|
||||||
|
"hover:bg-primary_hover hover:text-secondary_hover focus-visible:z-10 focus-visible:outline-2 focus-visible:outline-offset-2",
|
||||||
|
// Disabled styles
|
||||||
|
"disabled:cursor-not-allowed disabled:bg-primary disabled:text-disabled",
|
||||||
|
// Selected styles
|
||||||
|
"selected:bg-active selected:text-secondary_hover selected:disabled:bg-disabled_subtle",
|
||||||
|
].join(" "),
|
||||||
|
icon: "pointer-events-none text-fg-quaternary transition-[inherit] group-hover/button-group:text-fg-quaternary_hover group-disabled/button-group:text-fg-disabled_subtle",
|
||||||
|
},
|
||||||
|
|
||||||
|
sizes: {
|
||||||
|
sm: {
|
||||||
|
root: "gap-1.5 px-3.5 py-2 text-sm not-last:pr-[calc(calc(var(--spacing)*3.5)+1px)] first:rounded-l-lg last:rounded-r-lg data-icon-leading:pl-3 data-icon-only:p-2",
|
||||||
|
icon: "size-5",
|
||||||
|
},
|
||||||
|
md: {
|
||||||
|
root: "gap-1.5 px-4 py-2.5 text-sm not-last:pr-[calc(calc(var(--spacing)*4)+1px)] first:rounded-l-lg last:rounded-r-lg data-icon-leading:pl-3.5 data-icon-only:px-3",
|
||||||
|
icon: "size-5",
|
||||||
|
},
|
||||||
|
lg: {
|
||||||
|
root: "gap-2 px-4.5 py-2.5 text-md not-last:pr-[calc(calc(var(--spacing)*4.5)+1px)] first:rounded-l-lg last:rounded-r-lg data-icon-leading:pl-4 data-icon-only:p-3",
|
||||||
|
icon: "size-5",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
type ButtonSize = keyof typeof styles.sizes;
|
||||||
|
|
||||||
|
const ButtonGroupContext = createContext<{ size: ButtonSize }>({ size: "md" });
|
||||||
|
|
||||||
|
interface ButtonGroupItemProps extends ToggleButtonProps, RefAttributes<HTMLButtonElement> {
|
||||||
|
iconLeading?: FC<{ className?: string }> | ReactNode;
|
||||||
|
iconTrailing?: FC<{ className?: string }> | ReactNode;
|
||||||
|
onClick?: () => void;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ButtonGroupItem = ({
|
||||||
|
iconLeading: IconLeading,
|
||||||
|
iconTrailing: IconTrailing,
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
...otherProps
|
||||||
|
}: PropsWithChildren<ButtonGroupItemProps>) => {
|
||||||
|
const context = useContext(ButtonGroupContext);
|
||||||
|
|
||||||
|
if (!context) {
|
||||||
|
throw new Error("ButtonGroupItem must be used within a ButtonGroup component");
|
||||||
|
}
|
||||||
|
|
||||||
|
const { size } = context;
|
||||||
|
|
||||||
|
const isIcon = (IconLeading || IconTrailing) && !children;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AriaToggleButton
|
||||||
|
{...otherProps}
|
||||||
|
data-icon-only={isIcon ? true : undefined}
|
||||||
|
data-icon-leading={IconLeading ? true : undefined}
|
||||||
|
className={cx(styles.common.root, styles.sizes[size].root, className)}
|
||||||
|
>
|
||||||
|
{isReactComponent(IconLeading) && <IconLeading className={cx(styles.common.icon, styles.sizes[size].icon)} />}
|
||||||
|
{isValidElement(IconLeading) && IconLeading}
|
||||||
|
|
||||||
|
{children}
|
||||||
|
|
||||||
|
{isReactComponent(IconTrailing) && <IconTrailing className={cx(styles.common.icon, styles.sizes[size].icon)} />}
|
||||||
|
{isValidElement(IconTrailing) && IconTrailing}
|
||||||
|
</AriaToggleButton>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ButtonGroupProps extends Omit<ToggleButtonGroupProps, "orientation">, RefAttributes<HTMLDivElement> {
|
||||||
|
size?: ButtonSize;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ButtonGroup = ({ children, size = "md", className, ...otherProps }: ButtonGroupProps) => {
|
||||||
|
return (
|
||||||
|
<ButtonGroupContext.Provider value={{ size }}>
|
||||||
|
<AriaToggleButtonGroup
|
||||||
|
selectionMode="single"
|
||||||
|
className={cx("relative z-0 inline-flex w-max -space-x-px rounded-lg shadow-xs", className)}
|
||||||
|
{...otherProps}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</AriaToggleButtonGroup>
|
||||||
|
</ButtonGroupContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
376
src/components/base/buttons/app-store-buttons-outline.tsx
Normal file
376
src/components/base/buttons/app-store-buttons-outline.tsx
Normal file
@@ -0,0 +1,376 @@
|
|||||||
|
import type { AnchorHTMLAttributes } from "react";
|
||||||
|
import { cx } from "@/utils/cx";
|
||||||
|
|
||||||
|
export const GooglePlayButton = ({ size = "md", ...props }: AnchorHTMLAttributes<HTMLAnchorElement> & { size?: "md" | "lg" }) => {
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
aria-label="Get it on Google Play"
|
||||||
|
href="#"
|
||||||
|
{...props}
|
||||||
|
className={cx(
|
||||||
|
"rounded-[7px] text-fg-primary ring-1 ring-fg-primary outline-focus-ring ring-inset focus-visible:outline-2 focus-visible:outline-offset-2",
|
||||||
|
props.className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<svg width={size === "md" ? 135 : 149} height={size === "md" ? 40 : 44} viewBox="0 0 135 40" fill="none">
|
||||||
|
<path
|
||||||
|
d="M68.136 21.7511C65.784 21.7511 63.867 23.5401 63.867 26.0041C63.867 28.4531 65.784 30.2571 68.136 30.2571C70.489 30.2571 72.406 28.4531 72.406 26.0041C72.405 23.5401 70.488 21.7511 68.136 21.7511ZM68.136 28.5831C66.847 28.5831 65.736 27.5201 65.736 26.0051C65.736 24.4741 66.848 23.4271 68.136 23.4271C69.425 23.4271 70.536 24.4741 70.536 26.0051C70.536 27.5191 69.425 28.5831 68.136 28.5831ZM58.822 21.7511C56.47 21.7511 54.553 23.5401 54.553 26.0041C54.553 28.4531 56.47 30.2571 58.822 30.2571C61.175 30.2571 63.092 28.4531 63.092 26.0041C63.092 23.5401 61.175 21.7511 58.822 21.7511ZM58.822 28.5831C57.533 28.5831 56.422 27.5201 56.422 26.0051C56.422 24.4741 57.534 23.4271 58.822 23.4271C60.111 23.4271 61.222 24.4741 61.222 26.0051C61.223 27.5191 60.111 28.5831 58.822 28.5831ZM47.744 23.0571V24.8611H52.062C51.933 25.8761 51.595 26.6171 51.079 27.1321C50.451 27.7601 49.468 28.4531 47.744 28.4531C45.086 28.4531 43.008 26.3101 43.008 23.6521C43.008 20.9941 45.086 18.8511 47.744 18.8511C49.178 18.8511 50.225 19.4151 50.998 20.1401L52.271 18.8671C51.191 17.8361 49.758 17.0471 47.744 17.0471C44.103 17.0471 41.042 20.0111 41.042 23.6521C41.042 27.2931 44.103 30.2571 47.744 30.2571C49.709 30.2571 51.192 29.6121 52.351 28.4041C53.543 27.2121 53.914 25.5361 53.914 24.1831C53.914 23.7651 53.882 23.3781 53.817 23.0561H47.744V23.0571ZM93.052 24.4581C92.698 23.5081 91.618 21.7511 89.411 21.7511C87.22 21.7511 85.399 23.4751 85.399 26.0041C85.399 28.3881 87.204 30.2571 89.62 30.2571C91.569 30.2571 92.697 29.0651 93.165 28.3721L91.715 27.4051C91.232 28.1141 90.571 28.5811 89.62 28.5811C88.67 28.5811 87.993 28.1461 87.558 27.2921L93.245 24.9401L93.052 24.4581ZM87.252 25.8761C87.204 24.2321 88.525 23.3951 89.476 23.3951C90.217 23.3951 90.845 23.7661 91.055 24.2971L87.252 25.8761ZM82.629 30.0001H84.497V17.4991H82.629V30.0001ZM79.567 22.7021H79.503C79.084 22.2021 78.278 21.7511 77.264 21.7511C75.137 21.7511 73.188 23.6201 73.188 26.0211C73.188 28.4051 75.137 30.2581 77.264 30.2581C78.279 30.2581 79.084 29.8071 79.503 29.2921H79.567V29.9041C79.567 31.5311 78.697 32.4011 77.296 32.4011C76.152 32.4011 75.443 31.5801 75.153 30.8871L73.526 31.5641C73.993 32.6911 75.233 34.0771 77.296 34.0771C79.487 34.0771 81.34 32.7881 81.34 29.6461V22.0101H79.568V22.7021H79.567ZM77.425 28.5831C76.136 28.5831 75.057 27.5031 75.057 26.0211C75.057 24.5221 76.136 23.4271 77.425 23.4271C78.697 23.4271 79.696 24.5221 79.696 26.0211C79.696 27.5031 78.697 28.5831 77.425 28.5831ZM101.806 17.4991H97.335V30.0001H99.2V25.2641H101.805C103.873 25.2641 105.907 23.7671 105.907 21.3821C105.907 18.9971 103.874 17.4991 101.806 17.4991ZM101.854 23.5241H99.2V19.2391H101.854C103.249 19.2391 104.041 20.3941 104.041 21.3821C104.041 22.3501 103.249 23.5241 101.854 23.5241ZM113.386 21.7291C112.035 21.7291 110.636 22.3241 110.057 23.6431L111.713 24.3341C112.067 23.6431 112.727 23.4171 113.418 23.4171C114.383 23.4171 115.364 23.9961 115.38 25.0251V25.1541C115.042 24.9611 114.318 24.6721 113.434 24.6721C111.649 24.6721 109.831 25.6531 109.831 27.4861C109.831 29.1591 111.295 30.2361 112.935 30.2361C114.189 30.2361 114.881 29.6731 115.315 29.0131H115.379V29.9781H117.181V25.1851C117.182 22.9671 115.524 21.7291 113.386 21.7291ZM113.16 28.5801C112.55 28.5801 111.697 28.2741 111.697 27.5181C111.697 26.5531 112.759 26.1831 113.676 26.1831C114.495 26.1831 114.882 26.3601 115.38 26.6011C115.235 27.7601 114.238 28.5801 113.16 28.5801ZM123.743 22.0021L121.604 27.4221H121.54L119.32 22.0021H117.31L120.639 29.5771L118.741 33.7911H120.687L125.818 22.0021H123.743ZM106.937 30.0001H108.802V17.4991H106.937V30.0001Z"
|
||||||
|
className="fill-current"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M47.418 10.2429C47.418 11.0809 47.1701 11.7479 46.673 12.2459C46.109 12.8379 45.3731 13.1339 44.4691 13.1339C43.6031 13.1339 42.8661 12.8339 42.2611 12.2339C41.6551 11.6329 41.3521 10.8889 41.3521 10.0009C41.3521 9.11194 41.6551 8.36794 42.2611 7.76794C42.8661 7.16694 43.6031 6.86694 44.4691 6.86694C44.8991 6.86694 45.3101 6.95094 45.7001 7.11794C46.0911 7.28594 46.404 7.50894 46.6381 7.78794L46.111 8.31594C45.714 7.84094 45.167 7.60394 44.468 7.60394C43.836 7.60394 43.29 7.82594 42.829 8.26994C42.368 8.71394 42.1381 9.29094 42.1381 9.99994C42.1381 10.7089 42.368 11.2859 42.829 11.7299C43.29 12.1739 43.836 12.3959 44.468 12.3959C45.138 12.3959 45.6971 12.1729 46.1441 11.7259C46.4341 11.4349 46.602 11.0299 46.647 10.5109H44.468V9.78994H47.375C47.405 9.94694 47.418 10.0979 47.418 10.2429Z"
|
||||||
|
className="fill-current"
|
||||||
|
/>
|
||||||
|
<path d="M52.0281 7.737H49.2961V9.639H51.7601V10.36H49.2961V12.262H52.0281V13H48.5251V7H52.0281V7.737Z" className="fill-current" />
|
||||||
|
<path d="M55.279 13H54.508V7.737H52.832V7H56.955V7.737H55.279V13Z" className="fill-current" />
|
||||||
|
<path d="M59.938 13V7H60.709V13H59.938Z" className="fill-current" />
|
||||||
|
<path d="M64.1281 13H63.3572V7.737H61.6812V7H65.8042V7.737H64.1281V13Z" className="fill-current" />
|
||||||
|
<path
|
||||||
|
d="M73.6089 12.225C73.0189 12.831 72.2859 13.134 71.4089 13.134C70.5319 13.134 69.7989 12.831 69.2099 12.225C68.6199 11.619 68.3259 10.877 68.3259 9.99999C68.3259 9.12299 68.6199 8.38099 69.2099 7.77499C69.7989 7.16899 70.5319 6.86499 71.4089 6.86499C72.2809 6.86499 73.0129 7.16999 73.6049 7.77899C74.1969 8.38799 74.4929 9.12799 74.4929 9.99999C74.4929 10.877 74.1979 11.619 73.6089 12.225ZM69.7789 11.722C70.2229 12.172 70.7659 12.396 71.4089 12.396C72.0519 12.396 72.5959 12.171 73.0389 11.722C73.4829 11.272 73.7059 10.698 73.7059 9.99999C73.7059 9.30199 73.4829 8.72799 73.0389 8.27799C72.5959 7.82799 72.0519 7.60399 71.4089 7.60399C70.7659 7.60399 70.2229 7.82899 69.7789 8.27799C69.3359 8.72799 69.1129 9.30199 69.1129 9.99999C69.1129 10.698 69.3359 11.272 69.7789 11.722Z"
|
||||||
|
className="fill-current"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M75.5749 13V7H76.513L79.429 11.667H79.4619L79.429 10.511V7H80.1999V13H79.3949L76.344 8.106H76.3109L76.344 9.262V13H75.5749Z"
|
||||||
|
className="fill-current"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M47.418 10.2429C47.418 11.0809 47.1701 11.7479 46.673 12.2459C46.109 12.8379 45.3731 13.1339 44.4691 13.1339C43.6031 13.1339 42.8661 12.8339 42.2611 12.2339C41.6551 11.6329 41.3521 10.8889 41.3521 10.0009C41.3521 9.11194 41.6551 8.36794 42.2611 7.76794C42.8661 7.16694 43.6031 6.86694 44.4691 6.86694C44.8991 6.86694 45.3101 6.95094 45.7001 7.11794C46.0911 7.28594 46.404 7.50894 46.6381 7.78794L46.111 8.31594C45.714 7.84094 45.167 7.60394 44.468 7.60394C43.836 7.60394 43.29 7.82594 42.829 8.26994C42.368 8.71394 42.1381 9.29094 42.1381 9.99994C42.1381 10.7089 42.368 11.2859 42.829 11.7299C43.29 12.1739 43.836 12.3959 44.468 12.3959C45.138 12.3959 45.6971 12.1729 46.1441 11.7259C46.4341 11.4349 46.602 11.0299 46.647 10.5109H44.468V9.78994H47.375C47.405 9.94694 47.418 10.0979 47.418 10.2429Z"
|
||||||
|
className="stroke-current"
|
||||||
|
strokeWidth="0.2"
|
||||||
|
strokeMiterlimit="10"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M52.0281 7.737H49.2961V9.639H51.7601V10.36H49.2961V12.262H52.0281V13H48.5251V7H52.0281V7.737Z"
|
||||||
|
className="stroke-current"
|
||||||
|
strokeWidth="0.2"
|
||||||
|
strokeMiterlimit="10"
|
||||||
|
/>
|
||||||
|
<path d="M55.279 13H54.508V7.737H52.832V7H56.955V7.737H55.279V13Z" className="stroke-current" strokeWidth="0.2" strokeMiterlimit="10" />
|
||||||
|
<path d="M59.938 13V7H60.709V13H59.938Z" className="stroke-current" strokeWidth="0.2" strokeMiterlimit="10" />
|
||||||
|
<path d="M64.1281 13H63.3572V7.737H61.6812V7H65.8042V7.737H64.1281V13Z" className="stroke-current" strokeWidth="0.2" strokeMiterlimit="10" />
|
||||||
|
<path
|
||||||
|
d="M73.6089 12.225C73.0189 12.831 72.2859 13.134 71.4089 13.134C70.5319 13.134 69.7989 12.831 69.2099 12.225C68.6199 11.619 68.3259 10.877 68.3259 9.99999C68.3259 9.12299 68.6199 8.38099 69.2099 7.77499C69.7989 7.16899 70.5319 6.86499 71.4089 6.86499C72.2809 6.86499 73.0129 7.16999 73.6049 7.77899C74.1969 8.38799 74.4929 9.12799 74.4929 9.99999C74.4929 10.877 74.1979 11.619 73.6089 12.225ZM69.7789 11.722C70.2229 12.172 70.7659 12.396 71.4089 12.396C72.0519 12.396 72.5959 12.171 73.0389 11.722C73.4829 11.272 73.7059 10.698 73.7059 9.99999C73.7059 9.30199 73.4829 8.72799 73.0389 8.27799C72.5959 7.82799 72.0519 7.60399 71.4089 7.60399C70.7659 7.60399 70.2229 7.82899 69.7789 8.27799C69.3359 8.72799 69.1129 9.30199 69.1129 9.99999C69.1129 10.698 69.3359 11.272 69.7789 11.722Z"
|
||||||
|
className="stroke-current"
|
||||||
|
strokeWidth="0.2"
|
||||||
|
strokeMiterlimit="10"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M75.5749 13V7H76.513L79.429 11.667H79.4619L79.429 10.511V7H80.1999V13H79.3949L76.344 8.106H76.3109L76.344 9.262V13H75.5749Z"
|
||||||
|
className="stroke-current"
|
||||||
|
strokeWidth="0.2"
|
||||||
|
strokeMiterlimit="10"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M10.1565 7.96648C10.0384 8.23398 9.97314 8.56174 9.97314 8.943V31.059C9.97314 31.4411 10.0385 31.7689 10.1567 32.0363L22.1907 20.0006L10.1565 7.96648ZM10.8517 32.7555C11.2978 32.9464 11.8797 32.8858 12.5141 32.526L26.6712 24.4812L22.8978 20.7077L10.8517 32.7555ZM27.5737 23.9695L32.0151 21.446C33.4121 20.651 33.4121 19.352 32.0151 18.558L27.5717 16.0331L23.6048 20.0006L27.5737 23.9695ZM26.6699 15.5207L12.5141 7.47701C11.8796 7.11643 11.2977 7.05626 10.8516 7.2474L22.8977 19.2935L26.6699 15.5207Z"
|
||||||
|
className="fill-current"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AppStoreButton = ({ size = "md", ...props }: AnchorHTMLAttributes<HTMLAnchorElement> & { size?: "md" | "lg" }) => {
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
aria-label="Download on the App Store"
|
||||||
|
href="#"
|
||||||
|
{...props}
|
||||||
|
className={cx(
|
||||||
|
"rounded-[7px] text-fg-primary ring-1 ring-fg-primary outline-focus-ring ring-inset focus-visible:outline-2 focus-visible:outline-offset-2",
|
||||||
|
props.className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<svg width={size === "md" ? 120 : 132} height={size === "md" ? 40 : 44} viewBox="0 0 120 40" fill="none">
|
||||||
|
<path
|
||||||
|
d="M81.5257 19.2009V21.4919H80.0896V22.9944H81.5257V28.0994C81.5257 29.8425 82.3143 30.5398 84.2981 30.5398C84.6468 30.5398 84.9788 30.4983 85.2693 30.4485V28.9626C85.0203 28.9875 84.8626 29.0041 84.5887 29.0041C83.7005 29.0041 83.3104 28.5891 83.3104 27.6428V22.9944H85.2693V21.4919H83.3104V19.2009H81.5257Z"
|
||||||
|
className="fill-current"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M90.3232 30.6643C92.9628 30.6643 94.5815 28.8962 94.5815 25.9661C94.5815 23.0525 92.9545 21.2761 90.3232 21.2761C87.6835 21.2761 86.0566 23.0525 86.0566 25.9661C86.0566 28.8962 87.6752 30.6643 90.3232 30.6643ZM90.3232 29.0789C88.7709 29.0789 87.8994 27.9416 87.8994 25.9661C87.8994 24.0071 88.7709 22.8616 90.3232 22.8616C91.8671 22.8616 92.747 24.0071 92.747 25.9661C92.747 27.9333 91.8671 29.0789 90.3232 29.0789Z"
|
||||||
|
className="fill-current"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M95.9664 30.49H97.7511V25.1526C97.7511 23.8826 98.7056 23.0276 100.059 23.0276C100.374 23.0276 100.905 23.0857 101.055 23.1355V21.3757C100.864 21.3259 100.524 21.301 100.258 21.301C99.0792 21.301 98.0748 21.9485 97.8175 22.8367H97.6846V21.4504H95.9664V30.49Z"
|
||||||
|
className="fill-current"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M105.486 22.7952C106.806 22.7952 107.669 23.7165 107.711 25.136H103.145C103.245 23.7248 104.166 22.7952 105.486 22.7952ZM107.702 28.0496C107.37 28.7551 106.632 29.1453 105.552 29.1453C104.125 29.1453 103.203 28.1409 103.145 26.5554V26.4558H109.529V25.8332C109.529 22.9944 108.009 21.2761 105.494 21.2761C102.946 21.2761 101.327 23.1106 101.327 25.9993C101.327 28.8879 102.913 30.6643 105.503 30.6643C107.57 30.6643 109.014 29.6682 109.421 28.0496H107.702Z"
|
||||||
|
className="fill-current"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M69.8221 27.1518C69.9598 29.3715 71.8095 30.7911 74.5626 30.7911C77.505 30.7911 79.3462 29.3027 79.3462 26.9281C79.3462 25.0612 78.2966 24.0287 75.7499 23.4351L74.382 23.0996C72.7645 22.721 72.1106 22.2134 72.1106 21.3272C72.1106 20.2088 73.1259 19.4775 74.6487 19.4775C76.0941 19.4775 77.0921 20.1916 77.2727 21.3358H79.1483C79.0365 19.2452 77.1953 17.774 74.6745 17.774C71.9644 17.774 70.1576 19.2452 70.1576 21.4563C70.1576 23.2802 71.1815 24.3643 73.427 24.8891L75.0272 25.2763C76.6705 25.6634 77.3932 26.2312 77.3932 27.1776C77.3932 28.2789 76.2575 29.079 74.7089 29.079C73.0484 29.079 71.8955 28.3305 71.7321 27.1518H69.8221Z"
|
||||||
|
className="fill-current"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M51.3348 21.301C50.1063 21.301 49.0437 21.9153 48.4959 22.9446H48.3631V21.4504H46.6448V33.4949H48.4295V29.1204H48.5706C49.0437 30.0749 50.0647 30.6394 51.3514 30.6394C53.6341 30.6394 55.0867 28.8381 55.0867 25.9661C55.0867 23.094 53.6341 21.301 51.3348 21.301ZM50.8284 29.0373C49.3343 29.0373 48.3963 27.8586 48.3963 25.9744C48.3963 24.0818 49.3343 22.9031 50.8367 22.9031C52.3475 22.9031 53.2522 24.0569 53.2522 25.9661C53.2522 27.8835 52.3475 29.0373 50.8284 29.0373Z"
|
||||||
|
className="fill-current"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M61.3316 21.301C60.103 21.301 59.0405 21.9153 58.4927 22.9446H58.3599V21.4504H56.6416V33.4949H58.4263V29.1204H58.5674C59.0405 30.0749 60.0615 30.6394 61.3482 30.6394C63.6309 30.6394 65.0835 28.8381 65.0835 25.9661C65.0835 23.094 63.6309 21.301 61.3316 21.301ZM60.8252 29.0373C59.3311 29.0373 58.3931 27.8586 58.3931 25.9744C58.3931 24.0818 59.3311 22.9031 60.8335 22.9031C62.3443 22.9031 63.249 24.0569 63.249 25.9661C63.249 27.8835 62.3443 29.0373 60.8252 29.0373Z"
|
||||||
|
className="fill-current"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M43.4428 30.49H45.4905L41.008 18.0751H38.9346L34.4521 30.49H36.431L37.5752 27.1948H42.3072L43.4428 30.49ZM39.8724 20.3292H40.0186L41.8168 25.5774H38.0656L39.8724 20.3292Z"
|
||||||
|
className="fill-current"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M35.6514 8.71094V14.7H37.8137C39.5984 14.7 40.6318 13.6001 40.6318 11.6868C40.6318 9.80249 39.5901 8.71094 37.8137 8.71094H35.6514ZM36.5811 9.55762H37.71C38.9509 9.55762 39.6855 10.3462 39.6855 11.6992C39.6855 13.073 38.9634 13.8533 37.71 13.8533H36.5811V9.55762Z"
|
||||||
|
className="fill-current"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M43.7969 14.7871C45.1167 14.7871 45.9261 13.9031 45.9261 12.438C45.9261 10.9812 45.1126 10.093 43.7969 10.093C42.4771 10.093 41.6636 10.9812 41.6636 12.438C41.6636 13.9031 42.4729 14.7871 43.7969 14.7871ZM43.7969 13.9944C43.0208 13.9944 42.585 13.4258 42.585 12.438C42.585 11.4585 43.0208 10.8857 43.7969 10.8857C44.5689 10.8857 45.0088 11.4585 45.0088 12.438C45.0088 13.4216 44.5689 13.9944 43.7969 13.9944Z"
|
||||||
|
className="fill-current"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M52.8182 10.1802H51.9259L51.1207 13.6292H51.0501L50.1205 10.1802H49.2655L48.3358 13.6292H48.2694L47.4601 10.1802H46.5553L47.8004 14.7H48.7176L49.6473 11.3713H49.7179L50.6517 14.7H51.5772L52.8182 10.1802Z"
|
||||||
|
className="fill-current"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M53.8458 14.7H54.7382V12.0562C54.7382 11.3506 55.1574 10.9106 55.8173 10.9106C56.4772 10.9106 56.7926 11.2717 56.7926 11.998V14.7H57.685V11.7739C57.685 10.699 57.1288 10.093 56.1203 10.093C55.4396 10.093 54.9914 10.396 54.7714 10.8982H54.705V10.1802H53.8458V14.7Z"
|
||||||
|
className="fill-current"
|
||||||
|
/>
|
||||||
|
<path d="M59.0903 14.7H59.9826V8.41626H59.0903V14.7Z" className="fill-current" />
|
||||||
|
<path
|
||||||
|
d="M63.3386 14.7871C64.6584 14.7871 65.4678 13.9031 65.4678 12.438C65.4678 10.9812 64.6543 10.093 63.3386 10.093C62.0188 10.093 61.2053 10.9812 61.2053 12.438C61.2053 13.9031 62.0146 14.7871 63.3386 14.7871ZM63.3386 13.9944C62.5625 13.9944 62.1267 13.4258 62.1267 12.438C62.1267 11.4585 62.5625 10.8857 63.3386 10.8857C64.1106 10.8857 64.5505 11.4585 64.5505 12.438C64.5505 13.4216 64.1106 13.9944 63.3386 13.9944Z"
|
||||||
|
className="fill-current"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M68.1265 14.0234C67.6409 14.0234 67.2881 13.7869 67.2881 13.3801C67.2881 12.9817 67.5704 12.77 68.1929 12.7285L69.2969 12.658V13.0356C69.2969 13.5959 68.7989 14.0234 68.1265 14.0234ZM67.8982 14.7747C68.4917 14.7747 68.9856 14.5173 69.2554 14.0649H69.326V14.7H70.1851V11.6121C70.1851 10.6575 69.5459 10.093 68.4129 10.093C67.3877 10.093 66.6573 10.5911 66.566 11.3672H67.4292C67.5289 11.0476 67.8733 10.865 68.3714 10.865C68.9815 10.865 69.2969 11.1348 69.2969 11.6121V12.0022L68.0726 12.0728C66.9976 12.1392 66.3916 12.6082 66.3916 13.4216C66.3916 14.2476 67.0267 14.7747 67.8982 14.7747Z"
|
||||||
|
className="fill-current"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M73.2132 14.7747C73.8358 14.7747 74.3629 14.48 74.6327 13.9861H74.7032V14.7H75.5582V8.41626H74.6659V10.8982H74.5995C74.3546 10.4001 73.8316 10.1055 73.2132 10.1055C72.0719 10.1055 71.3373 11.0103 71.3373 12.438C71.3373 13.8699 72.0636 14.7747 73.2132 14.7747ZM73.4664 10.9065C74.2135 10.9065 74.6825 11.5 74.6825 12.4421C74.6825 13.3884 74.2176 13.9736 73.4664 13.9736C72.711 13.9736 72.2586 13.3967 72.2586 12.438C72.2586 11.4875 72.7152 10.9065 73.4664 10.9065Z"
|
||||||
|
className="fill-current"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M81.3447 14.7871C82.6645 14.7871 83.4738 13.9031 83.4738 12.438C83.4738 10.9812 82.6604 10.093 81.3447 10.093C80.0249 10.093 79.2114 10.9812 79.2114 12.438C79.2114 13.9031 80.0207 14.7871 81.3447 14.7871ZM81.3447 13.9944C80.5686 13.9944 80.1328 13.4258 80.1328 12.438C80.1328 11.4585 80.5686 10.8857 81.3447 10.8857C82.1166 10.8857 82.5566 11.4585 82.5566 12.438C82.5566 13.4216 82.1166 13.9944 81.3447 13.9944Z"
|
||||||
|
className="fill-current"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M84.655 14.7H85.5474V12.0562C85.5474 11.3506 85.9666 10.9106 86.6265 10.9106C87.2864 10.9106 87.6018 11.2717 87.6018 11.998V14.7H88.4941V11.7739C88.4941 10.699 87.938 10.093 86.9294 10.093C86.2488 10.093 85.8005 10.396 85.5806 10.8982H85.5142V10.1802H84.655V14.7Z"
|
||||||
|
className="fill-current"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M92.6039 9.05542V10.2009H91.8858V10.9521H92.6039V13.5046C92.6039 14.3762 92.9981 14.7249 93.9901 14.7249C94.1644 14.7249 94.3304 14.7041 94.4757 14.6792V13.9363C94.3512 13.9487 94.2723 13.957 94.1353 13.957C93.6913 13.957 93.4962 13.7495 93.4962 13.2764V10.9521H94.4757V10.2009H93.4962V9.05542H92.6039Z"
|
||||||
|
className="fill-current"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M95.6735 14.7H96.5658V12.0603C96.5658 11.3755 96.9726 10.9148 97.703 10.9148C98.3339 10.9148 98.6701 11.28 98.6701 12.0022V14.7H99.5624V11.7822C99.5624 10.7073 98.9689 10.0972 98.006 10.0972C97.3253 10.0972 96.848 10.4001 96.6281 10.9065H96.5575V8.41626H95.6735V14.7Z"
|
||||||
|
className="fill-current"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M102.781 10.8525C103.441 10.8525 103.873 11.3132 103.894 12.0229H101.611C101.661 11.3174 102.122 10.8525 102.781 10.8525ZM103.89 13.4797C103.724 13.8325 103.354 14.0276 102.815 14.0276C102.101 14.0276 101.64 13.5254 101.611 12.7327V12.6829H104.803V12.3716C104.803 10.9521 104.043 10.093 102.786 10.093C101.511 10.093 100.702 11.0103 100.702 12.4546C100.702 13.8989 101.495 14.7871 102.79 14.7871C103.823 14.7871 104.545 14.2891 104.749 13.4797H103.89Z"
|
||||||
|
className="fill-current"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M24.769 20.3008C24.7907 18.6198 25.6934 17.0292 27.1256 16.1488C26.2221 14.8584 24.7088 14.0403 23.1344 13.9911C21.4552 13.8148 19.8272 14.9959 18.9715 14.9959C18.0992 14.9959 16.7817 14.0086 15.363 14.0378C13.5137 14.0975 11.7898 15.1489 10.8901 16.7656C8.95607 20.1141 10.3987 25.0351 12.2513 27.7417C13.1782 29.0671 14.2615 30.5475 15.6789 30.495C17.066 30.4375 17.584 29.6105 19.2583 29.6105C20.9171 29.6105 21.4031 30.495 22.8493 30.4616C24.3377 30.4375 25.2754 29.1304 26.1698 27.7925C26.8358 26.8481 27.3483 25.8044 27.6882 24.7C25.9391 23.9602 24.771 22.2 24.769 20.3008Z"
|
||||||
|
className="fill-current"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M22.0373 12.2111C22.8489 11.2369 23.2487 9.98469 23.1518 8.72046C21.912 8.85068 20.7668 9.44324 19.9443 10.3801C19.14 11.2954 18.7214 12.5255 18.8006 13.7415C20.0408 13.7542 21.2601 13.1777 22.0373 12.2111Z"
|
||||||
|
className="fill-current"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const GalaxyStoreButton = ({ size = "md", ...props }: AnchorHTMLAttributes<HTMLAnchorElement> & { size?: "md" | "lg" }) => {
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
aria-label="Available on Galaxy Store"
|
||||||
|
href="#"
|
||||||
|
{...props}
|
||||||
|
className={cx(
|
||||||
|
"rounded-[7px] text-fg-primary ring-1 ring-fg-primary outline-focus-ring ring-inset focus-visible:outline-2 focus-visible:outline-offset-2",
|
||||||
|
props.className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<svg width={size === "md" ? 147 : 162} height={size === "md" ? 40 : 44} viewBox="0 0 147 40" fill="none">
|
||||||
|
<path d="M64.7516 20.3987H66.7715V31.3885H64.7516V20.3987Z" className="fill-current" />
|
||||||
|
<path
|
||||||
|
d="M42.5 25.9699C42.5 22.8811 44.8314 20.4009 48.039 20.4009C50.2816 20.4009 52.0489 21.5444 52.9695 23.1779L51.1875 24.2473C50.5193 23.0146 49.4799 22.3611 48.054 22.3611C46.0343 22.3611 44.5047 23.9946 44.5047 25.9699C44.5047 27.9745 46.0196 29.5786 48.1281 29.5786C49.7469 29.5786 50.8757 28.6578 51.2616 27.2322H47.8017V25.3017H53.4298V26.1037C53.4298 29.0289 51.3657 31.5392 48.1281 31.5392C44.7423 31.5392 42.5 28.9695 42.5 25.9699Z"
|
||||||
|
className="fill-current"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M54.3525 27.0543C54.3525 24.1732 56.4613 22.525 58.6592 22.525C59.8027 22.525 60.8274 22.9999 61.4512 23.7426V22.7032H63.4558V31.3907H61.4512V30.2616C60.8124 31.0636 59.773 31.5689 58.6295 31.5689C56.5354 31.5689 54.3525 29.8904 54.3525 27.0543ZM61.555 27.0246C61.555 25.5543 60.4412 24.3514 58.9562 24.3514C57.4713 24.3514 56.3278 25.5249 56.3278 27.0246C56.3278 28.539 57.4713 29.7272 58.9562 29.7272C60.4412 29.7272 61.555 28.5243 61.555 27.0246Z"
|
||||||
|
className="fill-current"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M67.7938 27.0543C67.7938 24.1732 69.9026 22.525 72.1005 22.525C73.244 22.525 74.2686 22.9999 74.8925 23.7426V22.7032H76.8971V31.3907H74.8925V30.2616C74.2539 31.0636 73.2146 31.5689 72.0707 31.5689C69.977 31.5689 67.7938 29.8904 67.7938 27.0543ZM74.9966 27.0246C74.9966 25.5543 73.8828 24.3514 72.3981 24.3514C70.9128 24.3514 69.7693 25.5249 69.7693 27.0246C69.7693 28.539 70.9128 29.7272 72.3981 29.7272C73.8825 29.7272 74.9966 28.5243 74.9966 27.0246Z"
|
||||||
|
className="fill-current"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M80.8048 26.9652L77.6566 22.7032H80.1072L82.0818 25.4652L84.0424 22.7032H86.4182L83.2994 26.9949L86.6261 31.3907H84.1759L82.0524 28.4949L79.988 31.3907H77.5375L80.8048 26.9652Z"
|
||||||
|
className="fill-current"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M90.3846 30.9598L86.8206 22.7029H88.9588L91.3796 28.554L93.6663 22.7029H95.7757L90.5926 35.4H88.5582L90.3846 30.9598Z"
|
||||||
|
className="fill-current"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M99.8907 29.5936L101.732 28.1384C102.282 29.074 103.128 29.6083 104.108 29.6083C105.178 29.6083 105.757 28.911 105.757 28.1531C105.757 27.2325 104.658 26.9505 103.499 26.594C102.044 26.1334 100.426 25.5693 100.426 23.5049C100.426 21.7676 101.94 20.4012 104.019 20.4012C105.772 20.4012 106.781 21.0697 107.658 21.9756L105.994 23.2376C105.534 22.5547 104.895 22.1982 104.034 22.1982C103.054 22.1982 102.519 22.7329 102.519 23.4305C102.519 24.292 103.559 24.5743 104.732 24.9605C106.203 25.4355 107.866 26.089 107.866 28.1681C107.866 29.8757 106.499 31.5395 104.123 31.5395C102.163 31.5392 100.871 30.7071 99.8907 29.5936Z"
|
||||||
|
className="fill-current"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M108.03 22.7029H109.515V21.3812L111.535 20V22.7029H113.302V24.4999H111.535V27.7519C111.535 29.2669 111.743 29.5045 113.302 29.5045V31.3904H113.02C110.332 31.3904 109.515 30.5292 109.515 27.7672V24.4999H108.03L108.03 22.7029Z"
|
||||||
|
className="fill-current"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M113.838 27.0543C113.838 24.5296 115.827 22.5247 118.337 22.5247C120.832 22.5247 122.837 24.5296 122.837 27.0543C122.837 29.5639 120.832 31.5689 118.337 31.5689C115.827 31.5689 113.838 29.5639 113.838 27.0543ZM120.862 27.0543C120.862 25.599 119.747 24.4108 118.337 24.4108C116.896 24.4108 115.812 25.599 115.812 27.0543C115.812 28.4949 116.896 29.6828 118.337 29.6828C119.747 29.6828 120.862 28.4949 120.862 27.0543Z"
|
||||||
|
className="fill-current"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M125.853 22.7029V23.9949C126.254 23.1335 126.981 22.7029 128.08 22.7029C128.704 22.7029 129.223 22.8514 129.61 23.0741L128.852 24.9749C128.555 24.782 128.214 24.6334 127.649 24.6334C126.491 24.6334 125.867 25.2573 125.867 26.7569V31.3904H123.862V22.7029H125.853Z"
|
||||||
|
className="fill-current"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M129.834 27.069C129.834 24.5296 131.808 22.5247 134.348 22.5247C136.932 22.5247 138.818 24.4255 138.818 26.9949V27.7519H131.749C132.017 28.9701 132.997 29.8016 134.423 29.8016C135.536 29.8016 136.413 29.1928 136.828 28.2719L138.477 29.2222C137.719 30.6186 136.353 31.5686 134.423 31.5686C131.69 31.5686 129.834 29.5786 129.834 27.069ZM131.853 26.074H136.784C136.487 24.9158 135.596 24.292 134.348 24.292C133.145 24.292 132.21 25.0196 131.853 26.074Z"
|
||||||
|
className="fill-current"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M46.6986 13.974H43.9018L43.3866 15.4H42.5034L44.8218 9.0244H45.7878L48.097 15.4H47.2138L46.6986 13.974ZM46.4594 13.2932L45.3002 10.0548L44.141 13.2932H46.4594Z"
|
||||||
|
className="fill-current"
|
||||||
|
/>
|
||||||
|
<path d="M50.4322 14.6272L51.9962 10.3584H52.8886L50.9106 15.4H49.9354L47.9574 10.3584H48.859L50.4322 14.6272Z" className="fill-current" />
|
||||||
|
<path
|
||||||
|
d="M53.4917 12.8608C53.4917 12.3456 53.5959 11.8948 53.8045 11.5084C54.013 11.1159 54.2982 10.8123 54.6601 10.5976C55.0281 10.3829 55.4359 10.2756 55.8837 10.2756C56.3253 10.2756 56.7086 10.3707 57.0337 10.5608C57.3587 10.7509 57.601 10.9901 57.7605 11.2784V10.3584H58.6069V15.4H57.7605V14.4616C57.5949 14.756 57.3465 15.0013 57.0153 15.1976C56.6902 15.3877 56.3099 15.4828 55.8745 15.4828C55.4267 15.4828 55.0219 15.3724 54.6601 15.1516C54.2982 14.9308 54.013 14.6211 53.8045 14.2224C53.5959 13.8237 53.4917 13.3699 53.4917 12.8608ZM57.7605 12.87C57.7605 12.4897 57.6838 12.1585 57.5305 11.8764C57.3771 11.5943 57.1686 11.3796 56.9049 11.2324C56.6473 11.0791 56.3621 11.0024 56.0493 11.0024C55.7365 11.0024 55.4513 11.076 55.1937 11.2232C54.9361 11.3704 54.7306 11.5851 54.5773 11.8672C54.4239 12.1493 54.3473 12.4805 54.3473 12.8608C54.3473 13.2472 54.4239 13.5845 54.5773 13.8728C54.7306 14.1549 54.9361 14.3727 55.1937 14.526C55.4513 14.6732 55.7365 14.7468 56.0493 14.7468C56.3621 14.7468 56.6473 14.6732 56.9049 14.526C57.1686 14.3727 57.3771 14.1549 57.5305 13.8728C57.6838 13.5845 57.7605 13.2503 57.7605 12.87Z"
|
||||||
|
className="fill-current"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M60.2701 9.5396C60.1106 9.5396 59.9757 9.4844 59.8653 9.374C59.7549 9.2636 59.6997 9.12867 59.6997 8.9692C59.6997 8.80974 59.7549 8.6748 59.8653 8.5644C59.9757 8.454 60.1106 8.3988 60.2701 8.3988C60.4234 8.3988 60.5522 8.454 60.6565 8.5644C60.7669 8.6748 60.8221 8.80974 60.8221 8.9692C60.8221 9.12867 60.7669 9.2636 60.6565 9.374C60.5522 9.4844 60.4234 9.5396 60.2701 9.5396ZM60.6749 10.3584V15.4H59.8377V10.3584H60.6749Z"
|
||||||
|
className="fill-current"
|
||||||
|
/>
|
||||||
|
<path d="M62.7549 8.592V15.4H61.9177V8.592H62.7549Z" className="fill-current" />
|
||||||
|
<path
|
||||||
|
d="M63.869 12.8608C63.869 12.3456 63.9732 11.8948 64.1818 11.5084C64.3903 11.1159 64.6755 10.8123 65.0374 10.5976C65.4054 10.3829 65.8132 10.2756 66.261 10.2756C66.7026 10.2756 67.0859 10.3707 67.411 10.5608C67.736 10.7509 67.9783 10.9901 68.1378 11.2784V10.3584H68.9842V15.4H68.1378V14.4616C67.9722 14.756 67.7238 15.0013 67.3926 15.1976C67.0675 15.3877 66.6872 15.4828 66.2518 15.4828C65.804 15.4828 65.3992 15.3724 65.0374 15.1516C64.6755 14.9308 64.3903 14.6211 64.1818 14.2224C63.9732 13.8237 63.869 13.3699 63.869 12.8608ZM68.1378 12.87C68.1378 12.4897 68.0611 12.1585 67.9078 11.8764C67.7544 11.5943 67.5459 11.3796 67.2822 11.2324C67.0246 11.0791 66.7394 11.0024 66.4266 11.0024C66.1138 11.0024 65.8286 11.076 65.571 11.2232C65.3134 11.3704 65.1079 11.5851 64.9546 11.8672C64.8012 12.1493 64.7246 12.4805 64.7246 12.8608C64.7246 13.2472 64.8012 13.5845 64.9546 13.8728C65.1079 14.1549 65.3134 14.3727 65.571 14.526C65.8286 14.6732 66.1138 14.7468 66.4266 14.7468C66.7394 14.7468 67.0246 14.6732 67.2822 14.526C67.5459 14.3727 67.7544 14.1549 67.9078 13.8728C68.0611 13.5845 68.1378 13.2503 68.1378 12.87Z"
|
||||||
|
className="fill-current"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M71.3282 11.2968C71.4999 10.9963 71.7514 10.7509 72.0826 10.5608C72.4138 10.3707 72.791 10.2756 73.2142 10.2756C73.668 10.2756 74.0759 10.3829 74.4378 10.5976C74.7996 10.8123 75.0848 11.1159 75.2934 11.5084C75.5019 11.8948 75.6062 12.3456 75.6062 12.8608C75.6062 13.3699 75.5019 13.8237 75.2934 14.2224C75.0848 14.6211 74.7966 14.9308 74.4286 15.1516C74.0667 15.3724 73.6619 15.4828 73.2142 15.4828C72.7787 15.4828 72.3954 15.3877 72.0642 15.1976C71.7391 15.0075 71.4938 14.7652 71.3282 14.4708V15.4H70.491V8.592H71.3282V11.2968ZM74.7506 12.8608C74.7506 12.4805 74.6739 12.1493 74.5206 11.8672C74.3672 11.5851 74.1587 11.3704 73.895 11.2232C73.6374 11.076 73.3522 11.0024 73.0394 11.0024C72.7327 11.0024 72.4475 11.0791 72.1838 11.2324C71.9262 11.3796 71.7176 11.5973 71.5582 11.8856C71.4048 12.1677 71.3282 12.4959 71.3282 12.87C71.3282 13.2503 71.4048 13.5845 71.5582 13.8728C71.7176 14.1549 71.9262 14.3727 72.1838 14.526C72.4475 14.6732 72.7327 14.7468 73.0394 14.7468C73.3522 14.7468 73.6374 14.6732 73.895 14.526C74.1587 14.3727 74.3672 14.1549 74.5206 13.8728C74.6739 13.5845 74.7506 13.2472 74.7506 12.8608Z"
|
||||||
|
className="fill-current"
|
||||||
|
/>
|
||||||
|
<path d="M77.5454 8.592V15.4H76.7082V8.592H77.5454Z" className="fill-current" />
|
||||||
|
<path
|
||||||
|
d="M83.6642 12.686C83.6642 12.8455 83.655 13.0141 83.6366 13.192H79.607C79.6377 13.6888 79.8064 14.0783 80.113 14.3604C80.4258 14.6364 80.803 14.7744 81.2446 14.7744C81.6065 14.7744 81.907 14.6916 82.1462 14.526C82.3916 14.3543 82.5633 14.1273 82.6614 13.8452H83.563C83.4281 14.3297 83.1582 14.7253 82.7534 15.032C82.3486 15.3325 81.8457 15.4828 81.2446 15.4828C80.7662 15.4828 80.3369 15.3755 79.9566 15.1608C79.5825 14.9461 79.2881 14.6425 79.0734 14.25C78.8587 13.8513 78.7514 13.3913 78.7514 12.87C78.7514 12.3487 78.8557 11.8917 79.0642 11.4992C79.2728 11.1067 79.5641 10.8061 79.9382 10.5976C80.3185 10.3829 80.754 10.2756 81.2446 10.2756C81.723 10.2756 82.1462 10.3799 82.5142 10.5884C82.8822 10.7969 83.1643 11.0852 83.3606 11.4532C83.563 11.8151 83.6642 12.226 83.6642 12.686ZM82.7994 12.5112C82.7994 12.1923 82.7289 11.9193 82.5878 11.6924C82.4467 11.4593 82.2536 11.2845 82.0082 11.168C81.769 11.0453 81.5022 10.984 81.2078 10.984C80.7846 10.984 80.4227 11.1189 80.1222 11.3888C79.8278 11.6587 79.6591 12.0328 79.6162 12.5112H82.7994Z"
|
||||||
|
className="fill-current"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M89.6048 15.4828C89.1326 15.4828 88.7032 15.3755 88.3168 15.1608C87.9366 14.9461 87.636 14.6425 87.4152 14.25C87.2006 13.8513 87.0932 13.3913 87.0932 12.87C87.0932 12.3548 87.2036 11.9009 87.4244 11.5084C87.6514 11.1097 87.958 10.8061 88.3444 10.5976C88.7308 10.3829 89.1632 10.2756 89.6416 10.2756C90.12 10.2756 90.5524 10.3829 90.9388 10.5976C91.3252 10.8061 91.6288 11.1067 91.8496 11.4992C92.0766 11.8917 92.19 12.3487 92.19 12.87C92.19 13.3913 92.0735 13.8513 91.8404 14.25C91.6135 14.6425 91.3038 14.9461 90.9112 15.1608C90.5187 15.3755 90.0832 15.4828 89.6048 15.4828ZM89.6048 14.7468C89.9054 14.7468 90.1875 14.6763 90.4512 14.5352C90.715 14.3941 90.9266 14.1825 91.086 13.9004C91.2516 13.6183 91.3344 13.2748 91.3344 12.87C91.3344 12.4652 91.2547 12.1217 91.0952 11.8396C90.9358 11.5575 90.7272 11.3489 90.4696 11.214C90.212 11.0729 89.933 11.0024 89.6324 11.0024C89.3258 11.0024 89.0436 11.0729 88.786 11.214C88.5346 11.3489 88.3322 11.5575 88.1788 11.8396C88.0255 12.1217 87.9488 12.4652 87.9488 12.87C87.9488 13.2809 88.0224 13.6275 88.1696 13.9096C88.323 14.1917 88.5254 14.4033 88.7768 14.5444C89.0283 14.6793 89.3043 14.7468 89.6048 14.7468Z"
|
||||||
|
className="fill-current"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M95.9312 10.2664C96.5445 10.2664 97.0413 10.4535 97.4216 10.8276C97.8019 11.1956 97.992 11.7292 97.992 12.4284V15.4H97.164V12.548C97.164 12.0451 97.0383 11.6617 96.7868 11.398C96.5353 11.1281 96.1919 10.9932 95.7564 10.9932C95.3148 10.9932 94.9621 11.1312 94.6984 11.4072C94.4408 11.6832 94.312 12.0849 94.312 12.6124V15.4H93.4748V10.3584H94.312V11.076C94.4776 10.8184 94.7015 10.6191 94.9836 10.478C95.2719 10.3369 95.5877 10.2664 95.9312 10.2664Z"
|
||||||
|
className="fill-current"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M9.2763 14.6655C9 15.8164 9 17.2109 9 20C9 22.7891 9 24.1836 9.2763 25.3345C10.1541 28.9909 13.0091 31.8459 16.6655 32.7237C17.8164 33 19.2109 33 22 33C24.7891 33 26.1836 33 27.3345 32.7237C30.9909 31.8459 33.8459 28.9909 34.7237 25.3345C35 24.1836 35 22.7891 35 20C35 17.2109 35 15.8164 34.7237 14.6655C33.8459 11.0091 30.9909 8.15415 27.3345 7.2763C26.1836 7 24.7891 7 22 7C19.2109 7 17.8164 7 16.6655 7.2763C13.0091 8.15415 10.1541 11.0091 9.2763 14.6655ZM25.738 16.3341L25.7914 16.5695H28.8609L28.0334 24.2828C27.8677 25.8515 26.5446 27.0421 24.9669 27.0421H19.0326C17.4549 27.0421 16.1318 25.8515 15.966 24.2826L15.1387 16.5695H18.2081L18.2617 16.3341C18.2617 14.2733 19.9385 12.5968 21.9997 12.5968C24.0609 12.5968 25.738 14.2733 25.738 16.3341ZM19.8934 16.3341L19.8331 16.5695H24.1664L24.1063 16.3341C24.1063 15.1729 23.1613 14.2281 21.9997 14.2281C20.8383 14.2281 19.8934 15.1729 19.8934 16.3341Z"
|
||||||
|
className="fill-current"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AppGalleryButton = ({ size = "md", ...props }: AnchorHTMLAttributes<HTMLAnchorElement> & { size?: "md" | "lg" }) => {
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
aria-label="Explore it on AppGallery"
|
||||||
|
href="#"
|
||||||
|
{...props}
|
||||||
|
className={cx(
|
||||||
|
"rounded-[7px] text-fg-primary ring-1 ring-fg-primary outline-focus-ring ring-inset focus-visible:outline-2 focus-visible:outline-offset-2",
|
||||||
|
props.className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<svg width={size === "md" ? 133 : 147} height={size === "md" ? 40 : 44} viewBox="0 0 133 40" fill="none">
|
||||||
|
<path
|
||||||
|
d="M45.3962 25.4116H48.8919L47.6404 22.0615C47.4682 21.5986 47.2989 21.0875 47.1319 20.5276C46.9813 21.0229 46.817 21.5286 46.6394 22.0453L45.3962 25.4116ZM49.4893 27.0021H44.8068L43.6607 30.1344H41.6021L46.1874 18.4368H48.133L52.8234 30.1344H50.6599L49.4893 27.0021Z"
|
||||||
|
className="fill-current"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M58.8962 28.0072C59.3026 27.461 59.5058 26.663 59.5058 25.6135C59.5058 24.6396 59.3375 23.933 59.0013 23.4942C58.6647 23.0557 58.2167 22.8364 57.657 22.8364C57.2695 22.8364 56.9117 22.9281 56.5834 23.1109C56.2551 23.2939 55.9429 23.5387 55.6469 23.8455V28.5117C55.8461 28.6085 56.0775 28.6853 56.3413 28.7416C56.605 28.7984 56.8661 28.8266 57.1242 28.8266C57.8994 28.8266 58.4898 28.5533 58.8962 28.0072ZM53.653 23.555C53.653 22.9092 53.6314 22.1986 53.5885 21.4237H55.4613C55.5311 21.7844 55.5797 22.1531 55.6066 22.5297C56.3815 21.6849 57.2695 21.2622 58.2706 21.2622C58.8519 21.2622 59.3901 21.4088 59.8853 21.7021C60.3802 21.9955 60.7799 22.4583 61.0839 23.0906C61.3882 23.7231 61.5402 24.5265 61.5402 25.5005C61.5402 26.5177 61.3666 27.387 61.0194 28.108C60.6722 28.8294 60.1866 29.3754 59.5623 29.747C58.9381 30.1181 58.2167 30.3039 57.3989 30.3039C56.8066 30.3039 56.2226 30.2042 55.6469 30.0053V33.6056L53.653 33.7752V23.555Z"
|
||||||
|
className="fill-current"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M67.9935 28.0072C68.3999 27.461 68.6031 26.663 68.6031 25.6135C68.6031 24.6396 68.4349 23.933 68.0986 23.4942C67.7621 23.0557 67.3141 22.8364 66.7543 22.8364C66.3669 22.8364 66.009 22.9281 65.6807 23.1109C65.3522 23.2939 65.0402 23.5387 64.7442 23.8455V28.5117C64.9434 28.6085 65.1746 28.6853 65.4386 28.7416C65.7021 28.7984 65.9631 28.8266 66.2215 28.8266C66.9964 28.8266 67.5871 28.5533 67.9935 28.0072ZM62.7501 23.555C62.7501 22.9092 62.7285 22.1986 62.6855 21.4237H64.5586C64.6285 21.7844 64.677 22.1531 64.7039 22.5297C65.4789 21.6849 66.3668 21.2622 67.3679 21.2622C67.9493 21.2622 68.4871 21.4088 68.9826 21.7021C69.4775 21.9955 69.8772 22.4583 70.1815 23.0906C70.4852 23.7231 70.6375 24.5265 70.6375 25.5005C70.6375 26.5177 70.4637 27.387 70.1167 28.108C69.7695 28.8294 69.2837 29.3754 68.6597 29.747C68.0351 30.1181 67.314 30.3039 66.4959 30.3039C65.9039 30.3039 65.3199 30.2042 64.7442 30.0053V33.6056L62.7501 33.7752V23.555Z"
|
||||||
|
className="fill-current"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M74.3005 29.5813C73.4391 29.1053 72.7773 28.4229 72.3146 27.5349C71.8514 26.6469 71.6202 25.5974 71.6202 24.3864C71.6202 23.0734 71.8866 21.9578 72.4194 21.0401C72.9522 20.1227 73.6775 19.4337 74.5949 18.9735C75.5127 18.5134 76.5418 18.2833 77.6831 18.2833C78.3557 18.2833 78.9975 18.3574 79.6085 18.5053C80.2191 18.6534 80.7882 18.8564 81.3159 19.1149L80.8071 20.6486C79.7468 20.1428 78.7351 19.8898 77.7719 19.8898C76.9591 19.8898 76.2474 20.0634 75.6367 20.4106C75.0258 20.7578 74.5506 21.2677 74.2117 21.9404C73.8725 22.6132 73.7031 23.4258 73.7031 24.3783C73.7031 25.2128 73.8335 25.9526 74.0943 26.5984C74.3557 27.2442 74.7674 27.7557 75.3298 28.1323C75.8922 28.509 76.6013 28.6973 77.457 28.6973C77.8445 28.6973 78.2319 28.6651 78.6194 28.6005C79.0069 28.536 79.3703 28.4418 79.7093 28.3179V25.9526H77.005V24.4026H81.6469V29.3272C80.9794 29.6392 80.2783 29.8789 79.5439 30.0456C78.809 30.2123 78.0786 30.2957 77.3519 30.2957C76.1786 30.2957 75.1613 30.0579 74.3005 29.5813Z"
|
||||||
|
className="fill-current"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M87.5381 28.5197C87.9522 28.3208 88.2914 28.073 88.5551 27.7771V26.1625C88.0114 26.1034 87.5674 26.0737 87.2231 26.0737C86.3997 26.0737 85.8303 26.2068 85.5159 26.4734C85.2007 26.7394 85.0434 27.0989 85.0434 27.5509C85.0434 27.9818 85.1578 28.3005 85.3866 28.5077C85.6154 28.7149 85.9261 28.8184 86.3189 28.8184C86.717 28.8184 87.1234 28.7189 87.5381 28.5197ZM88.7327 30.1344C88.6626 29.7952 88.6167 29.4106 88.5954 28.98C88.2887 29.3461 87.8893 29.6568 87.3965 29.9125C86.9045 30.168 86.3485 30.2957 85.7295 30.2957C85.229 30.2957 84.773 30.1976 84.361 30.001C83.9498 29.8048 83.6226 29.5087 83.3805 29.113C83.1383 28.7176 83.017 28.2347 83.017 27.6639C83.017 26.8192 83.321 26.145 83.9293 25.6416C84.5375 25.1385 85.5519 24.887 86.9727 24.887C87.5055 24.887 88.033 24.9248 88.5551 25V24.8305C88.5551 24.0609 88.3909 23.5187 88.0626 23.2036C87.7343 22.889 87.2634 22.7316 86.6501 22.7316C86.2247 22.7316 85.7701 22.7935 85.2855 22.9172C84.8013 23.0411 84.3759 23.189 84.0101 23.3612L83.6951 21.9081C84.0503 21.7465 84.5186 21.5986 85.0999 21.464C85.6813 21.3297 86.2946 21.2622 86.9405 21.2622C87.6941 21.2622 88.3343 21.3765 88.8618 21.6052C89.3893 21.834 89.801 22.2271 90.097 22.7838C90.393 23.3411 90.541 24.0906 90.541 25.0323V28.4954C90.541 28.8562 90.5623 29.4027 90.6055 30.1344H88.7327Z"
|
||||||
|
className="fill-current"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M92.0709 28.0434V17.7505L94.0567 17.5891V27.6882C94.0567 28.0597 94.1199 28.3218 94.2463 28.4754C94.3727 28.6285 94.5732 28.7056 94.8479 28.7056C94.9716 28.7056 95.1466 28.676 95.3724 28.6168L95.6066 30.0456C95.418 30.121 95.1882 30.1816 94.9167 30.2272C94.6447 30.2728 94.3876 30.2957 94.1455 30.2957C92.762 30.2957 92.0709 29.5451 92.0709 28.0434Z"
|
||||||
|
className="fill-current"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M97.0356 28.0434V17.7505L99.0215 17.5891V27.6882C99.0215 28.0597 99.0847 28.3218 99.2111 28.4754C99.3378 28.6285 99.5383 28.7056 99.8127 28.7056C99.9365 28.7056 100.111 28.676 100.338 28.6168L100.572 30.0456C100.383 30.121 100.153 30.1816 99.8815 30.2272C99.6095 30.2728 99.3524 30.2957 99.1103 30.2957C97.7271 30.2957 97.0356 29.5451 97.0356 28.0434Z"
|
||||||
|
className="fill-current"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M106.795 24.7659C106.755 24.0716 106.581 23.551 106.269 23.2036C105.957 22.8568 105.539 22.683 105.019 22.683C104.512 22.683 104.091 22.8581 103.755 23.2078C103.419 23.5578 103.197 24.0771 103.096 24.7659H106.795ZM108.739 26.0333H103.04C103.131 27.8579 104 28.7701 105.648 28.7701C106.056 28.7701 106.475 28.7203 106.904 28.6208C107.331 28.521 107.741 28.388 108.133 28.221L108.571 29.5853C107.595 30.0592 106.501 30.2957 105.285 30.2957C104.357 30.2957 103.579 30.121 102.944 29.7711C102.307 29.4213 101.829 28.9181 101.509 28.2613C101.19 27.6051 101.03 26.8138 101.03 25.888C101.03 24.9248 101.199 24.0958 101.539 23.4016C101.877 22.7073 102.349 22.1773 102.955 21.8112C103.56 21.4453 104.259 21.2622 105.051 21.2622C105.875 21.2622 106.56 21.4547 107.112 21.8396C107.664 22.2241 108.072 22.737 108.339 23.3773C108.605 24.018 108.739 24.7255 108.739 25.5005V26.0333Z"
|
||||||
|
className="fill-current"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M110.248 23.6115C110.248 23.1326 110.224 22.4034 110.181 21.4237H112.048C112.08 21.6659 112.109 21.9552 112.141 22.2916C112.171 22.6279 112.189 22.901 112.2 23.1109C112.432 22.7288 112.659 22.4073 112.883 22.1463C113.107 21.8851 113.368 21.6727 113.667 21.5083C113.965 21.3443 114.304 21.2622 114.688 21.2622C114.995 21.2622 115.256 21.2946 115.477 21.3592L115.227 23.0868C115.035 23.0276 114.819 22.9979 114.581 22.9979C114.115 22.9979 113.704 23.1177 113.355 23.3573C113.005 23.5965 112.632 23.9908 112.232 24.5398V30.1344H110.248V23.6115Z"
|
||||||
|
className="fill-current"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M116.597 33.6984C116.307 33.6475 116.067 33.5896 115.88 33.5251L116.243 32.072C116.376 32.1094 116.547 32.1458 116.752 32.1808C116.955 32.216 117.149 32.2333 117.333 32.2333C118.216 32.2333 118.877 31.6653 119.317 30.5299L119.448 30.207L116.235 21.4237H118.373L119.989 26.332C120.251 27.1717 120.421 27.8149 120.496 28.2613C120.648 27.6317 120.824 27.0021 121.029 26.3724L122.669 21.4237H124.677L121.475 30.2475C121.173 31.0816 120.845 31.7541 120.496 32.2656C120.147 32.7768 119.733 33.1562 119.259 33.4039C118.781 33.6512 118.208 33.7752 117.533 33.7752C117.2 33.7752 116.888 33.7499 116.597 33.6984Z"
|
||||||
|
className="fill-current"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M41.5933 7.30106H45.1474L45.0474 8.15946H42.6349V9.82186H44.9098V10.6261H42.6349V12.4677H45.1765L45.089 13.3344H41.5933V7.30106Z"
|
||||||
|
className="fill-current"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M47.5973 10.2552L45.7223 7.30106H46.9055L48.1765 9.472L49.514 7.30106H50.6639L48.8055 10.2176L50.8181 13.3344H49.6181L48.1807 10.9595L46.7181 13.3344H45.5682L47.5973 10.2552Z"
|
||||||
|
className="fill-current"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M54.1973 9.99466C54.4194 9.7936 54.5306 9.50933 54.5306 9.14266C54.5306 8.7704 54.4172 8.50266 54.1911 8.33866C53.9647 8.17466 53.6319 8.0928 53.1932 8.0928H52.6266V10.2344C52.8876 10.276 53.0876 10.2968 53.2266 10.2968C53.6514 10.2968 53.9751 10.1963 54.1973 9.99466ZM51.5847 7.30106H53.2098C53.9735 7.30106 54.5583 7.4568 54.9639 7.76773C55.3695 8.07893 55.5722 8.5288 55.5722 9.1176C55.5722 9.5176 55.4813 9.8672 55.2994 10.1656C55.1173 10.4643 54.8639 10.6936 54.5388 10.8531C54.214 11.0131 53.8402 11.0928 53.418 11.0928C53.1876 11.0928 52.9236 11.0651 52.6266 11.0093V13.3344H51.5847V7.30106Z"
|
||||||
|
className="fill-current"
|
||||||
|
/>
|
||||||
|
<path d="M56.6015 7.30106H57.6431V12.4427H60.1389L60.0514 13.3344H56.6015V7.30106Z" className="fill-current" />
|
||||||
|
<path
|
||||||
|
d="M64.4223 12.2989C64.714 12.1032 64.9319 11.8339 65.0764 11.4907C65.221 11.1477 65.2932 10.7552 65.2932 10.3136C65.2932 9.88026 65.2292 9.49439 65.1015 9.15519C64.9735 8.81625 64.7695 8.54639 64.489 8.34479C64.2084 8.14346 63.8474 8.04266 63.4055 8.04266C62.9834 8.04266 62.625 8.14479 62.3306 8.34906C62.0362 8.55306 61.8154 8.82692 61.6682 9.16986C61.521 9.51306 61.4474 9.89145 61.4474 10.3053C61.4474 10.7413 61.5167 11.1317 61.6556 11.476C61.7943 11.8205 62.0068 12.0928 62.2932 12.2928C62.5791 12.4928 62.9332 12.5928 63.3556 12.5928C63.7751 12.5928 64.1306 12.4947 64.4223 12.2989ZM61.7223 13.0387C61.2807 12.7859 60.9431 12.4309 60.7098 11.9739C60.4764 11.5171 60.3599 10.9859 60.3599 10.3803C60.3599 9.74426 60.4839 9.18799 60.7327 8.71146C60.9813 8.23519 61.3396 7.86719 61.8076 7.60719C62.2756 7.34772 62.8277 7.21759 63.4639 7.21759C64.0722 7.21759 64.5959 7.34346 65.0348 7.59466C65.4735 7.84639 65.8084 8.19972 66.0388 8.65519C66.2695 9.11092 66.3847 9.63865 66.3847 10.2387C66.3847 10.8859 66.2591 11.4485 66.0076 11.9261C65.7562 12.4037 65.3981 12.772 64.9327 13.0301C64.4674 13.2885 63.921 13.4177 63.2932 13.4177C62.6876 13.4177 62.1639 13.2915 61.7223 13.0387Z"
|
||||||
|
className="fill-current"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M69.1807 10.1552C69.4333 10.1552 69.657 10.1061 69.8514 10.0072C70.0458 9.9088 70.1973 9.76986 70.3055 9.59066C70.4141 9.41146 70.4682 9.20373 70.4682 8.96773C70.4682 8.66506 70.3722 8.44346 70.1807 8.3032C69.989 8.16293 69.7098 8.0928 69.3431 8.0928H68.589V10.1552H69.1807ZM67.5474 7.30106H69.4349C70.1237 7.30106 70.6453 7.43866 70.9994 7.7136C71.3535 7.98853 71.5306 8.38186 71.5306 8.8928C71.5306 9.21226 71.4666 9.4936 71.3389 9.73653C71.2111 9.97973 71.0527 10.1776 70.864 10.3301C70.6749 10.4829 70.4807 10.5968 70.2807 10.672L72.1349 13.3344H70.9266L69.3557 10.9427H68.589V13.3344H67.5474V7.30106Z"
|
||||||
|
className="fill-current"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M72.9599 7.30106H76.5141L76.4141 8.15946H74.0015V9.82186H76.2765V10.6261H74.0015V12.4677H76.5431L76.4556 13.3344H72.9599V7.30106Z"
|
||||||
|
className="fill-current"
|
||||||
|
/>
|
||||||
|
<path d="M80.3807 7.30106H81.4223V13.3344H80.3807V7.30106Z" className="fill-current" />
|
||||||
|
<path d="M84.1765 8.172H82.3055L82.3973 7.30106H87.0933L86.9973 8.172H85.218V13.3344H84.1765V8.172Z" className="fill-current" />
|
||||||
|
<path
|
||||||
|
d="M94.3141 12.2989C94.6055 12.1032 94.8237 11.8339 94.9682 11.4907C95.1125 11.1477 95.1847 10.7552 95.1847 10.3136C95.1847 9.88026 95.1207 9.49439 94.9933 9.15519C94.8653 8.81625 94.661 8.54639 94.3807 8.34479C94.0999 8.14346 93.7389 8.04266 93.2973 8.04266C92.8749 8.04266 92.5167 8.14479 92.2223 8.34906C91.9277 8.55306 91.7069 8.82692 91.5599 9.16986C91.4125 9.51306 91.3389 9.89145 91.3389 10.3053C91.3389 10.7413 91.4082 11.1317 91.5474 11.476C91.6861 11.8205 91.8986 12.0928 92.1847 12.2928C92.4709 12.4928 92.825 12.5928 93.2474 12.5928C93.6666 12.5928 94.0223 12.4947 94.3141 12.2989ZM91.6141 13.0387C91.1722 12.7859 90.8349 12.4309 90.6015 11.9739C90.3682 11.5171 90.2514 10.9859 90.2514 10.3803C90.2514 9.74426 90.3757 9.18799 90.6245 8.71146C90.8727 8.23519 91.2311 7.86719 91.6994 7.60719C92.1674 7.34772 92.7194 7.21759 93.3557 7.21759C93.9639 7.21759 94.4874 7.34346 94.9266 7.59466C95.3653 7.84639 95.6999 8.19972 95.9306 8.65519C96.161 9.11092 96.2765 9.63865 96.2765 10.2387C96.2765 10.8859 96.1506 11.4485 95.8994 11.9261C95.6479 12.4037 95.2895 12.772 94.8245 13.0301C94.3591 13.2885 93.8125 13.4177 93.1847 13.4177C92.5791 13.4177 92.0557 13.2915 91.6141 13.0387Z"
|
||||||
|
className="fill-current"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M97.4389 7.30106H98.6349L101.619 11.976C101.592 11.5317 101.581 11.1219 101.581 10.7469V7.30106H102.547V13.3344H101.389L98.3599 8.58426C98.3903 9.12346 98.4055 9.60106 98.4055 10.0176V13.3344H97.4389V7.30106Z"
|
||||||
|
className="fill-current"
|
||||||
|
/>
|
||||||
|
<path d="M19.7881 22.0255H20.8041L20.2945 20.8404L19.7881 22.0255Z" className="fill-current" />
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M26.9417 7.33331H15.8633C10.6446 7.33331 8.73584 9.24211 8.73584 14.4608V25.5394C8.73584 30.7579 10.6446 32.6666 15.8633 32.6666H26.9382C32.1569 32.6666 34.0692 30.7579 34.0692 25.5394V14.4608C34.0692 9.24211 32.1604 7.33331 26.9417 7.33331ZM19.538 22.6229L19.2366 23.3125H18.5505L20.0097 20.0022H20.6025L22.0558 23.3125H21.3513L21.0537 22.6229H19.538ZM30.5787 23.3102H31.242V20.0022H30.5787V23.3102ZM27.9396 21.8887H29.1617V21.2859H27.9396V20.6078H29.7137V20.0042H27.2766V23.3122H29.7777V22.7088H27.9396V21.8887ZM25.3046 22.2797L24.5526 20.0021H24.0043L23.2524 22.2797L22.5206 20.0036H21.8054L22.9598 23.314H23.5161L24.2692 21.1396L25.022 23.314H25.5833L26.7348 20.0036H26.038L25.3046 22.2797ZM17.5382 21.8979C17.5382 22.4364 17.2708 22.7241 16.7852 22.7241C16.2969 22.7241 16.0281 22.4283 16.0281 21.875V20.004H15.3561V21.8979C15.3561 22.8297 15.8737 23.3638 16.7761 23.3638C17.6873 23.3638 18.2099 22.8194 18.2099 21.8705V20.0021H17.5382V21.8979ZM13.7526 20.0022H14.4243V23.3142H13.7526V21.9693H12.235V23.3142H11.563V20.0022H12.235V21.3383H13.7526V20.0022ZM17.1857 11.5683C17.1857 13.8933 19.0774 15.7851 21.4025 15.7851C23.7276 15.7851 25.6193 13.8933 25.6193 11.5683H25.0236C25.0236 13.5649 23.3993 15.1894 21.4025 15.1894C19.4057 15.1894 17.7814 13.5649 17.7814 11.5683H17.1857Z"
|
||||||
|
className="fill-current"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
};
|
||||||
565
src/components/base/buttons/app-store-buttons.tsx
Normal file
565
src/components/base/buttons/app-store-buttons.tsx
Normal file
@@ -0,0 +1,565 @@
|
|||||||
|
import type { AnchorHTMLAttributes } from "react";
|
||||||
|
import { cx } from "@/utils/cx";
|
||||||
|
|
||||||
|
export const GooglePlayButton = ({ size = "md", ...props }: AnchorHTMLAttributes<HTMLAnchorElement> & { size?: "md" | "lg" }) => {
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
aria-label="Get it on Google Play"
|
||||||
|
href="#"
|
||||||
|
{...props}
|
||||||
|
className={cx(
|
||||||
|
"rounded-[7px] bg-black ring-1 ring-app-store-badge-border outline-focus-ring ring-inset focus-visible:outline-2 focus-visible:outline-offset-2",
|
||||||
|
props.className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<svg width={size === "md" ? 135 : 149} height={size === "md" ? 40 : 44} viewBox="0 0 135 40" fill="none">
|
||||||
|
<path
|
||||||
|
d="M68.136 21.7511C65.784 21.7511 63.867 23.5401 63.867 26.0041C63.867 28.4531 65.784 30.2571 68.136 30.2571C70.489 30.2571 72.406 28.4531 72.406 26.0041C72.405 23.5401 70.488 21.7511 68.136 21.7511ZM68.136 28.5831C66.847 28.5831 65.736 27.5201 65.736 26.0051C65.736 24.4741 66.848 23.4271 68.136 23.4271C69.425 23.4271 70.536 24.4741 70.536 26.0051C70.536 27.5191 69.425 28.5831 68.136 28.5831ZM58.822 21.7511C56.47 21.7511 54.553 23.5401 54.553 26.0041C54.553 28.4531 56.47 30.2571 58.822 30.2571C61.175 30.2571 63.092 28.4531 63.092 26.0041C63.092 23.5401 61.175 21.7511 58.822 21.7511ZM58.822 28.5831C57.533 28.5831 56.422 27.5201 56.422 26.0051C56.422 24.4741 57.534 23.4271 58.822 23.4271C60.111 23.4271 61.222 24.4741 61.222 26.0051C61.223 27.5191 60.111 28.5831 58.822 28.5831ZM47.744 23.0571V24.8611H52.062C51.933 25.8761 51.595 26.6171 51.079 27.1321C50.451 27.7601 49.468 28.4531 47.744 28.4531C45.086 28.4531 43.008 26.3101 43.008 23.6521C43.008 20.9941 45.086 18.8511 47.744 18.8511C49.178 18.8511 50.225 19.4151 50.998 20.1401L52.271 18.8671C51.191 17.8361 49.758 17.0471 47.744 17.0471C44.103 17.0471 41.042 20.0111 41.042 23.6521C41.042 27.2931 44.103 30.2571 47.744 30.2571C49.709 30.2571 51.192 29.6121 52.351 28.4041C53.543 27.2121 53.914 25.5361 53.914 24.1831C53.914 23.7651 53.882 23.3781 53.817 23.0561H47.744V23.0571ZM93.052 24.4581C92.698 23.5081 91.618 21.7511 89.411 21.7511C87.22 21.7511 85.399 23.4751 85.399 26.0041C85.399 28.3881 87.204 30.2571 89.62 30.2571C91.569 30.2571 92.697 29.0651 93.165 28.3721L91.715 27.4051C91.232 28.1141 90.571 28.5811 89.62 28.5811C88.67 28.5811 87.993 28.1461 87.558 27.2921L93.245 24.9401L93.052 24.4581ZM87.252 25.8761C87.204 24.2321 88.525 23.3951 89.476 23.3951C90.217 23.3951 90.845 23.7661 91.055 24.2971L87.252 25.8761ZM82.629 30.0001H84.497V17.4991H82.629V30.0001ZM79.567 22.7021H79.503C79.084 22.2021 78.278 21.7511 77.264 21.7511C75.137 21.7511 73.188 23.6201 73.188 26.0211C73.188 28.4051 75.137 30.2581 77.264 30.2581C78.279 30.2581 79.084 29.8071 79.503 29.2921H79.567V29.9041C79.567 31.5311 78.697 32.4011 77.296 32.4011C76.152 32.4011 75.443 31.5801 75.153 30.8871L73.526 31.5641C73.993 32.6911 75.233 34.0771 77.296 34.0771C79.487 34.0771 81.34 32.7881 81.34 29.6461V22.0101H79.568V22.7021H79.567ZM77.425 28.5831C76.136 28.5831 75.057 27.5031 75.057 26.0211C75.057 24.5221 76.136 23.4271 77.425 23.4271C78.697 23.4271 79.696 24.5221 79.696 26.0211C79.696 27.5031 78.697 28.5831 77.425 28.5831ZM101.806 17.4991H97.335V30.0001H99.2V25.2641H101.805C103.873 25.2641 105.907 23.7671 105.907 21.3821C105.907 18.9971 103.874 17.4991 101.806 17.4991ZM101.854 23.5241H99.2V19.2391H101.854C103.249 19.2391 104.041 20.3941 104.041 21.3821C104.041 22.3501 103.249 23.5241 101.854 23.5241ZM113.386 21.7291C112.035 21.7291 110.636 22.3241 110.057 23.6431L111.713 24.3341C112.067 23.6431 112.727 23.4171 113.418 23.4171C114.383 23.4171 115.364 23.9961 115.38 25.0251V25.1541C115.042 24.9611 114.318 24.6721 113.434 24.6721C111.649 24.6721 109.831 25.6531 109.831 27.4861C109.831 29.1591 111.295 30.2361 112.935 30.2361C114.189 30.2361 114.881 29.6731 115.315 29.0131H115.379V29.9781H117.181V25.1851C117.182 22.9671 115.524 21.7291 113.386 21.7291ZM113.16 28.5801C112.55 28.5801 111.697 28.2741 111.697 27.5181C111.697 26.5531 112.759 26.1831 113.676 26.1831C114.495 26.1831 114.882 26.3601 115.38 26.6011C115.235 27.7601 114.238 28.5801 113.16 28.5801ZM123.743 22.0021L121.604 27.4221H121.54L119.32 22.0021H117.31L120.639 29.5771L118.741 33.7911H120.687L125.818 22.0021H123.743ZM106.937 30.0001H108.802V17.4991H106.937V30.0001Z"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M47.418 10.2429C47.418 11.0809 47.1701 11.7479 46.673 12.2459C46.109 12.8379 45.3731 13.1339 44.4691 13.1339C43.6031 13.1339 42.8661 12.8339 42.2611 12.2339C41.6551 11.6329 41.3521 10.8889 41.3521 10.0009C41.3521 9.11194 41.6551 8.36794 42.2611 7.76794C42.8661 7.16694 43.6031 6.86694 44.4691 6.86694C44.8991 6.86694 45.3101 6.95094 45.7001 7.11794C46.0911 7.28594 46.404 7.50894 46.6381 7.78794L46.111 8.31594C45.714 7.84094 45.167 7.60394 44.468 7.60394C43.836 7.60394 43.29 7.82594 42.829 8.26994C42.368 8.71394 42.1381 9.29094 42.1381 9.99994C42.1381 10.7089 42.368 11.2859 42.829 11.7299C43.29 12.1739 43.836 12.3959 44.468 12.3959C45.138 12.3959 45.6971 12.1729 46.1441 11.7259C46.4341 11.4349 46.602 11.0299 46.647 10.5109H44.468V9.78994H47.375C47.405 9.94694 47.418 10.0979 47.418 10.2429Z"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
<path d="M52.0281 7.737H49.2961V9.639H51.7601V10.36H49.2961V12.262H52.0281V13H48.5251V7H52.0281V7.737Z" fill="white" />
|
||||||
|
<path d="M55.279 13H54.508V7.737H52.832V7H56.955V7.737H55.279V13Z" fill="white" />
|
||||||
|
<path d="M59.938 13V7H60.709V13H59.938Z" fill="white" />
|
||||||
|
<path d="M64.1281 13H63.3572V7.737H61.6812V7H65.8042V7.737H64.1281V13Z" fill="white" />
|
||||||
|
<path
|
||||||
|
d="M73.6089 12.225C73.0189 12.831 72.2859 13.134 71.4089 13.134C70.5319 13.134 69.7989 12.831 69.2099 12.225C68.6199 11.619 68.3259 10.877 68.3259 9.99999C68.3259 9.12299 68.6199 8.38099 69.2099 7.77499C69.7989 7.16899 70.5319 6.86499 71.4089 6.86499C72.2809 6.86499 73.0129 7.16999 73.6049 7.77899C74.1969 8.38799 74.4929 9.12799 74.4929 9.99999C74.4929 10.877 74.1979 11.619 73.6089 12.225ZM69.7789 11.722C70.2229 12.172 70.7659 12.396 71.4089 12.396C72.0519 12.396 72.5959 12.171 73.0389 11.722C73.4829 11.272 73.7059 10.698 73.7059 9.99999C73.7059 9.30199 73.4829 8.72799 73.0389 8.27799C72.5959 7.82799 72.0519 7.60399 71.4089 7.60399C70.7659 7.60399 70.2229 7.82899 69.7789 8.27799C69.3359 8.72799 69.1129 9.30199 69.1129 9.99999C69.1129 10.698 69.3359 11.272 69.7789 11.722Z"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M75.5749 13V7H76.513L79.429 11.667H79.4619L79.429 10.511V7H80.1999V13H79.3949L76.344 8.106H76.3109L76.344 9.262V13H75.5749Z"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M47.418 10.2429C47.418 11.0809 47.1701 11.7479 46.673 12.2459C46.109 12.8379 45.3731 13.1339 44.4691 13.1339C43.6031 13.1339 42.8661 12.8339 42.2611 12.2339C41.6551 11.6329 41.3521 10.8889 41.3521 10.0009C41.3521 9.11194 41.6551 8.36794 42.2611 7.76794C42.8661 7.16694 43.6031 6.86694 44.4691 6.86694C44.8991 6.86694 45.3101 6.95094 45.7001 7.11794C46.0911 7.28594 46.404 7.50894 46.6381 7.78794L46.111 8.31594C45.714 7.84094 45.167 7.60394 44.468 7.60394C43.836 7.60394 43.29 7.82594 42.829 8.26994C42.368 8.71394 42.1381 9.29094 42.1381 9.99994C42.1381 10.7089 42.368 11.2859 42.829 11.7299C43.29 12.1739 43.836 12.3959 44.468 12.3959C45.138 12.3959 45.6971 12.1729 46.1441 11.7259C46.4341 11.4349 46.602 11.0299 46.647 10.5109H44.468V9.78994H47.375C47.405 9.94694 47.418 10.0979 47.418 10.2429Z"
|
||||||
|
stroke="white"
|
||||||
|
strokeWidth="0.2"
|
||||||
|
strokeMiterlimit="10"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M52.0281 7.737H49.2961V9.639H51.7601V10.36H49.2961V12.262H52.0281V13H48.5251V7H52.0281V7.737Z"
|
||||||
|
stroke="white"
|
||||||
|
strokeWidth="0.2"
|
||||||
|
strokeMiterlimit="10"
|
||||||
|
/>
|
||||||
|
<path d="M55.279 13H54.508V7.737H52.832V7H56.955V7.737H55.279V13Z" stroke="white" strokeWidth="0.2" strokeMiterlimit="10" />
|
||||||
|
<path d="M59.938 13V7H60.709V13H59.938Z" stroke="white" strokeWidth="0.2" strokeMiterlimit="10" />
|
||||||
|
<path d="M64.1281 13H63.3572V7.737H61.6812V7H65.8042V7.737H64.1281V13Z" stroke="white" strokeWidth="0.2" strokeMiterlimit="10" />
|
||||||
|
<path
|
||||||
|
d="M73.6089 12.225C73.0189 12.831 72.2859 13.134 71.4089 13.134C70.5319 13.134 69.7989 12.831 69.2099 12.225C68.6199 11.619 68.3259 10.877 68.3259 9.99999C68.3259 9.12299 68.6199 8.38099 69.2099 7.77499C69.7989 7.16899 70.5319 6.86499 71.4089 6.86499C72.2809 6.86499 73.0129 7.16999 73.6049 7.77899C74.1969 8.38799 74.4929 9.12799 74.4929 9.99999C74.4929 10.877 74.1979 11.619 73.6089 12.225ZM69.7789 11.722C70.2229 12.172 70.7659 12.396 71.4089 12.396C72.0519 12.396 72.5959 12.171 73.0389 11.722C73.4829 11.272 73.7059 10.698 73.7059 9.99999C73.7059 9.30199 73.4829 8.72799 73.0389 8.27799C72.5959 7.82799 72.0519 7.60399 71.4089 7.60399C70.7659 7.60399 70.2229 7.82899 69.7789 8.27799C69.3359 8.72799 69.1129 9.30199 69.1129 9.99999C69.1129 10.698 69.3359 11.272 69.7789 11.722Z"
|
||||||
|
stroke="white"
|
||||||
|
strokeWidth="0.2"
|
||||||
|
strokeMiterlimit="10"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M75.5749 13V7H76.513L79.429 11.667H79.4619L79.429 10.511V7H80.1999V13H79.3949L76.344 8.106H76.3109L76.344 9.262V13H75.5749Z"
|
||||||
|
stroke="white"
|
||||||
|
strokeWidth="0.2"
|
||||||
|
strokeMiterlimit="10"
|
||||||
|
/>
|
||||||
|
<g filter="url(#filter0_ii_1303_2188)">
|
||||||
|
<path
|
||||||
|
d="M10.4361 7.53803C10.1451 7.84603 9.97314 8.32403 9.97314 8.94303V31.059C9.97314 31.679 10.1451 32.156 10.4361 32.464L10.5101 32.536L22.8991 20.147V20.001V19.855L10.5101 7.46503L10.4361 7.53803Z"
|
||||||
|
fill="url(#paint0_linear_1303_2188)"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M27.0279 24.278L22.8989 20.147V20.001V19.855L27.0289 15.725L27.1219 15.778L32.0149 18.558C33.4119 19.352 33.4119 20.651 32.0149 21.446L27.1219 24.226L27.0279 24.278Z"
|
||||||
|
fill="url(#paint1_linear_1303_2188)"
|
||||||
|
/>
|
||||||
|
<g filter="url(#filter1_i_1303_2188)">
|
||||||
|
<path
|
||||||
|
d="M27.122 24.225L22.898 20.001L10.436 32.464C10.896 32.952 11.657 33.012 12.514 32.526L27.122 24.225Z"
|
||||||
|
fill="url(#paint2_linear_1303_2188)"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
<path
|
||||||
|
d="M27.122 15.777L12.514 7.47701C11.657 6.99001 10.896 7.05101 10.436 7.53901L22.899 20.002L27.122 15.777Z"
|
||||||
|
fill="url(#paint3_linear_1303_2188)"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<filter
|
||||||
|
id="filter0_ii_1303_2188"
|
||||||
|
x="9.97314"
|
||||||
|
y="7.14093"
|
||||||
|
width="23.0894"
|
||||||
|
height="25.7207"
|
||||||
|
filterUnits="userSpaceOnUse"
|
||||||
|
colorInterpolationFilters="sRGB"
|
||||||
|
>
|
||||||
|
<feFlood floodOpacity="0" result="BackgroundImageFix" />
|
||||||
|
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
|
||||||
|
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
|
||||||
|
<feOffset dy="-0.15" />
|
||||||
|
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" />
|
||||||
|
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.12 0" />
|
||||||
|
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_1303_2188" />
|
||||||
|
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
|
||||||
|
<feOffset dy="0.15" />
|
||||||
|
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" />
|
||||||
|
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.25 0" />
|
||||||
|
<feBlend mode="normal" in2="effect1_innerShadow_1303_2188" result="effect2_innerShadow_1303_2188" />
|
||||||
|
</filter>
|
||||||
|
<filter
|
||||||
|
id="filter1_i_1303_2188"
|
||||||
|
x="10.436"
|
||||||
|
y="20.001"
|
||||||
|
width="16.686"
|
||||||
|
height="12.8607"
|
||||||
|
filterUnits="userSpaceOnUse"
|
||||||
|
colorInterpolationFilters="sRGB"
|
||||||
|
>
|
||||||
|
<feFlood floodOpacity="0" result="BackgroundImageFix" />
|
||||||
|
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
|
||||||
|
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
|
||||||
|
<feOffset dy="-0.15" />
|
||||||
|
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" />
|
||||||
|
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.08 0" />
|
||||||
|
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_1303_2188" />
|
||||||
|
</filter>
|
||||||
|
<linearGradient id="paint0_linear_1303_2188" x1="21.8009" y1="8.70903" x2="5.01895" y2="25.491" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stopColor="#00A0FF" />
|
||||||
|
<stop offset="0.0066" stopColor="#00A1FF" />
|
||||||
|
<stop offset="0.2601" stopColor="#00BEFF" />
|
||||||
|
<stop offset="0.5122" stopColor="#00D2FF" />
|
||||||
|
<stop offset="0.7604" stopColor="#00DFFF" />
|
||||||
|
<stop offset="1" stopColor="#00E3FF" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="paint1_linear_1303_2188" x1="33.8334" y1="20.001" x2="9.63753" y2="20.001" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stopColor="#FFE000" />
|
||||||
|
<stop offset="0.4087" stopColor="#FFBD00" />
|
||||||
|
<stop offset="0.7754" stopColor="#FFA500" />
|
||||||
|
<stop offset="1" stopColor="#FF9C00" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="paint2_linear_1303_2188" x1="24.8281" y1="22.2949" x2="2.06964" y2="45.0534" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stopColor="#FF3A44" />
|
||||||
|
<stop offset="1" stopColor="#C31162" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="paint3_linear_1303_2188" x1="7.29743" y1="0.176806" x2="17.4597" y2="10.3391" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stopColor="#32A071" />
|
||||||
|
<stop offset="0.0685" stopColor="#2DA771" />
|
||||||
|
<stop offset="0.4762" stopColor="#15CF74" />
|
||||||
|
<stop offset="0.8009" stopColor="#06E775" />
|
||||||
|
<stop offset="1" stopColor="#00F076" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const GooglePlayWhiteButton = ({ size = "md", ...props }: AnchorHTMLAttributes<HTMLAnchorElement> & { size?: "md" | "lg" }) => {
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
aria-label="Get it on Google Play"
|
||||||
|
href="#"
|
||||||
|
{...props}
|
||||||
|
className={cx(
|
||||||
|
"rounded-[7px] bg-black ring-1 ring-app-store-badge-border outline-focus-ring ring-inset focus-visible:outline-2 focus-visible:outline-offset-2",
|
||||||
|
props.className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<svg width={size === "md" ? 135 : 149} height={size === "md" ? 40 : 44} viewBox="0 0 135 40" fill="none">
|
||||||
|
<rect x="0.5" y="0.5" width="134" height="39" rx="6.5" stroke="white" />
|
||||||
|
<path
|
||||||
|
d="M68.136 21.7509C65.784 21.7509 63.867 23.5399 63.867 26.0039C63.867 28.4529 65.784 30.2569 68.136 30.2569C70.489 30.2569 72.406 28.4529 72.406 26.0039C72.405 23.5399 70.488 21.7509 68.136 21.7509ZM68.136 28.5829C66.847 28.5829 65.736 27.5199 65.736 26.0049C65.736 24.4739 66.848 23.4269 68.136 23.4269C69.425 23.4269 70.536 24.4739 70.536 26.0049C70.536 27.5189 69.425 28.5829 68.136 28.5829ZM58.822 21.7509C56.47 21.7509 54.553 23.5399 54.553 26.0039C54.553 28.4529 56.47 30.2569 58.822 30.2569C61.175 30.2569 63.092 28.4529 63.092 26.0039C63.092 23.5399 61.175 21.7509 58.822 21.7509ZM58.822 28.5829C57.533 28.5829 56.422 27.5199 56.422 26.0049C56.422 24.4739 57.534 23.4269 58.822 23.4269C60.111 23.4269 61.222 24.4739 61.222 26.0049C61.223 27.5189 60.111 28.5829 58.822 28.5829ZM47.744 23.0569V24.8609H52.062C51.933 25.8759 51.595 26.6169 51.079 27.1319C50.451 27.7599 49.468 28.4529 47.744 28.4529C45.086 28.4529 43.008 26.3099 43.008 23.6519C43.008 20.9939 45.086 18.8509 47.744 18.8509C49.178 18.8509 50.225 19.4149 50.998 20.1399L52.271 18.8669C51.191 17.8359 49.758 17.0469 47.744 17.0469C44.103 17.0469 41.042 20.0109 41.042 23.6519C41.042 27.2929 44.103 30.2569 47.744 30.2569C49.709 30.2569 51.192 29.6119 52.351 28.4039C53.543 27.2119 53.914 25.5359 53.914 24.1829C53.914 23.7649 53.882 23.3779 53.817 23.0559H47.744V23.0569ZM93.052 24.4579C92.698 23.5079 91.618 21.7509 89.411 21.7509C87.22 21.7509 85.399 23.4749 85.399 26.0039C85.399 28.3879 87.204 30.2569 89.62 30.2569C91.569 30.2569 92.697 29.0649 93.165 28.3719L91.715 27.4049C91.232 28.1139 90.571 28.5809 89.62 28.5809C88.67 28.5809 87.993 28.1459 87.558 27.2919L93.245 24.9399L93.052 24.4579ZM87.252 25.8759C87.204 24.2319 88.525 23.3949 89.476 23.3949C90.217 23.3949 90.845 23.7659 91.055 24.2969L87.252 25.8759ZM82.629 29.9999H84.497V17.4989H82.629V29.9999ZM79.567 22.7019H79.503C79.084 22.2019 78.278 21.7509 77.264 21.7509C75.137 21.7509 73.188 23.6199 73.188 26.0209C73.188 28.4049 75.137 30.2579 77.264 30.2579C78.279 30.2579 79.084 29.8069 79.503 29.2919H79.567V29.9039C79.567 31.5309 78.697 32.4009 77.296 32.4009C76.152 32.4009 75.443 31.5799 75.153 30.8869L73.526 31.5639C73.993 32.6909 75.233 34.0769 77.296 34.0769C79.487 34.0769 81.34 32.7879 81.34 29.6459V22.0099H79.568V22.7019H79.567ZM77.425 28.5829C76.136 28.5829 75.057 27.5029 75.057 26.0209C75.057 24.5219 76.136 23.4269 77.425 23.4269C78.697 23.4269 79.696 24.5219 79.696 26.0209C79.696 27.5029 78.697 28.5829 77.425 28.5829ZM101.806 17.4989H97.335V29.9999H99.2V25.2639H101.805C103.873 25.2639 105.907 23.7669 105.907 21.3819C105.907 18.9969 103.874 17.4989 101.806 17.4989ZM101.854 23.5239H99.2V19.2389H101.854C103.249 19.2389 104.041 20.3939 104.041 21.3819C104.041 22.3499 103.249 23.5239 101.854 23.5239ZM113.386 21.7289C112.035 21.7289 110.636 22.3239 110.057 23.6429L111.713 24.3339C112.067 23.6429 112.727 23.4169 113.418 23.4169C114.383 23.4169 115.364 23.9959 115.38 25.0249V25.1539C115.042 24.9609 114.318 24.6719 113.434 24.6719C111.649 24.6719 109.831 25.6529 109.831 27.4859C109.831 29.1589 111.295 30.2359 112.935 30.2359C114.189 30.2359 114.881 29.6729 115.315 29.0129H115.379V29.9779H117.181V25.1849C117.182 22.9669 115.524 21.7289 113.386 21.7289ZM113.16 28.5799C112.55 28.5799 111.697 28.2739 111.697 27.5179C111.697 26.5529 112.759 26.1829 113.676 26.1829C114.495 26.1829 114.882 26.3599 115.38 26.6009C115.235 27.7599 114.238 28.5799 113.16 28.5799ZM123.743 22.0019L121.604 27.4219H121.54L119.32 22.0019H117.31L120.639 29.5769L118.741 33.7909H120.687L125.818 22.0019H123.743ZM106.937 29.9999H108.802V17.4989H106.937V29.9999Z"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M47.418 10.2432C47.418 11.0812 47.1701 11.7482 46.673 12.2462C46.109 12.8382 45.3731 13.1342 44.4691 13.1342C43.6031 13.1342 42.8661 12.8342 42.2611 12.2342C41.6551 11.6332 41.3521 10.8892 41.3521 10.0012C41.3521 9.11219 41.6551 8.36819 42.2611 7.76819C42.8661 7.16719 43.6031 6.86719 44.4691 6.86719C44.8991 6.86719 45.3101 6.95119 45.7001 7.11819C46.0911 7.28619 46.404 7.50919 46.6381 7.78819L46.111 8.31619C45.714 7.84119 45.167 7.60419 44.468 7.60419C43.836 7.60419 43.29 7.82619 42.829 8.27019C42.368 8.71419 42.1381 9.29119 42.1381 10.0002C42.1381 10.7092 42.368 11.2862 42.829 11.7302C43.29 12.1742 43.836 12.3962 44.468 12.3962C45.138 12.3962 45.6971 12.1732 46.1441 11.7262C46.4341 11.4352 46.602 11.0302 46.647 10.5112H44.468V9.79019H47.375C47.405 9.94719 47.418 10.0982 47.418 10.2432Z"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
<path d="M52.0281 7.73724H49.2961V9.63924H51.7601V10.3602H49.2961V12.2622H52.0281V13.0002H48.5251V7.00024H52.0281V7.73724Z" fill="white" />
|
||||||
|
<path d="M55.279 13.0002H54.508V7.73724H52.832V7.00024H56.955V7.73724H55.279V13.0002Z" fill="white" />
|
||||||
|
<path d="M59.938 13.0002V7.00024H60.709V13.0002H59.938Z" fill="white" />
|
||||||
|
<path d="M64.1281 13.0002H63.3572V7.73724H61.6812V7.00024H65.8042V7.73724H64.1281V13.0002Z" fill="white" />
|
||||||
|
<path
|
||||||
|
d="M73.6089 12.2252C73.0189 12.8312 72.2859 13.1342 71.4089 13.1342C70.5319 13.1342 69.7989 12.8312 69.2099 12.2252C68.6199 11.6192 68.3259 10.8772 68.3259 10.0002C68.3259 9.12323 68.6199 8.38123 69.2099 7.77523C69.7989 7.16923 70.5319 6.86523 71.4089 6.86523C72.2809 6.86523 73.0129 7.17023 73.6049 7.77923C74.1969 8.38823 74.4929 9.12823 74.4929 10.0002C74.4929 10.8772 74.1979 11.6192 73.6089 12.2252ZM69.7789 11.7222C70.2229 12.1722 70.7659 12.3962 71.4089 12.3962C72.0519 12.3962 72.5959 12.1712 73.0389 11.7222C73.4829 11.2722 73.7059 10.6982 73.7059 10.0002C73.7059 9.30223 73.4829 8.72823 73.0389 8.27823C72.5959 7.82823 72.0519 7.60423 71.4089 7.60423C70.7659 7.60423 70.2229 7.82923 69.7789 8.27823C69.3359 8.72823 69.1129 9.30223 69.1129 10.0002C69.1129 10.6982 69.3359 11.2722 69.7789 11.7222Z"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M75.5749 13.0002V7.00024H76.513L79.429 11.6672H79.4619L79.429 10.5112V7.00024H80.1999V13.0002H79.3949L76.344 8.10625H76.3109L76.344 9.26224V13.0002H75.5749Z"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M47.418 10.2432C47.418 11.0812 47.1701 11.7482 46.673 12.2462C46.109 12.8382 45.3731 13.1342 44.4691 13.1342C43.6031 13.1342 42.8661 12.8342 42.2611 12.2342C41.6551 11.6332 41.3521 10.8892 41.3521 10.0012C41.3521 9.11219 41.6551 8.36819 42.2611 7.76819C42.8661 7.16719 43.6031 6.86719 44.4691 6.86719C44.8991 6.86719 45.3101 6.95119 45.7001 7.11819C46.0911 7.28619 46.404 7.50919 46.6381 7.78819L46.111 8.31619C45.714 7.84119 45.167 7.60419 44.468 7.60419C43.836 7.60419 43.29 7.82619 42.829 8.27019C42.368 8.71419 42.1381 9.29119 42.1381 10.0002C42.1381 10.7092 42.368 11.2862 42.829 11.7302C43.29 12.1742 43.836 12.3962 44.468 12.3962C45.138 12.3962 45.6971 12.1732 46.1441 11.7262C46.4341 11.4352 46.602 11.0302 46.647 10.5112H44.468V9.79019H47.375C47.405 9.94719 47.418 10.0982 47.418 10.2432Z"
|
||||||
|
stroke="white"
|
||||||
|
strokeWidth="0.2"
|
||||||
|
strokeMiterlimit="10"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M52.0281 7.73724H49.2961V9.63924H51.7601V10.3602H49.2961V12.2622H52.0281V13.0002H48.5251V7.00024H52.0281V7.73724Z"
|
||||||
|
stroke="white"
|
||||||
|
strokeWidth="0.2"
|
||||||
|
strokeMiterlimit="10"
|
||||||
|
/>
|
||||||
|
<path d="M55.279 13.0002H54.508V7.73724H52.832V7.00024H56.955V7.73724H55.279V13.0002Z" stroke="white" strokeWidth="0.2" strokeMiterlimit="10" />
|
||||||
|
<path d="M59.938 13.0002V7.00024H60.709V13.0002H59.938Z" stroke="white" strokeWidth="0.2" strokeMiterlimit="10" />
|
||||||
|
<path
|
||||||
|
d="M64.1281 13.0002H63.3572V7.73724H61.6812V7.00024H65.8042V7.73724H64.1281V13.0002Z"
|
||||||
|
stroke="white"
|
||||||
|
strokeWidth="0.2"
|
||||||
|
strokeMiterlimit="10"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M73.6089 12.2252C73.0189 12.8312 72.2859 13.1342 71.4089 13.1342C70.5319 13.1342 69.7989 12.8312 69.2099 12.2252C68.6199 11.6192 68.3259 10.8772 68.3259 10.0002C68.3259 9.12323 68.6199 8.38123 69.2099 7.77523C69.7989 7.16923 70.5319 6.86523 71.4089 6.86523C72.2809 6.86523 73.0129 7.17023 73.6049 7.77923C74.1969 8.38823 74.4929 9.12823 74.4929 10.0002C74.4929 10.8772 74.1979 11.6192 73.6089 12.2252ZM69.7789 11.7222C70.2229 12.1722 70.7659 12.3962 71.4089 12.3962C72.0519 12.3962 72.5959 12.1712 73.0389 11.7222C73.4829 11.2722 73.7059 10.6982 73.7059 10.0002C73.7059 9.30223 73.4829 8.72823 73.0389 8.27823C72.5959 7.82823 72.0519 7.60423 71.4089 7.60423C70.7659 7.60423 70.2229 7.82923 69.7789 8.27823C69.3359 8.72823 69.1129 9.30223 69.1129 10.0002C69.1129 10.6982 69.3359 11.2722 69.7789 11.7222Z"
|
||||||
|
stroke="white"
|
||||||
|
strokeWidth="0.2"
|
||||||
|
strokeMiterlimit="10"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M75.5749 13.0002V7.00024H76.513L79.429 11.6672H79.4619L79.429 10.5112V7.00024H80.1999V13.0002H79.3949L76.344 8.10625H76.3109L76.344 9.26224V13.0002H75.5749Z"
|
||||||
|
stroke="white"
|
||||||
|
strokeWidth="0.2"
|
||||||
|
strokeMiterlimit="10"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M10.1565 7.96617C10.0384 8.23367 9.97314 8.56144 9.97314 8.94269V31.0587C9.97314 31.4408 10.0385 31.7686 10.1567 32.036L22.1907 20.0003L10.1565 7.96617ZM10.8517 32.7552C11.2978 32.9461 11.8797 32.8855 12.5141 32.5257L26.6712 24.4809L22.8978 20.7074L10.8517 32.7552ZM27.5737 23.9691L32.0151 21.4457C33.4121 20.6507 33.4121 19.3517 32.0151 18.5577L27.5717 16.0328L23.6048 20.0003L27.5737 23.9691ZM26.6699 15.5204L12.5141 7.4767C11.8796 7.11612 11.2977 7.05596 10.8516 7.2471L22.8977 19.2932L26.6699 15.5204Z"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AppStoreButton = ({ size = "md", ...props }: AnchorHTMLAttributes<HTMLAnchorElement> & { size?: "md" | "lg" }) => {
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
aria-label="Download on the App Store"
|
||||||
|
href="#"
|
||||||
|
{...props}
|
||||||
|
className={cx(
|
||||||
|
"rounded-[7px] bg-black ring-1 ring-app-store-badge-border outline-focus-ring ring-inset focus-visible:outline-2 focus-visible:outline-offset-2",
|
||||||
|
props.className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<svg width={size === "md" ? 120 : 132} height={size === "md" ? 40 : 44} viewBox="0 0 120 40" fill="none">
|
||||||
|
<path
|
||||||
|
d="M81.5257 19.2009V21.4919H80.0896V22.9944H81.5257V28.0994C81.5257 29.8425 82.3143 30.5398 84.2981 30.5398C84.6468 30.5398 84.9788 30.4983 85.2693 30.4485V28.9626C85.0203 28.9875 84.8626 29.0041 84.5887 29.0041C83.7005 29.0041 83.3104 28.5891 83.3104 27.6428V22.9944H85.2693V21.4919H83.3104V19.2009H81.5257Z"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M90.3232 30.6643C92.9628 30.6643 94.5815 28.8962 94.5815 25.9661C94.5815 23.0525 92.9545 21.2761 90.3232 21.2761C87.6835 21.2761 86.0566 23.0525 86.0566 25.9661C86.0566 28.8962 87.6752 30.6643 90.3232 30.6643ZM90.3232 29.0789C88.7709 29.0789 87.8994 27.9416 87.8994 25.9661C87.8994 24.0071 88.7709 22.8616 90.3232 22.8616C91.8671 22.8616 92.747 24.0071 92.747 25.9661C92.747 27.9333 91.8671 29.0789 90.3232 29.0789Z"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M95.9664 30.49H97.7511V25.1526C97.7511 23.8826 98.7056 23.0276 100.059 23.0276C100.374 23.0276 100.905 23.0857 101.055 23.1355V21.3757C100.864 21.3259 100.524 21.301 100.258 21.301C99.0792 21.301 98.0748 21.9485 97.8175 22.8367H97.6846V21.4504H95.9664V30.49Z"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M105.486 22.7952C106.806 22.7952 107.669 23.7165 107.711 25.136H103.145C103.245 23.7248 104.166 22.7952 105.486 22.7952ZM107.702 28.0496C107.37 28.7551 106.632 29.1453 105.552 29.1453C104.125 29.1453 103.203 28.1409 103.145 26.5554V26.4558H109.529V25.8332C109.529 22.9944 108.009 21.2761 105.494 21.2761C102.946 21.2761 101.327 23.1106 101.327 25.9993C101.327 28.8879 102.913 30.6643 105.503 30.6643C107.57 30.6643 109.014 29.6682 109.421 28.0496H107.702Z"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M69.8221 27.1518C69.9598 29.3715 71.8095 30.7911 74.5626 30.7911C77.505 30.7911 79.3462 29.3027 79.3462 26.9281C79.3462 25.0612 78.2966 24.0287 75.7499 23.4351L74.382 23.0996C72.7645 22.721 72.1106 22.2134 72.1106 21.3272C72.1106 20.2088 73.1259 19.4775 74.6487 19.4775C76.0941 19.4775 77.0921 20.1916 77.2727 21.3358H79.1483C79.0365 19.2452 77.1953 17.774 74.6745 17.774C71.9644 17.774 70.1576 19.2452 70.1576 21.4563C70.1576 23.2802 71.1815 24.3643 73.427 24.8891L75.0272 25.2763C76.6705 25.6634 77.3932 26.2312 77.3932 27.1776C77.3932 28.2789 76.2575 29.079 74.7089 29.079C73.0484 29.079 71.8955 28.3305 71.7321 27.1518H69.8221Z"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M51.3348 21.301C50.1063 21.301 49.0437 21.9153 48.4959 22.9446H48.3631V21.4504H46.6448V33.4949H48.4295V29.1204H48.5706C49.0437 30.0749 50.0647 30.6394 51.3514 30.6394C53.6341 30.6394 55.0867 28.8381 55.0867 25.9661C55.0867 23.094 53.6341 21.301 51.3348 21.301ZM50.8284 29.0373C49.3343 29.0373 48.3963 27.8586 48.3963 25.9744C48.3963 24.0818 49.3343 22.9031 50.8367 22.9031C52.3475 22.9031 53.2522 24.0569 53.2522 25.9661C53.2522 27.8835 52.3475 29.0373 50.8284 29.0373Z"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M61.3316 21.301C60.103 21.301 59.0405 21.9153 58.4927 22.9446H58.3599V21.4504H56.6416V33.4949H58.4263V29.1204H58.5674C59.0405 30.0749 60.0615 30.6394 61.3482 30.6394C63.6309 30.6394 65.0835 28.8381 65.0835 25.9661C65.0835 23.094 63.6309 21.301 61.3316 21.301ZM60.8252 29.0373C59.3311 29.0373 58.3931 27.8586 58.3931 25.9744C58.3931 24.0818 59.3311 22.9031 60.8335 22.9031C62.3443 22.9031 63.249 24.0569 63.249 25.9661C63.249 27.8835 62.3443 29.0373 60.8252 29.0373Z"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M43.4428 30.49H45.4905L41.008 18.0751H38.9346L34.4521 30.49H36.431L37.5752 27.1948H42.3072L43.4428 30.49ZM39.8724 20.3292H40.0186L41.8168 25.5774H38.0656L39.8724 20.3292Z"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M35.6514 8.71094V14.7H37.8137C39.5984 14.7 40.6318 13.6001 40.6318 11.6868C40.6318 9.80249 39.5901 8.71094 37.8137 8.71094H35.6514ZM36.5811 9.55762H37.71C38.9509 9.55762 39.6855 10.3462 39.6855 11.6992C39.6855 13.073 38.9634 13.8533 37.71 13.8533H36.5811V9.55762Z"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M43.7969 14.7871C45.1167 14.7871 45.9261 13.9031 45.9261 12.438C45.9261 10.9812 45.1126 10.093 43.7969 10.093C42.4771 10.093 41.6636 10.9812 41.6636 12.438C41.6636 13.9031 42.4729 14.7871 43.7969 14.7871ZM43.7969 13.9944C43.0208 13.9944 42.585 13.4258 42.585 12.438C42.585 11.4585 43.0208 10.8857 43.7969 10.8857C44.5689 10.8857 45.0088 11.4585 45.0088 12.438C45.0088 13.4216 44.5689 13.9944 43.7969 13.9944Z"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M52.8182 10.1802H51.9259L51.1207 13.6292H51.0501L50.1205 10.1802H49.2655L48.3358 13.6292H48.2694L47.4601 10.1802H46.5553L47.8004 14.7H48.7176L49.6473 11.3713H49.7179L50.6517 14.7H51.5772L52.8182 10.1802Z"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M53.8458 14.7H54.7382V12.0562C54.7382 11.3506 55.1574 10.9106 55.8173 10.9106C56.4772 10.9106 56.7926 11.2717 56.7926 11.998V14.7H57.685V11.7739C57.685 10.699 57.1288 10.093 56.1203 10.093C55.4396 10.093 54.9914 10.396 54.7714 10.8982H54.705V10.1802H53.8458V14.7Z"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
<path d="M59.0903 14.7H59.9826V8.41626H59.0903V14.7Z" fill="white" />
|
||||||
|
<path
|
||||||
|
d="M63.3386 14.7871C64.6584 14.7871 65.4678 13.9031 65.4678 12.438C65.4678 10.9812 64.6543 10.093 63.3386 10.093C62.0188 10.093 61.2053 10.9812 61.2053 12.438C61.2053 13.9031 62.0146 14.7871 63.3386 14.7871ZM63.3386 13.9944C62.5625 13.9944 62.1267 13.4258 62.1267 12.438C62.1267 11.4585 62.5625 10.8857 63.3386 10.8857C64.1106 10.8857 64.5505 11.4585 64.5505 12.438C64.5505 13.4216 64.1106 13.9944 63.3386 13.9944Z"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M68.1265 14.0234C67.6409 14.0234 67.2881 13.7869 67.2881 13.3801C67.2881 12.9817 67.5704 12.77 68.1929 12.7285L69.2969 12.658V13.0356C69.2969 13.5959 68.7989 14.0234 68.1265 14.0234ZM67.8982 14.7747C68.4917 14.7747 68.9856 14.5173 69.2554 14.0649H69.326V14.7H70.1851V11.6121C70.1851 10.6575 69.5459 10.093 68.4129 10.093C67.3877 10.093 66.6573 10.5911 66.566 11.3672H67.4292C67.5289 11.0476 67.8733 10.865 68.3714 10.865C68.9815 10.865 69.2969 11.1348 69.2969 11.6121V12.0022L68.0726 12.0728C66.9976 12.1392 66.3916 12.6082 66.3916 13.4216C66.3916 14.2476 67.0267 14.7747 67.8982 14.7747Z"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M73.2132 14.7747C73.8358 14.7747 74.3629 14.48 74.6327 13.9861H74.7032V14.7H75.5582V8.41626H74.6659V10.8982H74.5995C74.3546 10.4001 73.8316 10.1055 73.2132 10.1055C72.0719 10.1055 71.3373 11.0103 71.3373 12.438C71.3373 13.8699 72.0636 14.7747 73.2132 14.7747ZM73.4664 10.9065C74.2135 10.9065 74.6825 11.5 74.6825 12.4421C74.6825 13.3884 74.2176 13.9736 73.4664 13.9736C72.711 13.9736 72.2586 13.3967 72.2586 12.438C72.2586 11.4875 72.7152 10.9065 73.4664 10.9065Z"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M81.3447 14.7871C82.6645 14.7871 83.4738 13.9031 83.4738 12.438C83.4738 10.9812 82.6604 10.093 81.3447 10.093C80.0249 10.093 79.2114 10.9812 79.2114 12.438C79.2114 13.9031 80.0207 14.7871 81.3447 14.7871ZM81.3447 13.9944C80.5686 13.9944 80.1328 13.4258 80.1328 12.438C80.1328 11.4585 80.5686 10.8857 81.3447 10.8857C82.1166 10.8857 82.5566 11.4585 82.5566 12.438C82.5566 13.4216 82.1166 13.9944 81.3447 13.9944Z"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M84.655 14.7H85.5474V12.0562C85.5474 11.3506 85.9666 10.9106 86.6265 10.9106C87.2864 10.9106 87.6018 11.2717 87.6018 11.998V14.7H88.4941V11.7739C88.4941 10.699 87.938 10.093 86.9294 10.093C86.2488 10.093 85.8005 10.396 85.5806 10.8982H85.5142V10.1802H84.655V14.7Z"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M92.6039 9.05542V10.2009H91.8858V10.9521H92.6039V13.5046C92.6039 14.3762 92.9981 14.7249 93.9901 14.7249C94.1644 14.7249 94.3304 14.7041 94.4757 14.6792V13.9363C94.3512 13.9487 94.2723 13.957 94.1353 13.957C93.6913 13.957 93.4962 13.7495 93.4962 13.2764V10.9521H94.4757V10.2009H93.4962V9.05542H92.6039Z"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M95.6735 14.7H96.5658V12.0603C96.5658 11.3755 96.9726 10.9148 97.703 10.9148C98.3339 10.9148 98.6701 11.28 98.6701 12.0022V14.7H99.5624V11.7822C99.5624 10.7073 98.9689 10.0972 98.006 10.0972C97.3253 10.0972 96.848 10.4001 96.6281 10.9065H96.5575V8.41626H95.6735V14.7Z"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M102.781 10.8525C103.441 10.8525 103.873 11.3132 103.894 12.0229H101.611C101.661 11.3174 102.122 10.8525 102.781 10.8525ZM103.89 13.4797C103.724 13.8325 103.354 14.0276 102.815 14.0276C102.101 14.0276 101.64 13.5254 101.611 12.7327V12.6829H104.803V12.3716C104.803 10.9521 104.043 10.093 102.786 10.093C101.511 10.093 100.702 11.0103 100.702 12.4546C100.702 13.8989 101.495 14.7871 102.79 14.7871C103.823 14.7871 104.545 14.2891 104.749 13.4797H103.89Z"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M24.769 20.3008C24.7907 18.6198 25.6934 17.0292 27.1256 16.1488C26.2221 14.8584 24.7088 14.0403 23.1344 13.9911C21.4552 13.8148 19.8272 14.9959 18.9715 14.9959C18.0992 14.9959 16.7817 14.0086 15.363 14.0378C13.5137 14.0975 11.7898 15.1489 10.8901 16.7656C8.95607 20.1141 10.3987 25.0351 12.2513 27.7417C13.1782 29.0671 14.2615 30.5475 15.6789 30.495C17.066 30.4375 17.584 29.6105 19.2583 29.6105C20.9171 29.6105 21.4031 30.495 22.8493 30.4616C24.3377 30.4375 25.2754 29.1304 26.1698 27.7925C26.8358 26.8481 27.3483 25.8044 27.6882 24.7C25.9391 23.9602 24.771 22.2 24.769 20.3008Z"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M22.0373 12.2111C22.8489 11.2369 23.2487 9.98469 23.1518 8.72046C21.912 8.85068 20.7668 9.44324 19.9443 10.3801C19.14 11.2954 18.7214 12.5255 18.8006 13.7415C20.0408 13.7542 21.2601 13.1777 22.0373 12.2111Z"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const GalaxyStoreButton = ({ size = "md", ...props }: AnchorHTMLAttributes<HTMLAnchorElement> & { size?: "md" | "lg" }) => {
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
aria-label="Available on Galaxy Store"
|
||||||
|
href="#"
|
||||||
|
{...props}
|
||||||
|
className={cx(
|
||||||
|
"rounded-[7px] bg-black ring-1 ring-app-store-badge-border outline-focus-ring ring-inset focus-visible:outline-2 focus-visible:outline-offset-2",
|
||||||
|
props.className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<svg width={size === "md" ? 147 : 162} height={size === "md" ? 40 : 44} viewBox="0 0 147 40" fill="none">
|
||||||
|
<path d="M64.7516 20.3987H66.7715V31.3885H64.7516V20.3987Z" fill="white" />
|
||||||
|
<path
|
||||||
|
d="M42.5 25.9699C42.5 22.8811 44.8314 20.4009 48.039 20.4009C50.2816 20.4009 52.0489 21.5444 52.9695 23.1779L51.1875 24.2473C50.5193 23.0146 49.4799 22.3611 48.054 22.3611C46.0343 22.3611 44.5047 23.9946 44.5047 25.9699C44.5047 27.9745 46.0196 29.5786 48.1281 29.5786C49.7469 29.5786 50.8757 28.6578 51.2616 27.2322H47.8017V25.3017H53.4298V26.1037C53.4298 29.0289 51.3657 31.5392 48.1281 31.5392C44.7423 31.5392 42.5 28.9695 42.5 25.9699Z"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M54.3525 27.0543C54.3525 24.1732 56.4613 22.525 58.6592 22.525C59.8027 22.525 60.8274 22.9999 61.4512 23.7426V22.7032H63.4558V31.3907H61.4512V30.2616C60.8124 31.0636 59.773 31.5689 58.6295 31.5689C56.5354 31.5689 54.3525 29.8904 54.3525 27.0543ZM61.555 27.0246C61.555 25.5543 60.4412 24.3514 58.9562 24.3514C57.4713 24.3514 56.3278 25.5249 56.3278 27.0246C56.3278 28.539 57.4713 29.7272 58.9562 29.7272C60.4412 29.7272 61.555 28.5243 61.555 27.0246Z"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M67.7938 27.0543C67.7938 24.1732 69.9026 22.525 72.1005 22.525C73.244 22.525 74.2686 22.9999 74.8925 23.7426V22.7032H76.8971V31.3907H74.8925V30.2616C74.2539 31.0636 73.2146 31.5689 72.0707 31.5689C69.977 31.5689 67.7938 29.8904 67.7938 27.0543ZM74.9966 27.0246C74.9966 25.5543 73.8828 24.3514 72.3981 24.3514C70.9128 24.3514 69.7693 25.5249 69.7693 27.0246C69.7693 28.539 70.9128 29.7272 72.3981 29.7272C73.8825 29.7272 74.9966 28.5243 74.9966 27.0246Z"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M80.8048 26.9652L77.6566 22.7032H80.1072L82.0818 25.4652L84.0424 22.7032H86.4182L83.2994 26.9949L86.6261 31.3907H84.1759L82.0524 28.4949L79.988 31.3907H77.5375L80.8048 26.9652Z"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
<path d="M90.3846 30.9598L86.8206 22.7029H88.9588L91.3796 28.554L93.6663 22.7029H95.7757L90.5926 35.4H88.5582L90.3846 30.9598Z" fill="white" />
|
||||||
|
<path
|
||||||
|
d="M99.8907 29.5936L101.732 28.1384C102.282 29.074 103.128 29.6083 104.108 29.6083C105.178 29.6083 105.757 28.911 105.757 28.1531C105.757 27.2325 104.658 26.9505 103.499 26.594C102.044 26.1334 100.426 25.5693 100.426 23.5049C100.426 21.7676 101.94 20.4012 104.019 20.4012C105.772 20.4012 106.781 21.0697 107.658 21.9756L105.994 23.2376C105.534 22.5547 104.895 22.1982 104.034 22.1982C103.054 22.1982 102.519 22.7329 102.519 23.4305C102.519 24.292 103.559 24.5743 104.732 24.9605C106.203 25.4355 107.866 26.089 107.866 28.1681C107.866 29.8757 106.499 31.5395 104.123 31.5395C102.163 31.5392 100.871 30.7071 99.8907 29.5936Z"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M108.03 22.7029H109.515V21.3812L111.535 20V22.7029H113.302V24.4999H111.535V27.7519C111.535 29.2669 111.743 29.5045 113.302 29.5045V31.3904H113.02C110.332 31.3904 109.515 30.5292 109.515 27.7672V24.4999H108.03L108.03 22.7029Z"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M113.838 27.0543C113.838 24.5296 115.827 22.5247 118.337 22.5247C120.832 22.5247 122.837 24.5296 122.837 27.0543C122.837 29.5639 120.832 31.5689 118.337 31.5689C115.827 31.5689 113.838 29.5639 113.838 27.0543ZM120.862 27.0543C120.862 25.599 119.747 24.4108 118.337 24.4108C116.896 24.4108 115.812 25.599 115.812 27.0543C115.812 28.4949 116.896 29.6828 118.337 29.6828C119.747 29.6828 120.862 28.4949 120.862 27.0543Z"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M125.853 22.7029V23.9949C126.254 23.1335 126.981 22.7029 128.08 22.7029C128.704 22.7029 129.223 22.8514 129.61 23.0741L128.852 24.9749C128.555 24.782 128.214 24.6334 127.649 24.6334C126.491 24.6334 125.867 25.2573 125.867 26.7569V31.3904H123.862V22.7029H125.853Z"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M129.834 27.069C129.834 24.5296 131.808 22.5247 134.348 22.5247C136.932 22.5247 138.818 24.4255 138.818 26.9949V27.7519H131.749C132.017 28.9701 132.997 29.8016 134.423 29.8016C135.536 29.8016 136.413 29.1928 136.828 28.2719L138.477 29.2222C137.719 30.6186 136.353 31.5686 134.423 31.5686C131.69 31.5686 129.834 29.5786 129.834 27.069ZM131.853 26.074H136.784C136.487 24.9158 135.596 24.292 134.348 24.292C133.145 24.292 132.21 25.0196 131.853 26.074Z"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M46.6986 13.974H43.9018L43.3866 15.4H42.5034L44.8218 9.0244H45.7878L48.097 15.4H47.2138L46.6986 13.974ZM46.4594 13.2932L45.3002 10.0548L44.141 13.2932H46.4594Z"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
<path d="M50.4322 14.6272L51.9962 10.3584H52.8886L50.9106 15.4H49.9354L47.9574 10.3584H48.859L50.4322 14.6272Z" fill="white" />
|
||||||
|
<path
|
||||||
|
d="M53.4917 12.8608C53.4917 12.3456 53.5959 11.8948 53.8045 11.5084C54.013 11.1159 54.2982 10.8123 54.6601 10.5976C55.0281 10.3829 55.4359 10.2756 55.8837 10.2756C56.3253 10.2756 56.7086 10.3707 57.0337 10.5608C57.3587 10.7509 57.601 10.9901 57.7605 11.2784V10.3584H58.6069V15.4H57.7605V14.4616C57.5949 14.756 57.3465 15.0013 57.0153 15.1976C56.6902 15.3877 56.3099 15.4828 55.8745 15.4828C55.4267 15.4828 55.0219 15.3724 54.6601 15.1516C54.2982 14.9308 54.013 14.6211 53.8045 14.2224C53.5959 13.8237 53.4917 13.3699 53.4917 12.8608ZM57.7605 12.87C57.7605 12.4897 57.6838 12.1585 57.5305 11.8764C57.3771 11.5943 57.1686 11.3796 56.9049 11.2324C56.6473 11.0791 56.3621 11.0024 56.0493 11.0024C55.7365 11.0024 55.4513 11.076 55.1937 11.2232C54.9361 11.3704 54.7306 11.5851 54.5773 11.8672C54.4239 12.1493 54.3473 12.4805 54.3473 12.8608C54.3473 13.2472 54.4239 13.5845 54.5773 13.8728C54.7306 14.1549 54.9361 14.3727 55.1937 14.526C55.4513 14.6732 55.7365 14.7468 56.0493 14.7468C56.3621 14.7468 56.6473 14.6732 56.9049 14.526C57.1686 14.3727 57.3771 14.1549 57.5305 13.8728C57.6838 13.5845 57.7605 13.2503 57.7605 12.87Z"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M60.2701 9.5396C60.1106 9.5396 59.9757 9.4844 59.8653 9.374C59.7549 9.2636 59.6997 9.12867 59.6997 8.9692C59.6997 8.80974 59.7549 8.6748 59.8653 8.5644C59.9757 8.454 60.1106 8.3988 60.2701 8.3988C60.4234 8.3988 60.5522 8.454 60.6565 8.5644C60.7669 8.6748 60.8221 8.80974 60.8221 8.9692C60.8221 9.12867 60.7669 9.2636 60.6565 9.374C60.5522 9.4844 60.4234 9.5396 60.2701 9.5396ZM60.6749 10.3584V15.4H59.8377V10.3584H60.6749Z"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
<path d="M62.7549 8.592V15.4H61.9177V8.592H62.7549Z" fill="white" />
|
||||||
|
<path
|
||||||
|
d="M63.869 12.8608C63.869 12.3456 63.9732 11.8948 64.1818 11.5084C64.3903 11.1159 64.6755 10.8123 65.0374 10.5976C65.4054 10.3829 65.8132 10.2756 66.261 10.2756C66.7026 10.2756 67.0859 10.3707 67.411 10.5608C67.736 10.7509 67.9783 10.9901 68.1378 11.2784V10.3584H68.9842V15.4H68.1378V14.4616C67.9722 14.756 67.7238 15.0013 67.3926 15.1976C67.0675 15.3877 66.6872 15.4828 66.2518 15.4828C65.804 15.4828 65.3992 15.3724 65.0374 15.1516C64.6755 14.9308 64.3903 14.6211 64.1818 14.2224C63.9732 13.8237 63.869 13.3699 63.869 12.8608ZM68.1378 12.87C68.1378 12.4897 68.0611 12.1585 67.9078 11.8764C67.7544 11.5943 67.5459 11.3796 67.2822 11.2324C67.0246 11.0791 66.7394 11.0024 66.4266 11.0024C66.1138 11.0024 65.8286 11.076 65.571 11.2232C65.3134 11.3704 65.1079 11.5851 64.9546 11.8672C64.8012 12.1493 64.7246 12.4805 64.7246 12.8608C64.7246 13.2472 64.8012 13.5845 64.9546 13.8728C65.1079 14.1549 65.3134 14.3727 65.571 14.526C65.8286 14.6732 66.1138 14.7468 66.4266 14.7468C66.7394 14.7468 67.0246 14.6732 67.2822 14.526C67.5459 14.3727 67.7544 14.1549 67.9078 13.8728C68.0611 13.5845 68.1378 13.2503 68.1378 12.87Z"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M71.3282 11.2968C71.4999 10.9963 71.7514 10.7509 72.0826 10.5608C72.4138 10.3707 72.791 10.2756 73.2142 10.2756C73.668 10.2756 74.0759 10.3829 74.4378 10.5976C74.7996 10.8123 75.0848 11.1159 75.2934 11.5084C75.5019 11.8948 75.6062 12.3456 75.6062 12.8608C75.6062 13.3699 75.5019 13.8237 75.2934 14.2224C75.0848 14.6211 74.7966 14.9308 74.4286 15.1516C74.0667 15.3724 73.6619 15.4828 73.2142 15.4828C72.7787 15.4828 72.3954 15.3877 72.0642 15.1976C71.7391 15.0075 71.4938 14.7652 71.3282 14.4708V15.4H70.491V8.592H71.3282V11.2968ZM74.7506 12.8608C74.7506 12.4805 74.6739 12.1493 74.5206 11.8672C74.3672 11.5851 74.1587 11.3704 73.895 11.2232C73.6374 11.076 73.3522 11.0024 73.0394 11.0024C72.7327 11.0024 72.4475 11.0791 72.1838 11.2324C71.9262 11.3796 71.7176 11.5973 71.5582 11.8856C71.4048 12.1677 71.3282 12.4959 71.3282 12.87C71.3282 13.2503 71.4048 13.5845 71.5582 13.8728C71.7176 14.1549 71.9262 14.3727 72.1838 14.526C72.4475 14.6732 72.7327 14.7468 73.0394 14.7468C73.3522 14.7468 73.6374 14.6732 73.895 14.526C74.1587 14.3727 74.3672 14.1549 74.5206 13.8728C74.6739 13.5845 74.7506 13.2472 74.7506 12.8608Z"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
<path d="M77.5454 8.592V15.4H76.7082V8.592H77.5454Z" fill="white" />
|
||||||
|
<path
|
||||||
|
d="M83.6642 12.686C83.6642 12.8455 83.655 13.0141 83.6366 13.192H79.607C79.6377 13.6888 79.8064 14.0783 80.113 14.3604C80.4258 14.6364 80.803 14.7744 81.2446 14.7744C81.6065 14.7744 81.907 14.6916 82.1462 14.526C82.3916 14.3543 82.5633 14.1273 82.6614 13.8452H83.563C83.4281 14.3297 83.1582 14.7253 82.7534 15.032C82.3486 15.3325 81.8457 15.4828 81.2446 15.4828C80.7662 15.4828 80.3369 15.3755 79.9566 15.1608C79.5825 14.9461 79.2881 14.6425 79.0734 14.25C78.8587 13.8513 78.7514 13.3913 78.7514 12.87C78.7514 12.3487 78.8557 11.8917 79.0642 11.4992C79.2728 11.1067 79.5641 10.8061 79.9382 10.5976C80.3185 10.3829 80.754 10.2756 81.2446 10.2756C81.723 10.2756 82.1462 10.3799 82.5142 10.5884C82.8822 10.7969 83.1643 11.0852 83.3606 11.4532C83.563 11.8151 83.6642 12.226 83.6642 12.686ZM82.7994 12.5112C82.7994 12.1923 82.7289 11.9193 82.5878 11.6924C82.4467 11.4593 82.2536 11.2845 82.0082 11.168C81.769 11.0453 81.5022 10.984 81.2078 10.984C80.7846 10.984 80.4227 11.1189 80.1222 11.3888C79.8278 11.6587 79.6591 12.0328 79.6162 12.5112H82.7994Z"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M89.6048 15.4828C89.1326 15.4828 88.7032 15.3755 88.3168 15.1608C87.9366 14.9461 87.636 14.6425 87.4152 14.25C87.2006 13.8513 87.0932 13.3913 87.0932 12.87C87.0932 12.3548 87.2036 11.9009 87.4244 11.5084C87.6514 11.1097 87.958 10.8061 88.3444 10.5976C88.7308 10.3829 89.1632 10.2756 89.6416 10.2756C90.12 10.2756 90.5524 10.3829 90.9388 10.5976C91.3252 10.8061 91.6288 11.1067 91.8496 11.4992C92.0766 11.8917 92.19 12.3487 92.19 12.87C92.19 13.3913 92.0735 13.8513 91.8404 14.25C91.6135 14.6425 91.3038 14.9461 90.9112 15.1608C90.5187 15.3755 90.0832 15.4828 89.6048 15.4828ZM89.6048 14.7468C89.9054 14.7468 90.1875 14.6763 90.4512 14.5352C90.715 14.3941 90.9266 14.1825 91.086 13.9004C91.2516 13.6183 91.3344 13.2748 91.3344 12.87C91.3344 12.4652 91.2547 12.1217 91.0952 11.8396C90.9358 11.5575 90.7272 11.3489 90.4696 11.214C90.212 11.0729 89.933 11.0024 89.6324 11.0024C89.3258 11.0024 89.0436 11.0729 88.786 11.214C88.5346 11.3489 88.3322 11.5575 88.1788 11.8396C88.0255 12.1217 87.9488 12.4652 87.9488 12.87C87.9488 13.2809 88.0224 13.6275 88.1696 13.9096C88.323 14.1917 88.5254 14.4033 88.7768 14.5444C89.0283 14.6793 89.3043 14.7468 89.6048 14.7468Z"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M95.9312 10.2664C96.5445 10.2664 97.0413 10.4535 97.4216 10.8276C97.8019 11.1956 97.992 11.7292 97.992 12.4284V15.4H97.164V12.548C97.164 12.0451 97.0383 11.6617 96.7868 11.398C96.5353 11.1281 96.1919 10.9932 95.7564 10.9932C95.3148 10.9932 94.9621 11.1312 94.6984 11.4072C94.4408 11.6832 94.312 12.0849 94.312 12.6124V15.4H93.4748V10.3584H94.312V11.076C94.4776 10.8184 94.7015 10.6191 94.9836 10.478C95.2719 10.3369 95.5877 10.2664 95.9312 10.2664Z"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
<rect x="9" y="7" width="26" height="26" rx="10" fill="url(#paint0_angular_1303_2200)" />
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M25.7914 16.5695L25.738 16.3341C25.738 14.2733 24.0609 12.5968 21.9997 12.5968C19.9385 12.5968 18.2617 14.2733 18.2617 16.3341L18.2081 16.5695H15.1387L15.966 24.2826C16.1318 25.8515 17.4549 27.0421 19.0326 27.0421H24.9669C26.5446 27.0421 27.8677 25.8515 28.0334 24.2828L28.8609 16.5695H25.7914ZM19.8331 16.5695L19.8934 16.3341C19.8934 15.1729 20.8383 14.2281 21.9997 14.2281C23.1613 14.2281 24.1063 15.1729 24.1063 16.3341L24.1664 16.5695H19.8331Z"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
<defs>
|
||||||
|
<radialGradient
|
||||||
|
id="paint0_angular_1303_2200"
|
||||||
|
cx="0"
|
||||||
|
cy="0"
|
||||||
|
r="1"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
gradientTransform="translate(22 20) rotate(88.8309) scale(13.0027)"
|
||||||
|
>
|
||||||
|
<stop offset="0.000415415" stopColor="#F4605E" />
|
||||||
|
<stop offset="0.0642377" stopColor="#E94B80" />
|
||||||
|
<stop offset="0.128396" stopColor="#DE33A4" />
|
||||||
|
<stop offset="0.186654" stopColor="#D41AC8" />
|
||||||
|
<stop offset="0.250949" stopColor="#CB06E5" />
|
||||||
|
<stop offset="0.281719" stopColor="#C902EC" />
|
||||||
|
<stop offset="0.316858" stopColor="#CB04E5" />
|
||||||
|
<stop offset="0.371347" stopColor="#D108D3" />
|
||||||
|
<stop offset="0.43351" stopColor="#D80DBA" />
|
||||||
|
<stop offset="0.504024" stopColor="#E1139E" />
|
||||||
|
<stop offset="0.59576" stopColor="#EC1E7B" />
|
||||||
|
<stop offset="0.673288" stopColor="#F22A65" />
|
||||||
|
<stop offset="0.713795" stopColor="#F5355B" />
|
||||||
|
<stop offset="0.754495" stopColor="#F74452" />
|
||||||
|
<stop offset="0.818581" stopColor="#F75651" />
|
||||||
|
<stop offset="0.878478" stopColor="#F76051" />
|
||||||
|
<stop offset="0.938046" stopColor="#F76551" />
|
||||||
|
</radialGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AppGalleryButton = ({ size = "md", ...props }: AnchorHTMLAttributes<HTMLAnchorElement> & { size?: "md" | "lg" }) => {
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
aria-label="Explore it on AppGallery"
|
||||||
|
href="#"
|
||||||
|
{...props}
|
||||||
|
className={cx(
|
||||||
|
"rounded-[7px] bg-black ring-1 ring-app-store-badge-border outline-focus-ring ring-inset focus-visible:outline-2 focus-visible:outline-offset-2",
|
||||||
|
props.className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<svg width={size === "md" ? 133 : 147} height={size === "md" ? 40 : 44} viewBox="0 0 133 40" fill="none">
|
||||||
|
<path
|
||||||
|
d="M45.3962 25.4116H48.8919L47.6404 22.0615C47.4682 21.5986 47.2989 21.0875 47.1319 20.5276C46.9813 21.0229 46.817 21.5286 46.6394 22.0453L45.3962 25.4116ZM49.4893 27.0021H44.8068L43.6607 30.1344H41.6021L46.1874 18.4368H48.133L52.8234 30.1344H50.6599L49.4893 27.0021Z"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M58.8962 28.0072C59.3026 27.461 59.5058 26.663 59.5058 25.6135C59.5058 24.6396 59.3375 23.933 59.0013 23.4942C58.6647 23.0557 58.2167 22.8364 57.657 22.8364C57.2695 22.8364 56.9117 22.9281 56.5834 23.1109C56.2551 23.2939 55.9429 23.5387 55.6469 23.8455V28.5117C55.8461 28.6085 56.0775 28.6853 56.3413 28.7416C56.605 28.7984 56.8661 28.8266 57.1242 28.8266C57.8994 28.8266 58.4898 28.5533 58.8962 28.0072ZM53.653 23.555C53.653 22.9092 53.6314 22.1986 53.5885 21.4237H55.4613C55.5311 21.7844 55.5797 22.1531 55.6066 22.5297C56.3815 21.6849 57.2695 21.2622 58.2706 21.2622C58.8519 21.2622 59.3901 21.4088 59.8853 21.7021C60.3802 21.9955 60.7799 22.4583 61.0839 23.0906C61.3882 23.7231 61.5402 24.5265 61.5402 25.5005C61.5402 26.5177 61.3666 27.387 61.0194 28.108C60.6722 28.8294 60.1866 29.3754 59.5623 29.747C58.9381 30.1181 58.2167 30.3039 57.3989 30.3039C56.8066 30.3039 56.2226 30.2042 55.6469 30.0053V33.6056L53.653 33.7752V23.555Z"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M67.9935 28.0072C68.3999 27.461 68.6031 26.663 68.6031 25.6135C68.6031 24.6396 68.4349 23.933 68.0986 23.4942C67.7621 23.0557 67.3141 22.8364 66.7543 22.8364C66.3669 22.8364 66.009 22.9281 65.6807 23.1109C65.3522 23.2939 65.0402 23.5387 64.7442 23.8455V28.5117C64.9434 28.6085 65.1746 28.6853 65.4386 28.7416C65.7021 28.7984 65.9631 28.8266 66.2215 28.8266C66.9964 28.8266 67.5871 28.5533 67.9935 28.0072ZM62.7501 23.555C62.7501 22.9092 62.7285 22.1986 62.6855 21.4237H64.5586C64.6285 21.7844 64.677 22.1531 64.7039 22.5297C65.4789 21.6849 66.3669 21.2622 67.3679 21.2622C67.9493 21.2622 68.4871 21.4088 68.9826 21.7021C69.4775 21.9955 69.8772 22.4583 70.1815 23.0906C70.4852 23.7231 70.6375 24.5265 70.6375 25.5005C70.6375 26.5177 70.4637 27.387 70.1167 28.108C69.7695 28.8294 69.2837 29.3754 68.6597 29.747C68.0351 30.1181 67.314 30.3039 66.4959 30.3039C65.9039 30.3039 65.3199 30.2042 64.7442 30.0053V33.6056L62.7501 33.7752V23.555Z"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M74.3005 29.5813C73.4391 29.1053 72.7773 28.4229 72.3146 27.5349C71.8514 26.6469 71.6202 25.5974 71.6202 24.3864C71.6202 23.0734 71.8866 21.9578 72.4194 21.0401C72.9522 20.1227 73.6775 19.4337 74.5949 18.9735C75.5127 18.5134 76.5418 18.2833 77.6831 18.2833C78.3557 18.2833 78.9975 18.3574 79.6085 18.5053C80.2191 18.6534 80.7882 18.8564 81.3159 19.1149L80.8071 20.6486C79.7469 20.1428 78.7351 19.8898 77.7719 19.8898C76.9591 19.8898 76.2474 20.0634 75.6367 20.4106C75.0258 20.7578 74.5506 21.2677 74.2117 21.9404C73.8725 22.6132 73.7031 23.4258 73.7031 24.3783C73.7031 25.2128 73.8335 25.9526 74.0943 26.5984C74.3557 27.2442 74.7674 27.7557 75.3298 28.1323C75.8922 28.509 76.6013 28.6973 77.457 28.6973C77.8445 28.6973 78.2319 28.6651 78.6194 28.6005C79.0069 28.536 79.3703 28.4418 79.7093 28.3179V25.9526H77.005V24.4026H81.6469V29.3272C80.9794 29.6392 80.2783 29.8789 79.5439 30.0456C78.809 30.2123 78.0786 30.2957 77.3519 30.2957C76.1786 30.2957 75.1613 30.0579 74.3005 29.5813Z"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M87.5381 28.5197C87.9522 28.3208 88.2914 28.073 88.5551 27.7771V26.1625C88.0114 26.1034 87.5674 26.0737 87.2231 26.0737C86.3997 26.0737 85.8303 26.2068 85.5159 26.4734C85.2007 26.7394 85.0434 27.0989 85.0434 27.5509C85.0434 27.9818 85.1578 28.3005 85.3866 28.5077C85.6154 28.7149 85.9261 28.8184 86.3189 28.8184C86.717 28.8184 87.1234 28.7189 87.5381 28.5197ZM88.7327 30.1344C88.6626 29.7952 88.6167 29.4106 88.5954 28.98C88.2887 29.3461 87.8893 29.6568 87.3965 29.9125C86.9045 30.168 86.3485 30.2957 85.7295 30.2957C85.229 30.2957 84.773 30.1976 84.361 30.001C83.9498 29.8048 83.6226 29.5087 83.3805 29.113C83.1383 28.7176 83.017 28.2347 83.017 27.6639C83.017 26.8192 83.321 26.145 83.9293 25.6416C84.5375 25.1385 85.5519 24.887 86.9727 24.887C87.5055 24.887 88.033 24.9248 88.5551 25V24.8305C88.5551 24.0609 88.3909 23.5187 88.0626 23.2036C87.7343 22.889 87.2634 22.7316 86.6501 22.7316C86.2247 22.7316 85.7701 22.7935 85.2855 22.9172C84.8013 23.0411 84.3759 23.189 84.0101 23.3612L83.6951 21.9081C84.0503 21.7465 84.5186 21.5986 85.0999 21.464C85.6813 21.3297 86.2946 21.2622 86.9405 21.2622C87.6941 21.2622 88.3343 21.3765 88.8618 21.6052C89.3893 21.834 89.801 22.2271 90.097 22.7838C90.393 23.3411 90.541 24.0906 90.541 25.0323V28.4954C90.541 28.8562 90.5623 29.4027 90.6055 30.1344H88.7327Z"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M92.0709 28.0434V17.7505L94.0567 17.5891V27.6882C94.0567 28.0597 94.1199 28.3218 94.2463 28.4754C94.3727 28.6285 94.5732 28.7056 94.8479 28.7056C94.9716 28.7056 95.1466 28.676 95.3724 28.6168L95.6066 30.0456C95.418 30.121 95.1882 30.1816 94.9167 30.2272C94.6447 30.2728 94.3876 30.2957 94.1455 30.2957C92.762 30.2957 92.0709 29.5451 92.0709 28.0434Z"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M97.0356 28.0434V17.7505L99.0215 17.5891V27.6882C99.0215 28.0597 99.0847 28.3218 99.2111 28.4754C99.3378 28.6285 99.5383 28.7056 99.8127 28.7056C99.9365 28.7056 100.111 28.676 100.338 28.6168L100.572 30.0456C100.383 30.121 100.153 30.1816 99.8815 30.2272C99.6095 30.2728 99.3524 30.2957 99.1103 30.2957C97.7271 30.2957 97.0356 29.5451 97.0356 28.0434Z"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M106.795 24.7659C106.755 24.0716 106.581 23.551 106.269 23.2036C105.957 22.8568 105.539 22.683 105.019 22.683C104.512 22.683 104.091 22.8581 103.755 23.2078C103.419 23.5578 103.197 24.0771 103.096 24.7659H106.795ZM108.739 26.0333H103.04C103.131 27.8579 104 28.7701 105.648 28.7701C106.056 28.7701 106.475 28.7203 106.904 28.6208C107.331 28.521 107.741 28.388 108.133 28.221L108.571 29.5853C107.595 30.0592 106.501 30.2957 105.285 30.2957C104.357 30.2957 103.579 30.121 102.944 29.7711C102.307 29.4213 101.829 28.9181 101.509 28.2613C101.19 27.6051 101.03 26.8138 101.03 25.888C101.03 24.9248 101.199 24.0958 101.539 23.4016C101.877 22.7073 102.349 22.1773 102.955 21.8112C103.56 21.4453 104.259 21.2622 105.051 21.2622C105.875 21.2622 106.56 21.4547 107.112 21.8396C107.664 22.2241 108.072 22.737 108.339 23.3773C108.605 24.018 108.739 24.7255 108.739 25.5005V26.0333Z"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M110.248 23.6115C110.248 23.1326 110.224 22.4034 110.181 21.4237H112.048C112.08 21.6659 112.109 21.9552 112.141 22.2916C112.171 22.6279 112.189 22.901 112.2 23.1109C112.432 22.7288 112.659 22.4073 112.883 22.1463C113.107 21.8851 113.368 21.6727 113.667 21.5083C113.965 21.3443 114.304 21.2622 114.688 21.2622C114.995 21.2622 115.256 21.2946 115.477 21.3592L115.227 23.0868C115.035 23.0276 114.819 22.9979 114.581 22.9979C114.115 22.9979 113.704 23.1177 113.355 23.3573C113.005 23.5965 112.632 23.9908 112.232 24.5398V30.1344H110.248V23.6115Z"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M116.597 33.6984C116.307 33.6475 116.067 33.5896 115.88 33.5251L116.243 32.072C116.376 32.1094 116.547 32.1458 116.752 32.1808C116.955 32.216 117.149 32.2333 117.333 32.2333C118.216 32.2333 118.877 31.6653 119.317 30.5299L119.448 30.207L116.235 21.4237H118.373L119.989 26.332C120.251 27.1717 120.421 27.8149 120.496 28.2613C120.648 27.6317 120.824 27.0021 121.029 26.3724L122.669 21.4237H124.677L121.475 30.2475C121.173 31.0816 120.845 31.7541 120.496 32.2656C120.147 32.7768 119.733 33.1562 119.259 33.4039C118.781 33.6512 118.208 33.7752 117.533 33.7752C117.2 33.7752 116.888 33.7499 116.597 33.6984Z"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M41.5933 7.30106H45.1474L45.0474 8.15946H42.6349V9.82186H44.9098V10.6261H42.6349V12.4677H45.1765L45.089 13.3344H41.5933V7.30106Z"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M47.5973 10.2552L45.7223 7.30106H46.9055L48.1765 9.472L49.514 7.30106H50.6639L48.8055 10.2176L50.8181 13.3344H49.6181L48.1807 10.9595L46.7181 13.3344H45.5682L47.5973 10.2552Z"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M54.1973 9.99466C54.4194 9.7936 54.5306 9.50933 54.5306 9.14266C54.5306 8.7704 54.4172 8.50266 54.1911 8.33866C53.9647 8.17466 53.6319 8.0928 53.1932 8.0928H52.6266V10.2344C52.8876 10.276 53.0876 10.2968 53.2266 10.2968C53.6514 10.2968 53.9751 10.1963 54.1973 9.99466ZM51.5847 7.30106H53.2098C53.9735 7.30106 54.5583 7.4568 54.9639 7.76773C55.3695 8.07893 55.5722 8.5288 55.5722 9.1176C55.5722 9.5176 55.4813 9.8672 55.2994 10.1656C55.1173 10.4643 54.8639 10.6936 54.5388 10.8531C54.214 11.0131 53.8402 11.0928 53.418 11.0928C53.1876 11.0928 52.9236 11.0651 52.6266 11.0093V13.3344H51.5847V7.30106Z"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
<path d="M56.6015 7.30106H57.6431V12.4427H60.1389L60.0514 13.3344H56.6015V7.30106Z" fill="white" />
|
||||||
|
<path
|
||||||
|
d="M64.4223 12.2989C64.714 12.1032 64.9319 11.8339 65.0764 11.4907C65.221 11.1477 65.2932 10.7552 65.2932 10.3136C65.2932 9.88026 65.2292 9.49439 65.1015 9.15519C64.9735 8.81625 64.7695 8.54639 64.489 8.34479C64.2084 8.14346 63.8474 8.04266 63.4055 8.04266C62.9834 8.04266 62.625 8.14479 62.3306 8.34906C62.0362 8.55306 61.8154 8.82692 61.6682 9.16986C61.521 9.51306 61.4474 9.89145 61.4474 10.3053C61.4474 10.7413 61.5167 11.1317 61.6556 11.476C61.7943 11.8205 62.0068 12.0928 62.2932 12.2928C62.5791 12.4928 62.9332 12.5928 63.3556 12.5928C63.7751 12.5928 64.1306 12.4947 64.4223 12.2989ZM61.7223 13.0387C61.2807 12.7859 60.9431 12.4309 60.7098 11.9739C60.4764 11.5171 60.3599 10.9859 60.3599 10.3803C60.3599 9.74426 60.4839 9.18799 60.7327 8.71146C60.9813 8.23519 61.3396 7.86719 61.8076 7.60719C62.2756 7.34772 62.8277 7.21759 63.4639 7.21759C64.0722 7.21759 64.5959 7.34346 65.0348 7.59466C65.4735 7.84639 65.8084 8.19972 66.0388 8.65519C66.2695 9.11092 66.3847 9.63865 66.3847 10.2387C66.3847 10.8859 66.2591 11.4485 66.0076 11.9261C65.7562 12.4037 65.3981 12.772 64.9327 13.0301C64.4674 13.2885 63.921 13.4177 63.2932 13.4177C62.6876 13.4177 62.1639 13.2915 61.7223 13.0387Z"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M69.1807 10.1552C69.4333 10.1552 69.657 10.1061 69.8514 10.0072C70.0458 9.9088 70.1973 9.76986 70.3055 9.59066C70.4141 9.41146 70.4682 9.20373 70.4682 8.96773C70.4682 8.66506 70.3722 8.44346 70.1807 8.3032C69.989 8.16293 69.7098 8.0928 69.3431 8.0928H68.589V10.1552H69.1807ZM67.5474 7.30106H69.4349C70.1237 7.30106 70.6453 7.43866 70.9994 7.7136C71.3536 7.98853 71.5306 8.38186 71.5306 8.8928C71.5306 9.21226 71.4666 9.4936 71.3389 9.73653C71.2111 9.97973 71.0528 10.1776 70.864 10.3301C70.6749 10.4829 70.4807 10.5968 70.2807 10.672L72.1349 13.3344H70.9266L69.3557 10.9427H68.589V13.3344H67.5474V7.30106Z"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M72.9599 7.30106H76.5141L76.4141 8.15946H74.0015V9.82186H76.2765V10.6261H74.0015V12.4677H76.5431L76.4556 13.3344H72.9599V7.30106Z"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
<path d="M80.3807 7.30106H81.4223V13.3344H80.3807V7.30106Z" fill="white" />
|
||||||
|
<path d="M84.1765 8.172H82.3055L82.3973 7.30106H87.0933L86.9973 8.172H85.218V13.3344H84.1765V8.172Z" fill="white" />
|
||||||
|
<path
|
||||||
|
d="M94.3141 12.2989C94.6055 12.1032 94.8237 11.8339 94.9682 11.4907C95.1125 11.1477 95.1847 10.7552 95.1847 10.3136C95.1847 9.88026 95.1207 9.49439 94.9933 9.15519C94.8653 8.81625 94.661 8.54639 94.3807 8.34479C94.0999 8.14346 93.7389 8.04266 93.2973 8.04266C92.8749 8.04266 92.5167 8.14479 92.2223 8.34906C91.9277 8.55306 91.7069 8.82692 91.5599 9.16986C91.4125 9.51306 91.3389 9.89145 91.3389 10.3053C91.3389 10.7413 91.4082 11.1317 91.5474 11.476C91.6861 11.8205 91.8986 12.0928 92.1847 12.2928C92.4709 12.4928 92.825 12.5928 93.2474 12.5928C93.6666 12.5928 94.0223 12.4947 94.3141 12.2989ZM91.6141 13.0387C91.1722 12.7859 90.8349 12.4309 90.6015 11.9739C90.3682 11.5171 90.2514 10.9859 90.2514 10.3803C90.2514 9.74426 90.3757 9.18799 90.6245 8.71146C90.8727 8.23519 91.2311 7.86719 91.6994 7.60719C92.1674 7.34772 92.7194 7.21759 93.3557 7.21759C93.9639 7.21759 94.4874 7.34346 94.9266 7.59466C95.3653 7.84639 95.6999 8.19972 95.9306 8.65519C96.161 9.11092 96.2765 9.63865 96.2765 10.2387C96.2765 10.8859 96.1506 11.4485 95.8994 11.9261C95.6479 12.4037 95.2895 12.772 94.8245 13.0301C94.3591 13.2885 93.8125 13.4177 93.1847 13.4177C92.5791 13.4177 92.0557 13.2915 91.6141 13.0387Z"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M97.4389 7.30106H98.6349L101.619 11.976C101.592 11.5317 101.581 11.1219 101.581 10.7469V7.30106H102.547V13.3344H101.389L98.3599 8.58426C98.3903 9.12346 98.4055 9.60106 98.4055 10.0176V13.3344H97.4389V7.30106Z"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M26.9417 7.33329H15.8633C10.6446 7.33329 8.73584 9.24209 8.73584 14.4608V25.5393C8.73584 30.7579 10.6446 32.6666 15.8633 32.6666H26.9382C32.1569 32.6666 34.0692 30.7579 34.0692 25.5393V14.4608C34.0692 9.24209 32.1604 7.33329 26.9417 7.33329"
|
||||||
|
fill="#C91C2E"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M19.7881 22.0255H20.8041L20.2944 20.8404L19.7881 22.0255ZM19.5379 22.6229L19.2366 23.3125H18.5504L20.0097 20.0021H20.6025L22.0558 23.3125H21.3513L21.0537 22.6229H19.5379ZM30.5787 23.3102H31.2419V20.0021H30.5787V23.3102ZM27.9395 21.8887H29.1616V21.2859H27.9395V20.6078H29.7137V20.0042H27.2766V23.3121H29.7777V22.7088H27.9395V21.8887ZM25.3046 22.2796L24.5526 20.002H24.0043L23.2523 22.2796L22.5206 20.0036H21.8054L22.9598 23.314H23.516L24.2691 21.1395L25.0219 23.314H25.5832L26.7347 20.0036H26.0379L25.3046 22.2796ZM17.5382 21.8979C17.5382 22.4364 17.2707 22.7241 16.7851 22.7241C16.2969 22.7241 16.0281 22.4283 16.0281 21.875V20.004H15.356V21.8979C15.356 22.8296 15.8736 23.3638 16.776 23.3638C17.6872 23.3638 18.2099 22.8194 18.2099 21.8705V20.002H17.5382V21.8979ZM13.7526 20.0021H14.4243V23.3142H13.7526V21.9692H12.235V23.3142H11.563V20.0021H12.235V21.3383H13.7526V20.0021Z"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M21.4023 15.7851C19.0773 15.7851 17.1855 13.8933 17.1855 11.5683H17.7813C17.7813 13.5649 19.4055 15.1894 21.4023 15.1894C23.3991 15.1894 25.0234 13.5649 25.0234 11.5683H25.6191C25.6191 13.8933 23.7274 15.7851 21.4023 15.7851"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
};
|
||||||
117
src/components/base/buttons/button-utility.tsx
Normal file
117
src/components/base/buttons/button-utility.tsx
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
import type { AnchorHTMLAttributes, ButtonHTMLAttributes, DetailedHTMLProps, FC, ReactNode } from "react";
|
||||||
|
import { isValidElement } from "react";
|
||||||
|
import type { Placement } from "react-aria";
|
||||||
|
import type { ButtonProps as AriaButtonProps, LinkProps as AriaLinkProps } from "react-aria-components";
|
||||||
|
import { Button as AriaButton, Link as AriaLink } from "react-aria-components";
|
||||||
|
import { Tooltip } from "@/components/base/tooltip/tooltip";
|
||||||
|
import { cx } from "@/utils/cx";
|
||||||
|
import { isReactComponent } from "@/utils/is-react-component";
|
||||||
|
|
||||||
|
export const styles = {
|
||||||
|
secondary:
|
||||||
|
"bg-primary text-fg-quaternary shadow-xs-skeumorphic ring-1 ring-primary ring-inset hover:bg-primary_hover hover:text-fg-quaternary_hover disabled:shadow-xs disabled:ring-disabled_subtle",
|
||||||
|
tertiary: "text-fg-quaternary hover:bg-primary_hover hover:text-fg-quaternary_hover",
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Common props shared between button and anchor variants
|
||||||
|
*/
|
||||||
|
export interface CommonProps {
|
||||||
|
/** Disables the button and shows a disabled state */
|
||||||
|
isDisabled?: boolean;
|
||||||
|
/** The size variant of the button */
|
||||||
|
size?: "xs" | "sm";
|
||||||
|
/** The color variant of the button */
|
||||||
|
color?: "secondary" | "tertiary";
|
||||||
|
/** The icon to display in the button */
|
||||||
|
icon?: FC<{ className?: string }> | ReactNode;
|
||||||
|
/** The tooltip to display when hovering over the button */
|
||||||
|
tooltip?: string;
|
||||||
|
/** The placement of the tooltip */
|
||||||
|
tooltipPlacement?: Placement;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Props for the button variant (non-link)
|
||||||
|
*/
|
||||||
|
export interface ButtonProps extends CommonProps, DetailedHTMLProps<Omit<ButtonHTMLAttributes<HTMLButtonElement>, "color" | "slot">, HTMLButtonElement> {
|
||||||
|
/** Slot name for react-aria component */
|
||||||
|
slot?: AriaButtonProps["slot"];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Props for the link variant (anchor tag)
|
||||||
|
*/
|
||||||
|
interface LinkProps extends CommonProps, DetailedHTMLProps<Omit<AnchorHTMLAttributes<HTMLAnchorElement>, "color">, HTMLAnchorElement> {
|
||||||
|
/** Options for the configured client side router. */
|
||||||
|
routerOptions?: AriaLinkProps["routerOptions"];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Union type of button and link props */
|
||||||
|
export type Props = ButtonProps | LinkProps;
|
||||||
|
|
||||||
|
export const ButtonUtility = ({
|
||||||
|
tooltip,
|
||||||
|
className,
|
||||||
|
isDisabled,
|
||||||
|
icon: Icon,
|
||||||
|
size = "sm",
|
||||||
|
color = "secondary",
|
||||||
|
tooltipPlacement = "top",
|
||||||
|
...otherProps
|
||||||
|
}: Props) => {
|
||||||
|
const href = "href" in otherProps ? otherProps.href : undefined;
|
||||||
|
const Component = href ? AriaLink : AriaButton;
|
||||||
|
|
||||||
|
let props = {};
|
||||||
|
|
||||||
|
if (href) {
|
||||||
|
props = {
|
||||||
|
...otherProps,
|
||||||
|
|
||||||
|
href: isDisabled ? undefined : href,
|
||||||
|
|
||||||
|
// Since anchor elements do not support the `disabled` attribute and state,
|
||||||
|
// we need to specify `data-rac` and `data-disabled` in order to be able
|
||||||
|
// to use the `disabled:` selector in classes.
|
||||||
|
...(isDisabled ? { "data-rac": true, "data-disabled": true } : {}),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
props = {
|
||||||
|
...otherProps,
|
||||||
|
|
||||||
|
type: otherProps.type || "button",
|
||||||
|
isDisabled,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = (
|
||||||
|
<Component
|
||||||
|
aria-label={tooltip}
|
||||||
|
{...props}
|
||||||
|
className={cx(
|
||||||
|
"group relative inline-flex h-max cursor-pointer items-center justify-center rounded-md p-1.5 outline-focus-ring transition duration-100 ease-linear focus-visible:outline-2 focus-visible:outline-offset-2 disabled:cursor-not-allowed disabled:text-fg-disabled_subtle",
|
||||||
|
styles[color],
|
||||||
|
|
||||||
|
// Icon styles
|
||||||
|
"*:data-icon:pointer-events-none *:data-icon:shrink-0 *:data-icon:text-current *:data-icon:transition-inherit-all",
|
||||||
|
size === "xs" ? "*:data-icon:size-4" : "*:data-icon:size-5",
|
||||||
|
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{isReactComponent(Icon) && <Icon data-icon />}
|
||||||
|
{isValidElement(Icon) && Icon}
|
||||||
|
</Component>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (tooltip) {
|
||||||
|
return (
|
||||||
|
<Tooltip title={tooltip} placement={tooltipPlacement} isDisabled={isDisabled} offset={size === "xs" ? 4 : 6}>
|
||||||
|
{content}
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return content;
|
||||||
|
};
|
||||||
267
src/components/base/buttons/button.tsx
Normal file
267
src/components/base/buttons/button.tsx
Normal file
@@ -0,0 +1,267 @@
|
|||||||
|
import type { AnchorHTMLAttributes, ButtonHTMLAttributes, DetailedHTMLProps, FC, ReactNode } from "react";
|
||||||
|
import React, { isValidElement } from "react";
|
||||||
|
import type { ButtonProps as AriaButtonProps, LinkProps as AriaLinkProps } from "react-aria-components";
|
||||||
|
import { Button as AriaButton, Link as AriaLink } from "react-aria-components";
|
||||||
|
import { cx, sortCx } from "@/utils/cx";
|
||||||
|
import { isReactComponent } from "@/utils/is-react-component";
|
||||||
|
|
||||||
|
export const styles = sortCx({
|
||||||
|
common: {
|
||||||
|
root: [
|
||||||
|
"group relative inline-flex h-max cursor-pointer items-center justify-center whitespace-nowrap outline-brand transition duration-100 ease-linear before:absolute focus-visible:outline-2 focus-visible:outline-offset-2",
|
||||||
|
// When button is used within `InputGroup`
|
||||||
|
"in-data-input-wrapper:shadow-xs in-data-input-wrapper:focus:!z-50 in-data-input-wrapper:in-data-leading:-mr-px in-data-input-wrapper:in-data-leading:rounded-r-none in-data-input-wrapper:in-data-leading:before:rounded-r-none in-data-input-wrapper:in-data-trailing:-ml-px in-data-input-wrapper:in-data-trailing:rounded-l-none in-data-input-wrapper:in-data-trailing:before:rounded-l-none",
|
||||||
|
// Disabled styles
|
||||||
|
"disabled:cursor-not-allowed disabled:text-fg-disabled",
|
||||||
|
// Icon styles
|
||||||
|
"disabled:*:data-icon:text-fg-disabled_subtle",
|
||||||
|
// Same as `icon` but for SSR icons that cannot be passed to the client as functions.
|
||||||
|
"*:data-icon:pointer-events-none *:data-icon:size-5 *:data-icon:shrink-0 *:data-icon:transition-inherit-all",
|
||||||
|
].join(" "),
|
||||||
|
icon: "pointer-events-none size-5 shrink-0 transition-inherit-all",
|
||||||
|
},
|
||||||
|
sizes: {
|
||||||
|
sm: {
|
||||||
|
root: [
|
||||||
|
"gap-1 rounded-lg px-3 py-2 text-sm font-semibold before:rounded-[7px] data-icon-only:p-2",
|
||||||
|
"in-data-input-wrapper:px-3.5 in-data-input-wrapper:py-2.5 in-data-input-wrapper:data-icon-only:p-2.5",
|
||||||
|
].join(" "),
|
||||||
|
linkRoot: "gap-1",
|
||||||
|
},
|
||||||
|
md: {
|
||||||
|
root: [
|
||||||
|
"gap-1 rounded-lg px-3.5 py-2.5 text-sm font-semibold before:rounded-[7px] data-icon-only:p-2.5",
|
||||||
|
"in-data-input-wrapper:gap-1.5 in-data-input-wrapper:px-4 in-data-input-wrapper:text-md in-data-input-wrapper:data-icon-only:p-3",
|
||||||
|
].join(" "),
|
||||||
|
linkRoot: "gap-1",
|
||||||
|
},
|
||||||
|
lg: {
|
||||||
|
root: "gap-1.5 rounded-lg px-4 py-2.5 text-md font-semibold before:rounded-[7px] data-icon-only:p-3",
|
||||||
|
linkRoot: "gap-1.5",
|
||||||
|
},
|
||||||
|
xl: {
|
||||||
|
root: "gap-1.5 rounded-lg px-4.5 py-3 text-md font-semibold before:rounded-[7px] data-icon-only:p-3.5",
|
||||||
|
linkRoot: "gap-1.5",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
colors: {
|
||||||
|
primary: {
|
||||||
|
root: [
|
||||||
|
"bg-brand-solid text-white shadow-xs-skeumorphic ring-1 ring-transparent ring-inset hover:bg-brand-solid_hover data-loading:bg-brand-solid_hover",
|
||||||
|
// Inner border gradient
|
||||||
|
"before:absolute before:inset-px before:border before:border-white/12 before:mask-b-from-0%",
|
||||||
|
// Disabled styles
|
||||||
|
"disabled:bg-disabled disabled:shadow-xs disabled:ring-disabled_subtle",
|
||||||
|
// Icon styles
|
||||||
|
"*:data-icon:text-button-primary-icon hover:*:data-icon:text-button-primary-icon_hover",
|
||||||
|
].join(" "),
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
root: [
|
||||||
|
"bg-primary text-secondary shadow-xs-skeumorphic ring-1 ring-primary ring-inset hover:bg-primary_hover hover:text-secondary_hover data-loading:bg-primary_hover",
|
||||||
|
// Disabled styles
|
||||||
|
"disabled:shadow-xs disabled:ring-disabled_subtle",
|
||||||
|
// Icon styles
|
||||||
|
"*:data-icon:text-fg-quaternary hover:*:data-icon:text-fg-quaternary_hover",
|
||||||
|
].join(" "),
|
||||||
|
},
|
||||||
|
tertiary: {
|
||||||
|
root: [
|
||||||
|
"text-tertiary hover:bg-primary_hover hover:text-tertiary_hover data-loading:bg-primary_hover",
|
||||||
|
// Icon styles
|
||||||
|
"*:data-icon:text-fg-quaternary hover:*:data-icon:text-fg-quaternary_hover",
|
||||||
|
].join(" "),
|
||||||
|
},
|
||||||
|
"link-gray": {
|
||||||
|
root: [
|
||||||
|
"justify-normal rounded p-0! text-tertiary hover:text-tertiary_hover",
|
||||||
|
// Inner text underline
|
||||||
|
"*:data-text:underline *:data-text:decoration-transparent *:data-text:underline-offset-2 hover:*:data-text:decoration-current",
|
||||||
|
// Icon styles
|
||||||
|
"*:data-icon:text-fg-quaternary hover:*:data-icon:text-fg-quaternary_hover",
|
||||||
|
].join(" "),
|
||||||
|
},
|
||||||
|
"link-color": {
|
||||||
|
root: [
|
||||||
|
"justify-normal rounded p-0! text-brand-secondary hover:text-brand-secondary_hover",
|
||||||
|
// Inner text underline
|
||||||
|
"*:data-text:underline *:data-text:decoration-transparent *:data-text:underline-offset-2 hover:*:data-text:decoration-current",
|
||||||
|
// Icon styles
|
||||||
|
"*:data-icon:text-fg-brand-secondary_alt hover:*:data-icon:text-fg-brand-secondary_hover",
|
||||||
|
].join(" "),
|
||||||
|
},
|
||||||
|
"primary-destructive": {
|
||||||
|
root: [
|
||||||
|
"bg-error-solid text-white shadow-xs-skeumorphic ring-1 ring-transparent outline-error ring-inset hover:bg-error-solid_hover data-loading:bg-error-solid_hover",
|
||||||
|
// Inner border gradient
|
||||||
|
"before:absolute before:inset-px before:border before:border-white/12 before:mask-b-from-0%",
|
||||||
|
// Disabled styles
|
||||||
|
"disabled:bg-disabled disabled:shadow-xs disabled:ring-disabled_subtle",
|
||||||
|
// Icon styles
|
||||||
|
"*:data-icon:text-button-destructive-primary-icon hover:*:data-icon:text-button-destructive-primary-icon_hover",
|
||||||
|
].join(" "),
|
||||||
|
},
|
||||||
|
"secondary-destructive": {
|
||||||
|
root: [
|
||||||
|
"bg-primary text-error-primary shadow-xs-skeumorphic ring-1 ring-error_subtle outline-error ring-inset hover:bg-error-primary hover:text-error-primary_hover data-loading:bg-error-primary",
|
||||||
|
// Disabled styles
|
||||||
|
"disabled:bg-primary disabled:shadow-xs disabled:ring-disabled_subtle",
|
||||||
|
// Icon styles
|
||||||
|
"*:data-icon:text-fg-error-secondary hover:*:data-icon:text-fg-error-primary",
|
||||||
|
].join(" "),
|
||||||
|
},
|
||||||
|
"tertiary-destructive": {
|
||||||
|
root: [
|
||||||
|
"text-error-primary outline-error hover:bg-error-primary hover:text-error-primary_hover data-loading:bg-error-primary",
|
||||||
|
// Icon styles
|
||||||
|
"*:data-icon:text-fg-error-secondary hover:*:data-icon:text-fg-error-primary",
|
||||||
|
].join(" "),
|
||||||
|
},
|
||||||
|
"link-destructive": {
|
||||||
|
root: [
|
||||||
|
"justify-normal rounded p-0! text-error-primary outline-error hover:text-error-primary_hover",
|
||||||
|
// Inner text underline
|
||||||
|
"*:data-text:underline *:data-text:decoration-transparent *:data-text:underline-offset-2 hover:*:data-text:decoration-current",
|
||||||
|
// Icon styles
|
||||||
|
"*:data-icon:text-fg-error-secondary hover:*:data-icon:text-fg-error-primary",
|
||||||
|
].join(" "),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Common props shared between button and anchor variants
|
||||||
|
*/
|
||||||
|
export interface CommonProps {
|
||||||
|
/** Disables the button and shows a disabled state */
|
||||||
|
isDisabled?: boolean;
|
||||||
|
/** Shows a loading spinner and disables the button */
|
||||||
|
isLoading?: boolean;
|
||||||
|
/** The size variant of the button */
|
||||||
|
size?: keyof typeof styles.sizes;
|
||||||
|
/** The color variant of the button */
|
||||||
|
color?: keyof typeof styles.colors;
|
||||||
|
/** Icon component or element to show before the text */
|
||||||
|
iconLeading?: FC<{ className?: string }> | ReactNode;
|
||||||
|
/** Icon component or element to show after the text */
|
||||||
|
iconTrailing?: FC<{ className?: string }> | ReactNode;
|
||||||
|
/** Removes horizontal padding from the text content */
|
||||||
|
noTextPadding?: boolean;
|
||||||
|
/** When true, keeps the text visible during loading state */
|
||||||
|
showTextWhileLoading?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Props for the button variant (non-link)
|
||||||
|
*/
|
||||||
|
export interface ButtonProps extends CommonProps, DetailedHTMLProps<Omit<ButtonHTMLAttributes<HTMLButtonElement>, "color" | "slot">, HTMLButtonElement> {
|
||||||
|
/** Slot name for react-aria component */
|
||||||
|
slot?: AriaButtonProps["slot"];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Props for the link variant (anchor tag)
|
||||||
|
*/
|
||||||
|
interface LinkProps extends CommonProps, DetailedHTMLProps<Omit<AnchorHTMLAttributes<HTMLAnchorElement>, "color">, HTMLAnchorElement> {
|
||||||
|
/** Options for the configured client side router. */
|
||||||
|
routerOptions?: AriaLinkProps["routerOptions"];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Union type of button and link props */
|
||||||
|
export type Props = ButtonProps | LinkProps;
|
||||||
|
|
||||||
|
export const Button = ({
|
||||||
|
size = "sm",
|
||||||
|
color = "primary",
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
noTextPadding,
|
||||||
|
iconLeading: IconLeading,
|
||||||
|
iconTrailing: IconTrailing,
|
||||||
|
isDisabled: disabled,
|
||||||
|
isLoading: loading,
|
||||||
|
showTextWhileLoading,
|
||||||
|
...otherProps
|
||||||
|
}: Props) => {
|
||||||
|
const href = "href" in otherProps ? otherProps.href : undefined;
|
||||||
|
const Component = href ? AriaLink : AriaButton;
|
||||||
|
|
||||||
|
const isIcon = (IconLeading || IconTrailing) && !children;
|
||||||
|
const isLinkType = ["link-gray", "link-color", "link-destructive"].includes(color);
|
||||||
|
|
||||||
|
noTextPadding = isLinkType || noTextPadding;
|
||||||
|
|
||||||
|
let props = {};
|
||||||
|
|
||||||
|
if (href) {
|
||||||
|
props = {
|
||||||
|
...otherProps,
|
||||||
|
|
||||||
|
href: disabled ? undefined : href,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
props = {
|
||||||
|
...otherProps,
|
||||||
|
|
||||||
|
type: otherProps.type || "button",
|
||||||
|
isPending: loading,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Component
|
||||||
|
data-loading={loading ? true : undefined}
|
||||||
|
data-icon-only={isIcon ? true : undefined}
|
||||||
|
{...props}
|
||||||
|
isDisabled={disabled}
|
||||||
|
className={cx(
|
||||||
|
styles.common.root,
|
||||||
|
styles.sizes[size].root,
|
||||||
|
styles.colors[color].root,
|
||||||
|
isLinkType && styles.sizes[size].linkRoot,
|
||||||
|
(loading || (href && (disabled || loading))) && "pointer-events-none",
|
||||||
|
// If in `loading` state, hide everything except the loading icon (and text if `showTextWhileLoading` is true).
|
||||||
|
loading && (showTextWhileLoading ? "[&>*:not([data-icon=loading]):not([data-text])]:hidden" : "[&>*:not([data-icon=loading])]:invisible"),
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{/* Leading icon */}
|
||||||
|
{isValidElement(IconLeading) && IconLeading}
|
||||||
|
{isReactComponent(IconLeading) && <IconLeading data-icon="leading" className={styles.common.icon} />}
|
||||||
|
|
||||||
|
{loading && (
|
||||||
|
<svg
|
||||||
|
fill="none"
|
||||||
|
data-icon="loading"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
className={cx(styles.common.icon, !showTextWhileLoading && "absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2")}
|
||||||
|
>
|
||||||
|
{/* Background circle */}
|
||||||
|
<circle className="stroke-current opacity-30" cx="10" cy="10" r="8" fill="none" strokeWidth="2" />
|
||||||
|
{/* Spinning circle */}
|
||||||
|
<circle
|
||||||
|
className="origin-center animate-spin stroke-current"
|
||||||
|
cx="10"
|
||||||
|
cy="10"
|
||||||
|
r="8"
|
||||||
|
fill="none"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeDasharray="12.5 50"
|
||||||
|
strokeLinecap="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{children && (
|
||||||
|
<span data-text className={cx("transition-inherit-all", !noTextPadding && "px-0.5")}>
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Trailing icon */}
|
||||||
|
{isValidElement(IconTrailing) && IconTrailing}
|
||||||
|
{isReactComponent(IconTrailing) && <IconTrailing data-icon="trailing" className={styles.common.icon} />}
|
||||||
|
</Component>
|
||||||
|
);
|
||||||
|
};
|
||||||
40
src/components/base/buttons/close-button.tsx
Normal file
40
src/components/base/buttons/close-button.tsx
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { X as CloseIcon } from "@untitledui/icons";
|
||||||
|
import { Button as AriaButton, type ButtonProps as AriaButtonProps } from "react-aria-components";
|
||||||
|
import { cx } from "@/utils/cx";
|
||||||
|
|
||||||
|
const sizes = {
|
||||||
|
xs: { root: "size-7", icon: "size-4" },
|
||||||
|
sm: { root: "size-9", icon: "size-5" },
|
||||||
|
md: { root: "size-10", icon: "size-5" },
|
||||||
|
lg: { root: "size-11", icon: "size-6" },
|
||||||
|
};
|
||||||
|
|
||||||
|
const themes = {
|
||||||
|
light: "text-fg-quaternary hover:bg-primary_hover hover:text-fg-quaternary_hover focus-visible:outline-2 focus-visible:outline-offset-2 outline-focus-ring",
|
||||||
|
dark: "text-fg-white/70 hover:text-fg-white hover:bg-white/20 focus-visible:outline-2 focus-visible:outline-offset-2 outline-focus-ring",
|
||||||
|
};
|
||||||
|
|
||||||
|
interface CloseButtonProps extends AriaButtonProps {
|
||||||
|
theme?: "light" | "dark";
|
||||||
|
size?: "xs" | "sm" | "md" | "lg";
|
||||||
|
label?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CloseButton = ({ label, className, size = "sm", theme = "light", ...otherProps }: CloseButtonProps) => {
|
||||||
|
return (
|
||||||
|
<AriaButton
|
||||||
|
{...otherProps}
|
||||||
|
aria-label={label || "Close"}
|
||||||
|
className={(state) =>
|
||||||
|
cx(
|
||||||
|
"flex cursor-pointer items-center justify-center rounded-lg p-2 transition duration-100 ease-linear focus:outline-hidden",
|
||||||
|
sizes[size].root,
|
||||||
|
themes[theme],
|
||||||
|
typeof className === "function" ? className(state) : className,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<CloseIcon aria-hidden="true" className={cx("shrink-0 transition-inherit-all", sizes[size].icon)} />
|
||||||
|
</AriaButton>
|
||||||
|
);
|
||||||
|
};
|
||||||
149
src/components/base/buttons/social-button.tsx
Normal file
149
src/components/base/buttons/social-button.tsx
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
import type { AnchorHTMLAttributes, ButtonHTMLAttributes, DetailedHTMLProps } from "react";
|
||||||
|
import type { ButtonProps as AriaButtonProps, LinkProps as AriaLinkProps } from "react-aria-components";
|
||||||
|
import { Button as AriaButton, Link as AriaLink } from "react-aria-components";
|
||||||
|
import { cx, sortCx } from "@/utils/cx";
|
||||||
|
import { AppleLogo, DribbleLogo, FacebookLogo, FigmaLogo, FigmaLogoOutlined, GoogleLogo, TwitterLogo } from "./social-logos";
|
||||||
|
|
||||||
|
export const styles = sortCx({
|
||||||
|
common: {
|
||||||
|
root: "group relative inline-flex h-max cursor-pointer items-center justify-center font-semibold whitespace-nowrap outline-focus-ring transition duration-100 ease-linear before:absolute focus-visible:outline-2 focus-visible:outline-offset-2 disabled:cursor-not-allowed disabled:stroke-fg-disabled disabled:text-fg-disabled disabled:*:text-fg-disabled",
|
||||||
|
icon: "pointer-events-none shrink-0 transition-inherit-all",
|
||||||
|
},
|
||||||
|
|
||||||
|
sizes: {
|
||||||
|
sm: {
|
||||||
|
root: "gap-2 rounded-lg px-3 py-2 text-sm before:rounded-[7px] data-icon-only:p-2",
|
||||||
|
},
|
||||||
|
md: {
|
||||||
|
root: "gap-2.5 rounded-lg px-3.5 py-2.5 text-sm before:rounded-[7px] data-icon-only:p-2.5",
|
||||||
|
},
|
||||||
|
lg: {
|
||||||
|
root: "gap-3 rounded-lg px-4 py-2.5 text-md before:rounded-[7px] data-icon-only:p-2.5",
|
||||||
|
},
|
||||||
|
xl: {
|
||||||
|
root: "gap-3.5 rounded-lg px-4.5 py-3 text-md before:rounded-[7px] data-icon-only:p-3.5",
|
||||||
|
},
|
||||||
|
"2xl": {
|
||||||
|
root: "gap-4 rounded-[10px] px-5.5 py-4 text-lg before:rounded-[9px] data-icon-only:p-4",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
colors: {
|
||||||
|
gray: {
|
||||||
|
root: "bg-primary text-secondary shadow-xs-skeumorphic ring-1 ring-primary ring-inset hover:bg-primary_hover hover:text-secondary_hover",
|
||||||
|
icon: "text-fg-quaternary group-hover:text-fg-quaternary_hover",
|
||||||
|
},
|
||||||
|
black: {
|
||||||
|
root: "bg-black text-white shadow-xs-skeumorphic ring-1 ring-transparent ring-inset before:absolute before:inset-px before:border before:border-white/12 before:mask-b-from-0%",
|
||||||
|
icon: "",
|
||||||
|
},
|
||||||
|
|
||||||
|
facebook: {
|
||||||
|
root: "bg-[#1877F2] text-white shadow-xs-skeumorphic ring-1 ring-transparent ring-inset before:absolute before:inset-px before:border before:border-white/12 before:mask-b-from-0% hover:bg-[#0C63D4]",
|
||||||
|
icon: "",
|
||||||
|
},
|
||||||
|
|
||||||
|
dribble: {
|
||||||
|
root: "bg-[#EA4C89] text-white shadow-xs-skeumorphic ring-1 ring-transparent ring-inset before:absolute before:inset-px before:border before:border-white/12 before:mask-b-from-0% hover:bg-[#E62872]",
|
||||||
|
icon: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
interface CommonProps {
|
||||||
|
social: "google" | "facebook" | "apple" | "twitter" | "figma" | "dribble";
|
||||||
|
disabled?: boolean;
|
||||||
|
theme?: "brand" | "color" | "gray";
|
||||||
|
size?: keyof typeof styles.sizes;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ButtonProps extends CommonProps, DetailedHTMLProps<Omit<ButtonHTMLAttributes<HTMLButtonElement>, "color" | "slot">, HTMLButtonElement> {
|
||||||
|
slot?: AriaButtonProps["slot"];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LinkProps extends CommonProps, DetailedHTMLProps<Omit<AnchorHTMLAttributes<HTMLAnchorElement>, "color">, HTMLAnchorElement> {
|
||||||
|
/** Options for the configured client side router. */
|
||||||
|
routerOptions?: AriaLinkProps["routerOptions"];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SocialButtonProps = ButtonProps | LinkProps;
|
||||||
|
|
||||||
|
export const SocialButton = ({ size = "lg", theme = "brand", social, className, children, disabled, ...otherProps }: SocialButtonProps) => {
|
||||||
|
const href = "href" in otherProps ? otherProps.href : undefined;
|
||||||
|
const Component = href ? AriaLink : AriaButton;
|
||||||
|
|
||||||
|
const isIconOnly = !children;
|
||||||
|
|
||||||
|
const socialToColor = {
|
||||||
|
google: "gray",
|
||||||
|
facebook: "facebook",
|
||||||
|
apple: "black",
|
||||||
|
twitter: "black",
|
||||||
|
figma: "black",
|
||||||
|
dribble: "dribble",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const colorStyles = theme === "brand" ? styles.colors[socialToColor[social]] : styles.colors.gray;
|
||||||
|
|
||||||
|
const logos = {
|
||||||
|
google: GoogleLogo,
|
||||||
|
facebook: FacebookLogo,
|
||||||
|
apple: AppleLogo,
|
||||||
|
twitter: TwitterLogo,
|
||||||
|
figma: theme === "gray" ? FigmaLogoOutlined : FigmaLogo,
|
||||||
|
dribble: DribbleLogo,
|
||||||
|
};
|
||||||
|
|
||||||
|
const Logo = logos[social];
|
||||||
|
|
||||||
|
let props = {};
|
||||||
|
|
||||||
|
if (href) {
|
||||||
|
props = {
|
||||||
|
...otherProps,
|
||||||
|
|
||||||
|
href: disabled ? undefined : href,
|
||||||
|
|
||||||
|
// Since anchor elements do not support the `disabled` attribute and state,
|
||||||
|
// we need to specify `data-rac` and `data-disabled` in order to be able
|
||||||
|
// to use the `disabled:` selector in classes.
|
||||||
|
...(disabled ? { "data-rac": true, "data-disabled": true } : {}),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
props = {
|
||||||
|
...otherProps,
|
||||||
|
|
||||||
|
type: otherProps.type || "button",
|
||||||
|
isDisabled: disabled,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Component
|
||||||
|
isDisabled={disabled}
|
||||||
|
{...props}
|
||||||
|
data-icon-only={isIconOnly ? true : undefined}
|
||||||
|
className={cx(styles.common.root, styles.sizes[size].root, colorStyles.root, className)}
|
||||||
|
>
|
||||||
|
<Logo
|
||||||
|
className={cx(
|
||||||
|
styles.common.icon,
|
||||||
|
theme === "gray"
|
||||||
|
? colorStyles.icon
|
||||||
|
: theme === "brand" && (social === "facebook" || social === "apple" || social === "twitter")
|
||||||
|
? "text-white"
|
||||||
|
: theme === "color" && (social === "apple" || social === "twitter")
|
||||||
|
? "text-alpha-black"
|
||||||
|
: "",
|
||||||
|
)}
|
||||||
|
colorful={
|
||||||
|
(theme === "brand" && (social === "google" || social === "figma")) ||
|
||||||
|
(theme === "color" && (social === "google" || social === "facebook" || social === "figma" || social === "dribble")) ||
|
||||||
|
undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{children}
|
||||||
|
</Component>
|
||||||
|
);
|
||||||
|
};
|
||||||
113
src/components/base/buttons/social-logos.tsx
Normal file
113
src/components/base/buttons/social-logos.tsx
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
import type { SVGProps } from "react";
|
||||||
|
|
||||||
|
export const GoogleLogo = ({ colorful, ...props }: SVGProps<SVGSVGElement> & { colorful?: boolean }) => {
|
||||||
|
return (
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" {...props}>
|
||||||
|
<path
|
||||||
|
d="M23.766 12.2764C23.766 11.4607 23.6999 10.6406 23.5588 9.83807H12.24V14.4591H18.7217C18.4528 15.9494 17.5885 17.2678 16.323 18.1056V21.1039H20.19C22.4608 19.0139 23.766 15.9274 23.766 12.2764Z"
|
||||||
|
fill={colorful ? "#4285F4" : "currentColor"}
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M12.24 24.0008C15.4764 24.0008 18.2058 22.9382 20.1944 21.1039L16.3274 18.1055C15.2516 18.8375 13.8626 19.252 12.2444 19.252C9.11376 19.252 6.45934 17.1399 5.50693 14.3003H1.51648V17.3912C3.55359 21.4434 7.70278 24.0008 12.24 24.0008Z"
|
||||||
|
fill={colorful ? "#34A853" : "currentColor"}
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M5.50253 14.3003C4.99987 12.8099 4.99987 11.1961 5.50253 9.70575V6.61481H1.51649C-0.18551 10.0056 -0.18551 14.0004 1.51649 17.3912L5.50253 14.3003Z"
|
||||||
|
fill={colorful ? "#FBBC04" : "currentColor"}
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M12.24 4.74966C13.9508 4.7232 15.6043 5.36697 16.8433 6.54867L20.2694 3.12262C18.1 1.0855 15.2207 -0.034466 12.24 0.000808666C7.70277 0.000808666 3.55359 2.55822 1.51648 6.61481L5.50252 9.70575C6.45052 6.86173 9.10935 4.74966 12.24 4.74966Z"
|
||||||
|
fill={colorful ? "#EA4335" : "currentColor"}
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FigmaLogo = ({ colorful, ...props }: SVGProps<SVGSVGElement> & { colorful?: boolean }) => {
|
||||||
|
return (
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" {...props}>
|
||||||
|
<path
|
||||||
|
d="M8.00006 24.0001C10.2081 24.0001 12.0001 22.208 12.0001 20V16H8.00006C5.79205 16 4 17.792 4 20C4 22.208 5.79205 24.0001 8.00006 24.0001Z"
|
||||||
|
fill={colorful ? "#24CB71" : "currentColor"}
|
||||||
|
/>
|
||||||
|
<path d="M4 12C4 9.79203 5.79205 8 8.00006 8H12.0001V16H8.00006C5.79205 16.0001 4 14.208 4 12Z" fill={colorful ? "#874FFF" : "currentColor"} />
|
||||||
|
<path
|
||||||
|
d="M4 4.00003C4 1.79203 5.79205 0 8.00006 0H12.0001V7.99997H8.00006C5.79205 7.99997 4 6.20803 4 4.00003Z"
|
||||||
|
fill={colorful ? "#FF3737" : "currentColor"}
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M12 0H16.0001C18.2081 0 20.0001 1.79203 20.0001 4.00003C20.0001 6.20803 18.2081 7.99997 16.0001 7.99997H12V0Z"
|
||||||
|
fill={colorful ? "#FF7237" : "currentColor"}
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M20.0001 12C20.0001 14.208 18.2081 16.0001 16.0001 16.0001C13.792 16.0001 12 14.208 12 12C12 9.79203 13.792 8 16.0001 8C18.2081 8 20.0001 9.79203 20.0001 12Z"
|
||||||
|
fill={colorful ? "#00B6FF" : "currentColor"}
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FigmaLogoOutlined = (props: SVGProps<SVGSVGElement>) => {
|
||||||
|
return (
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" {...props}>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M8.25 2C7.51349 2 6.81155 2.28629 6.29747 2.78895C5.78414 3.29087 5.5 3.96677 5.5 4.66667C5.5 5.36657 5.78414 6.04247 6.29747 6.54438C6.81155 7.04705 7.51349 7.33333 8.25 7.33333H11V2H8.25ZM13 2V7.33333H15.75C16.1142 7.33333 16.4744 7.26316 16.8097 7.12736C17.145 6.99157 17.4482 6.79311 17.7025 6.54438C17.9569 6.29571 18.1574 6.00171 18.2938 5.67977C18.4301 5.35788 18.5 5.0137 18.5 4.66667C18.5 4.31964 18.4301 3.97545 18.2938 3.65356C18.1574 3.33162 17.9569 3.03763 17.7025 2.78895C17.4482 2.54022 17.145 2.34177 16.8097 2.20598C16.4744 2.07017 16.1142 2 15.75 2H13ZM18.6884 8.33334C18.8324 8.22191 18.9702 8.10211 19.1008 7.9744C19.5429 7.54211 19.8948 7.02769 20.1353 6.45991C20.3759 5.89208 20.5 5.28266 20.5 4.66667C20.5 4.05067 20.3759 3.44126 20.1353 2.87342C19.8948 2.30564 19.5429 1.79122 19.1008 1.35894C18.6587 0.926696 18.1351 0.584984 17.5605 0.352241C16.9858 0.119512 16.3707 0 15.75 0H8.25C6.99738 0 5.79167 0.486331 4.89923 1.35894C4.00603 2.23228 3.5 3.42165 3.5 4.66667C3.5 5.91169 4.00603 7.10105 4.89923 7.9744C5.03021 8.10247 5.16794 8.22222 5.31158 8.33333C5.16794 8.44445 5.03021 8.5642 4.89923 8.69227C4.00603 9.56562 3.5 10.755 3.5 12C3.5 13.245 4.00603 14.4344 4.89923 15.3077C5.03022 15.4358 5.16795 15.5556 5.31159 15.6667C5.16795 15.7778 5.03022 15.8975 4.89923 16.0256C4.00603 16.899 3.5 18.0883 3.5 19.3333C3.5 20.5784 4.00603 21.7677 4.89923 22.6411C5.79167 23.5137 6.99738 24 8.25 24C9.5026 24 10.7083 23.5137 11.6008 22.6411C12.494 21.7677 13 20.5784 13 19.3333V15.8051C13.2922 16.0089 13.6073 16.1799 13.9395 16.3144C14.5142 16.5472 15.1293 16.6667 15.75 16.6667C16.3707 16.6667 16.9858 16.5472 17.5605 16.3144C18.1351 16.0817 18.6587 15.74 19.1008 15.3077C19.5429 14.8754 19.8948 14.361 20.1353 13.7932C20.3759 13.2254 20.5 12.616 20.5 12C20.5 11.384 20.3759 10.7746 20.1353 10.2068C19.8948 9.63898 19.5429 9.12456 19.1008 8.69227C18.9702 8.56456 18.8324 8.44476 18.6884 8.33334ZM11 14.6667V9.33333H8.25C7.51349 9.33333 6.81155 9.61962 6.29747 10.1223C5.78414 10.6242 5.5 11.3001 5.5 12C5.5 12.6999 5.78414 13.3758 6.29747 13.8777C6.81155 14.3804 7.51349 14.6667 8.25 14.6667H11ZM11 16.6667H8.25C7.51349 16.6667 6.81155 16.953 6.29747 17.4556C5.78414 17.9575 5.5 18.6334 5.5 19.3333C5.5 20.0332 5.78414 20.7091 6.29747 21.2111C6.81155 21.7137 7.51349 22 8.25 22C8.98651 22 9.6884 21.7137 10.2025 21.2111C10.7159 20.7091 11 20.0332 11 19.3333V16.6667ZM15.75 9.33333C15.3858 9.33333 15.0256 9.4035 14.6903 9.53931C14.355 9.6751 14.0518 9.87356 13.7975 10.1223C13.5431 10.371 13.3426 10.665 13.2062 10.9869C13.0699 11.3088 13 11.653 13 12C13 12.347 13.0699 12.6912 13.2062 13.0131C13.3426 13.335 13.5431 13.629 13.7975 13.8777C14.0518 14.1264 14.355 14.3249 14.6903 14.4607C15.0256 14.5965 15.3858 14.6667 15.75 14.6667C16.1142 14.6667 16.4744 14.5965 16.8097 14.4607C17.145 14.3249 17.4482 14.1264 17.7025 13.8777C17.9569 13.629 18.1574 13.335 18.2938 13.0131C18.4301 12.6912 18.5 12.347 18.5 12C18.5 11.653 18.4301 11.3088 18.2938 10.9869C18.1574 10.665 17.9569 10.371 17.7025 10.1223C17.4482 9.87356 17.145 9.6751 16.8097 9.53931C16.4744 9.4035 16.1142 9.33333 15.75 9.33333Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DribbleLogo = ({ colorful, ...props }: SVGProps<SVGSVGElement> & { colorful?: boolean }) => {
|
||||||
|
return (
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" {...props}>
|
||||||
|
<path
|
||||||
|
d="M12 23.625C18.4203 23.625 23.625 18.4203 23.625 12C23.625 5.57969 18.4203 0.375 12 0.375C5.57969 0.375 0.375 5.57969 0.375 12C0.375 18.4203 5.57969 23.625 12 23.625Z"
|
||||||
|
fill={colorful ? "#EA4C89" : "none"}
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M12 0C5.37527 0 0 5.37527 0 12C0 18.6248 5.37527 24 12 24C18.6117 24 24 18.6248 24 12C24 5.37527 18.6117 0 12 0ZM19.9262 5.53145C21.3579 7.27549 22.217 9.50107 22.243 11.9089C21.9046 11.8439 18.5206 11.154 15.1106 11.5835C15.0325 11.4143 14.9675 11.2321 14.8894 11.0499C14.6811 10.5554 14.4469 10.0477 14.2126 9.56618C17.9869 8.0304 19.705 5.81779 19.9262 5.53145ZM12 1.77007C14.603 1.77007 16.9848 2.74621 18.7939 4.34707C18.6117 4.60738 17.0629 6.67679 13.4186 8.04338C11.7397 4.95878 9.87855 2.43384 9.5922 2.04338C10.3601 1.86117 11.1671 1.77007 12 1.77007ZM7.63995 2.73319C7.91325 3.09761 9.73538 5.63558 11.4404 8.65508C6.65076 9.9306 2.42083 9.90458 1.96529 9.90458C2.62907 6.72885 4.77657 4.08676 7.63995 2.73319ZM1.74404 12.0131C1.74404 11.9089 1.74404 11.8048 1.74404 11.7007C2.18655 11.7136 7.15835 11.7787 12.2733 10.243C12.5727 10.8156 12.846 11.4013 13.1063 11.9869C12.9761 12.026 12.8329 12.0651 12.7028 12.1041C7.41865 13.8091 4.60738 18.4685 4.3731 18.859C2.7462 17.0499 1.74404 14.6421 1.74404 12.0131ZM12 22.256C9.6312 22.256 7.44469 21.449 5.71367 20.0954C5.89588 19.718 7.97827 15.7094 13.757 13.692C13.783 13.679 13.7961 13.679 13.8221 13.666C15.2668 17.4013 15.8525 20.5379 16.0087 21.436C14.7722 21.9696 13.4186 22.256 12 22.256ZM17.7136 20.4989C17.6096 19.8742 17.0629 16.8807 15.7223 13.1974C18.9371 12.6898 21.7484 13.5228 22.0998 13.6399C21.6573 16.4902 20.0173 18.9501 17.7136 20.4989Z"
|
||||||
|
fill={colorful ? "#C32361" : "currentColor"}
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FacebookLogo = ({ colorful, ...props }: SVGProps<SVGSVGElement> & { colorful?: boolean }) => {
|
||||||
|
return (
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" {...props}>
|
||||||
|
<path
|
||||||
|
d="M24 12C24 5.37258 18.6274 0 12 0C5.37258 0 0 5.37258 0 12C0 17.9895 4.3882 22.954 10.125 23.8542V15.4688H7.07812V12H10.125V9.35625C10.125 6.34875 11.9166 4.6875 14.6576 4.6875C15.9701 4.6875 17.3438 4.92188 17.3438 4.92188V7.875H15.8306C14.34 7.875 13.875 8.80008 13.875 9.75V12H17.2031L16.6711 15.4688H13.875V23.8542C19.6118 22.954 24 17.9895 24 12Z"
|
||||||
|
fill={colorful ? "#1877F2" : "currentColor"}
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AppleLogo = (props: SVGProps<SVGSVGElement>) => {
|
||||||
|
return (
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" {...props}>
|
||||||
|
<path
|
||||||
|
d="M20.8426 17.1449C20.5099 17.9135 20.1161 18.6211 19.6598 19.2715C19.0379 20.1583 18.5286 20.7721 18.1362 21.113C17.5278 21.6724 16.876 21.959 16.178 21.9753C15.6769 21.9753 15.0726 21.8327 14.3691 21.5434C13.6634 21.2555 13.0148 21.113 12.4218 21.113C11.7998 21.113 11.1328 21.2555 10.4193 21.5434C9.70475 21.8327 9.1291 21.9834 8.68898 21.9984C8.01963 22.0269 7.35246 21.7322 6.6865 21.113C6.26145 20.7422 5.7298 20.1067 5.09291 19.2063C4.40957 18.2449 3.84778 17.13 3.40766 15.8589C2.9363 14.486 2.70001 13.1565 2.70001 11.8694C2.70001 10.3951 3.01859 9.12345 3.65671 8.05784C4.15821 7.20191 4.82539 6.52672 5.66041 6.03105C6.49543 5.53539 7.39768 5.2828 8.36931 5.26664C8.90096 5.26664 9.59815 5.43109 10.4645 5.75429C11.3285 6.07858 11.8832 6.24303 12.1264 6.24303C12.3083 6.24303 12.9245 6.05074 13.9692 5.66738C14.9571 5.31186 15.7909 5.16466 16.474 5.22264C18.3249 5.37202 19.7155 6.10167 20.6403 7.41619C18.9849 8.4192 18.1661 9.82403 18.1824 11.6262C18.1973 13.03 18.7065 14.1981 19.7074 15.1256C20.1609 15.5561 20.6675 15.8888 21.231 16.1251C21.1088 16.4795 20.9798 16.819 20.8426 17.1449ZM16.5976 0.440369C16.5976 1.54062 16.1956 2.56792 15.3944 3.51878C14.4275 4.64917 13.258 5.30236 11.9898 5.19929C11.9737 5.06729 11.9643 4.92837 11.9643 4.78239C11.9643 3.72615 12.4241 2.59576 13.2407 1.67152C13.6483 1.20356 14.1668 0.814453 14.7955 0.504058C15.4229 0.198295 16.0164 0.0292007 16.5745 0.000244141C16.5908 0.147331 16.5976 0.294426 16.5976 0.440355V0.440369Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TwitterLogo = (props: SVGProps<SVGSVGElement>) => {
|
||||||
|
return (
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" {...props}>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M15.9455 23L10.396 15.0901L3.44886 23H0.509766L9.09209 13.2311L0.509766 1H8.05571L13.286 8.45502L19.8393 1H22.7784L14.5943 10.3165L23.4914 23H15.9455ZM19.2185 20.77H17.2398L4.71811 3.23H6.6971L11.7121 10.2532L12.5793 11.4719L19.2185 20.77Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
||||||
118
src/components/base/checkbox/checkbox.tsx
Normal file
118
src/components/base/checkbox/checkbox.tsx
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
import type { ReactNode, Ref } from "react";
|
||||||
|
import { Checkbox as AriaCheckbox, type CheckboxProps as AriaCheckboxProps } from "react-aria-components";
|
||||||
|
import { cx } from "@/utils/cx";
|
||||||
|
|
||||||
|
export interface CheckboxBaseProps {
|
||||||
|
size?: "sm" | "md";
|
||||||
|
className?: string;
|
||||||
|
isFocusVisible?: boolean;
|
||||||
|
isSelected?: boolean;
|
||||||
|
isDisabled?: boolean;
|
||||||
|
isIndeterminate?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CheckboxBase = ({ className, isSelected, isDisabled, isIndeterminate, size = "sm", isFocusVisible = false }: CheckboxBaseProps) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cx(
|
||||||
|
"relative flex size-4 shrink-0 cursor-pointer appearance-none items-center justify-center rounded bg-primary ring-1 ring-primary ring-inset",
|
||||||
|
size === "md" && "size-5 rounded-md",
|
||||||
|
(isSelected || isIndeterminate) && "bg-brand-solid ring-bg-brand-solid",
|
||||||
|
isDisabled && "cursor-not-allowed bg-disabled_subtle ring-disabled",
|
||||||
|
isFocusVisible && "outline-2 outline-offset-2 outline-focus-ring",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
viewBox="0 0 14 14"
|
||||||
|
fill="none"
|
||||||
|
className={cx(
|
||||||
|
"pointer-events-none absolute h-3 w-2.5 text-fg-white opacity-0 transition-inherit-all",
|
||||||
|
size === "md" && "size-3.5",
|
||||||
|
isIndeterminate && "opacity-100",
|
||||||
|
isDisabled && "text-fg-disabled_subtle",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<path d="M2.91675 7H11.0834" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
viewBox="0 0 14 14"
|
||||||
|
fill="none"
|
||||||
|
className={cx(
|
||||||
|
"pointer-events-none absolute size-3 text-fg-white opacity-0 transition-inherit-all",
|
||||||
|
size === "md" && "size-3.5",
|
||||||
|
isSelected && !isIndeterminate && "opacity-100",
|
||||||
|
isDisabled && "text-fg-disabled_subtle",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<path d="M11.6666 3.5L5.24992 9.91667L2.33325 7" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
CheckboxBase.displayName = "CheckboxBase";
|
||||||
|
|
||||||
|
interface CheckboxProps extends AriaCheckboxProps {
|
||||||
|
ref?: Ref<HTMLLabelElement>;
|
||||||
|
size?: "sm" | "md";
|
||||||
|
label?: ReactNode;
|
||||||
|
hint?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Checkbox = ({ label, hint, size = "sm", className, ...ariaCheckboxProps }: CheckboxProps) => {
|
||||||
|
const sizes = {
|
||||||
|
sm: {
|
||||||
|
root: "gap-2",
|
||||||
|
textWrapper: "",
|
||||||
|
label: "text-sm font-medium",
|
||||||
|
hint: "text-sm",
|
||||||
|
},
|
||||||
|
md: {
|
||||||
|
root: "gap-3",
|
||||||
|
textWrapper: "gap-0.5",
|
||||||
|
label: "text-md font-medium",
|
||||||
|
hint: "text-md",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AriaCheckbox
|
||||||
|
{...ariaCheckboxProps}
|
||||||
|
className={(state) =>
|
||||||
|
cx(
|
||||||
|
"flex items-start",
|
||||||
|
state.isDisabled && "cursor-not-allowed",
|
||||||
|
sizes[size].root,
|
||||||
|
typeof className === "function" ? className(state) : className,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{({ isSelected, isIndeterminate, isDisabled, isFocusVisible }) => (
|
||||||
|
<>
|
||||||
|
<CheckboxBase
|
||||||
|
size={size}
|
||||||
|
isSelected={isSelected}
|
||||||
|
isIndeterminate={isIndeterminate}
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
isFocusVisible={isFocusVisible}
|
||||||
|
className={label || hint ? "mt-0.5" : ""}
|
||||||
|
/>
|
||||||
|
{(label || hint) && (
|
||||||
|
<div className={cx("inline-flex flex-col", sizes[size].textWrapper)}>
|
||||||
|
{label && <p className={cx("text-secondary select-none", sizes[size].label)}>{label}</p>}
|
||||||
|
{hint && (
|
||||||
|
<span className={cx("text-tertiary", sizes[size].hint)} onClick={(event) => event.stopPropagation()}>
|
||||||
|
{hint}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</AriaCheckbox>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
Checkbox.displayName = "Checkbox";
|
||||||
161
src/components/base/dropdown/dropdown.tsx
Normal file
161
src/components/base/dropdown/dropdown.tsx
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
import type { FC, RefAttributes } from "react";
|
||||||
|
import { DotsVertical } from "@untitledui/icons";
|
||||||
|
import type {
|
||||||
|
ButtonProps as AriaButtonProps,
|
||||||
|
MenuItemProps as AriaMenuItemProps,
|
||||||
|
MenuProps as AriaMenuProps,
|
||||||
|
PopoverProps as AriaPopoverProps,
|
||||||
|
SeparatorProps as AriaSeparatorProps,
|
||||||
|
} from "react-aria-components";
|
||||||
|
import {
|
||||||
|
Button as AriaButton,
|
||||||
|
Header as AriaHeader,
|
||||||
|
Menu as AriaMenu,
|
||||||
|
MenuItem as AriaMenuItem,
|
||||||
|
MenuSection as AriaMenuSection,
|
||||||
|
MenuTrigger as AriaMenuTrigger,
|
||||||
|
Popover as AriaPopover,
|
||||||
|
Separator as AriaSeparator,
|
||||||
|
} from "react-aria-components";
|
||||||
|
import { cx } from "@/utils/cx";
|
||||||
|
|
||||||
|
interface DropdownItemProps extends AriaMenuItemProps {
|
||||||
|
/** The label of the item to be displayed. */
|
||||||
|
label?: string;
|
||||||
|
/** An addon to be displayed on the right side of the item. */
|
||||||
|
addon?: string;
|
||||||
|
/** If true, the item will not have any styles. */
|
||||||
|
unstyled?: boolean;
|
||||||
|
/** An icon to be displayed on the left side of the item. */
|
||||||
|
icon?: FC<{ className?: string }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DropdownItem = ({ label, children, addon, icon: Icon, unstyled, ...props }: DropdownItemProps) => {
|
||||||
|
if (unstyled) {
|
||||||
|
return <AriaMenuItem id={label} textValue={label} {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AriaMenuItem
|
||||||
|
{...props}
|
||||||
|
className={(state) =>
|
||||||
|
cx(
|
||||||
|
"group block cursor-pointer px-1.5 py-px outline-hidden",
|
||||||
|
state.isDisabled && "cursor-not-allowed",
|
||||||
|
typeof props.className === "function" ? props.className(state) : props.className,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{(state) => (
|
||||||
|
<div
|
||||||
|
className={cx(
|
||||||
|
"relative flex items-center rounded-md px-2.5 py-2 outline-focus-ring transition duration-100 ease-linear",
|
||||||
|
!state.isDisabled && "group-hover:bg-primary_hover",
|
||||||
|
state.isFocused && "bg-primary_hover",
|
||||||
|
state.isFocusVisible && "outline-2 -outline-offset-2",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{Icon && (
|
||||||
|
<Icon
|
||||||
|
aria-hidden="true"
|
||||||
|
className={cx("mr-2 size-4 shrink-0 stroke-[2.25px]", state.isDisabled ? "text-fg-disabled" : "text-fg-quaternary")}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<span
|
||||||
|
className={cx(
|
||||||
|
"grow truncate text-sm font-semibold",
|
||||||
|
state.isDisabled ? "text-disabled" : "text-secondary",
|
||||||
|
state.isFocused && "text-secondary_hover",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{label || (typeof children === "function" ? children(state) : children)}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{addon && (
|
||||||
|
<span
|
||||||
|
className={cx(
|
||||||
|
"ml-3 shrink-0 rounded px-1 py-px text-xs font-medium ring-1 ring-secondary ring-inset",
|
||||||
|
state.isDisabled ? "text-disabled" : "text-quaternary",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{addon}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</AriaMenuItem>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface DropdownMenuProps<T extends object> extends AriaMenuProps<T> {}
|
||||||
|
|
||||||
|
const DropdownMenu = <T extends object>(props: DropdownMenuProps<T>) => {
|
||||||
|
return (
|
||||||
|
<AriaMenu
|
||||||
|
disallowEmptySelection
|
||||||
|
selectionMode="single"
|
||||||
|
{...props}
|
||||||
|
className={(state) =>
|
||||||
|
cx("h-min overflow-y-auto py-1 outline-hidden select-none", typeof props.className === "function" ? props.className(state) : props.className)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface DropdownPopoverProps extends AriaPopoverProps {}
|
||||||
|
|
||||||
|
const DropdownPopover = (props: DropdownPopoverProps) => {
|
||||||
|
return (
|
||||||
|
<AriaPopover
|
||||||
|
placement="bottom right"
|
||||||
|
{...props}
|
||||||
|
className={(state) =>
|
||||||
|
cx(
|
||||||
|
"w-62 origin-(--trigger-anchor-point) overflow-auto rounded-lg bg-primary shadow-lg ring-1 ring-secondary_alt will-change-transform",
|
||||||
|
state.isEntering &&
|
||||||
|
"duration-150 ease-out animate-in fade-in placement-right:slide-in-from-left-0.5 placement-top:slide-in-from-bottom-0.5 placement-bottom:slide-in-from-top-0.5",
|
||||||
|
state.isExiting &&
|
||||||
|
"duration-100 ease-in animate-out fade-out placement-right:slide-out-to-left-0.5 placement-top:slide-out-to-bottom-0.5 placement-bottom:slide-out-to-top-0.5",
|
||||||
|
typeof props.className === "function" ? props.className(state) : props.className,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</AriaPopover>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const DropdownSeparator = (props: AriaSeparatorProps) => {
|
||||||
|
return <AriaSeparator {...props} className={cx("my-1 h-px w-full bg-border-secondary", props.className)} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DropdownDotsButton = (props: AriaButtonProps & RefAttributes<HTMLButtonElement>) => {
|
||||||
|
return (
|
||||||
|
<AriaButton
|
||||||
|
{...props}
|
||||||
|
aria-label="Open menu"
|
||||||
|
className={(state) =>
|
||||||
|
cx(
|
||||||
|
"cursor-pointer rounded-md text-fg-quaternary outline-focus-ring transition duration-100 ease-linear",
|
||||||
|
(state.isPressed || state.isHovered) && "text-fg-quaternary_hover",
|
||||||
|
(state.isPressed || state.isFocusVisible) && "outline-2 outline-offset-2",
|
||||||
|
typeof props.className === "function" ? props.className(state) : props.className,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<DotsVertical className="size-5 transition-inherit-all" />
|
||||||
|
</AriaButton>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Dropdown = {
|
||||||
|
Root: AriaMenuTrigger,
|
||||||
|
Popover: DropdownPopover,
|
||||||
|
Menu: DropdownMenu,
|
||||||
|
Section: AriaMenuSection,
|
||||||
|
SectionHeader: AriaHeader,
|
||||||
|
Item: DropdownItem,
|
||||||
|
Separator: DropdownSeparator,
|
||||||
|
DotsButton: DropdownDotsButton,
|
||||||
|
};
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import type { DetailedReactHTMLElement, HTMLAttributes, ReactNode } from "react";
|
||||||
|
import React, { cloneElement, useRef } from "react";
|
||||||
|
import { filterDOMProps } from "@react-aria/utils";
|
||||||
|
|
||||||
|
interface FileTriggerProps {
|
||||||
|
/**
|
||||||
|
* Specifies what mime type of files are allowed.
|
||||||
|
*/
|
||||||
|
acceptedFileTypes?: Array<string>;
|
||||||
|
/**
|
||||||
|
* Whether multiple files can be selected.
|
||||||
|
*/
|
||||||
|
allowsMultiple?: boolean;
|
||||||
|
/**
|
||||||
|
* Specifies the use of a media capture mechanism to capture the media on the spot.
|
||||||
|
*/
|
||||||
|
defaultCamera?: "user" | "environment";
|
||||||
|
/**
|
||||||
|
* Handler when a user selects a file.
|
||||||
|
*/
|
||||||
|
onSelect?: (files: FileList | null) => void;
|
||||||
|
/**
|
||||||
|
* The children of the component.
|
||||||
|
*/
|
||||||
|
children: ReactNode;
|
||||||
|
/**
|
||||||
|
* Enables the selection of directories instead of individual files.
|
||||||
|
*/
|
||||||
|
acceptDirectory?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A FileTrigger allows a user to access the file system with any pressable React Aria or React Spectrum component, or custom components built with usePress.
|
||||||
|
*/
|
||||||
|
export const FileTrigger = (props: FileTriggerProps) => {
|
||||||
|
const { children, onSelect, acceptedFileTypes, allowsMultiple, defaultCamera, acceptDirectory, ...rest } = props;
|
||||||
|
|
||||||
|
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||||
|
const domProps = filterDOMProps(rest);
|
||||||
|
|
||||||
|
// Make sure that only one child is passed to the component.
|
||||||
|
const clonableElement = React.Children.only(children);
|
||||||
|
|
||||||
|
// Clone the child element and add an `onClick` handler to open the file dialog.
|
||||||
|
const mainElement = cloneElement(clonableElement as DetailedReactHTMLElement<HTMLAttributes<HTMLElement>, HTMLElement>, {
|
||||||
|
onClick: () => {
|
||||||
|
if (inputRef.current?.value) {
|
||||||
|
inputRef.current.value = "";
|
||||||
|
}
|
||||||
|
inputRef.current?.click();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{mainElement}
|
||||||
|
<input
|
||||||
|
{...domProps}
|
||||||
|
type="file"
|
||||||
|
ref={inputRef}
|
||||||
|
style={{ display: "none" }}
|
||||||
|
accept={acceptedFileTypes?.toString()}
|
||||||
|
onChange={(e) => onSelect?.(e.target.files)}
|
||||||
|
capture={defaultCamera}
|
||||||
|
multiple={allowsMultiple}
|
||||||
|
// @ts-expect-error
|
||||||
|
webkitdirectory={acceptDirectory ? "" : undefined}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
8
src/components/base/form/form.tsx
Normal file
8
src/components/base/form/form.tsx
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import type { ComponentPropsWithRef } from "react";
|
||||||
|
import { Form as AriaForm } from "react-aria-components";
|
||||||
|
|
||||||
|
export const Form = (props: ComponentPropsWithRef<typeof AriaForm>) => {
|
||||||
|
return <AriaForm {...props} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
Form.displayName = "Form";
|
||||||
31
src/components/base/input/hint-text.tsx
Normal file
31
src/components/base/input/hint-text.tsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import type { ReactNode, Ref } from "react";
|
||||||
|
import type { TextProps as AriaTextProps } from "react-aria-components";
|
||||||
|
import { Text as AriaText } from "react-aria-components";
|
||||||
|
import { cx } from "@/utils/cx";
|
||||||
|
|
||||||
|
interface HintTextProps extends AriaTextProps {
|
||||||
|
/** Indicates that the hint text is an error message. */
|
||||||
|
isInvalid?: boolean;
|
||||||
|
ref?: Ref<HTMLElement>;
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const HintText = ({ isInvalid, className, ...props }: HintTextProps) => {
|
||||||
|
return (
|
||||||
|
<AriaText
|
||||||
|
{...props}
|
||||||
|
slot={isInvalid ? "errorMessage" : "description"}
|
||||||
|
className={cx(
|
||||||
|
"text-sm text-tertiary",
|
||||||
|
|
||||||
|
// Invalid state
|
||||||
|
isInvalid && "text-error-primary",
|
||||||
|
"group-invalid:text-error-primary",
|
||||||
|
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
HintText.displayName = "HintText";
|
||||||
131
src/components/base/input/input-group.tsx
Normal file
131
src/components/base/input/input-group.tsx
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
import { type HTMLAttributes, type ReactNode } from "react";
|
||||||
|
import { HintText } from "@/components/base/input/hint-text";
|
||||||
|
import type { InputBaseProps } from "@/components/base/input/input";
|
||||||
|
import { TextField } from "@/components/base/input/input";
|
||||||
|
import { Label } from "@/components/base/input/label";
|
||||||
|
import { cx, sortCx } from "@/utils/cx";
|
||||||
|
|
||||||
|
interface InputPrefixProps extends HTMLAttributes<HTMLDivElement> {
|
||||||
|
/** The position of the prefix. */
|
||||||
|
position?: "leading" | "trailing";
|
||||||
|
/** The size of the prefix. */
|
||||||
|
size?: "sm" | "md";
|
||||||
|
/** Indicates that the prefix is disabled. */
|
||||||
|
isDisabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const InputPrefix = ({ isDisabled, children, ...props }: InputPrefixProps) => (
|
||||||
|
<span
|
||||||
|
{...props}
|
||||||
|
className={cx(
|
||||||
|
"flex text-md text-tertiary shadow-xs ring-1 ring-border-primary ring-inset",
|
||||||
|
// Styles when the prefix is within an `InputGroup`
|
||||||
|
"in-data-input-wrapper:in-data-leading:-mr-px in-data-input-wrapper:in-data-leading:rounded-l-lg",
|
||||||
|
"in-data-input-wrapper:in-data-trailing:-ml-px in-data-input-wrapper:in-data-trailing:rounded-r-lg",
|
||||||
|
// Size styles based on size when within an `InputGroup`
|
||||||
|
"in-data-input-wrapper:in-data-[input-size=md]:py-2.5 in-data-input-wrapper:in-data-[input-size=md]:pr-3 in-data-input-wrapper:in-data-[input-size=md]:pl-3.5 in-data-input-wrapper:in-data-[input-size=sm]:px-3 in-data-input-wrapper:in-data-[input-size=sm]:py-2",
|
||||||
|
// Disabled styles
|
||||||
|
isDisabled && "border-disabled bg-disabled_subtle text-tertiary",
|
||||||
|
"in-data-input-wrapper:group-disabled:bg-disabled_subtle in-data-input-wrapper:group-disabled:text-disabled in-data-input-wrapper:group-disabled:ring-border-disabled",
|
||||||
|
|
||||||
|
props.className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
|
||||||
|
// `${string}ClassName` is used to omit any className prop that ends with a `ClassName` suffix
|
||||||
|
interface InputGroupProps extends Omit<InputBaseProps, "type" | "icon" | "placeholder" | "tooltip" | "shortcut" | `${string}ClassName`> {
|
||||||
|
/** A prefix text that is displayed in the same box as the input.*/
|
||||||
|
prefix?: string;
|
||||||
|
/** A leading addon that is displayed with visual separation from the input. */
|
||||||
|
leadingAddon?: ReactNode;
|
||||||
|
/** A trailing addon that is displayed with visual separation from the input. */
|
||||||
|
trailingAddon?: ReactNode;
|
||||||
|
/** The class name to apply to the input group. */
|
||||||
|
className?: string;
|
||||||
|
/** The children of the input group (i.e `<InputBase />`) */
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const InputGroup = ({ size = "sm", prefix, leadingAddon, trailingAddon, label, hint, children, ...props }: InputGroupProps) => {
|
||||||
|
const hasLeading = !!leadingAddon;
|
||||||
|
const hasTrailing = !!trailingAddon;
|
||||||
|
|
||||||
|
const paddings = sortCx({
|
||||||
|
sm: {
|
||||||
|
input: cx(
|
||||||
|
// Apply padding styles when select element is passed as a child
|
||||||
|
hasLeading && "group-has-[&>select]:px-2.5 group-has-[&>select]:pl-2.5",
|
||||||
|
hasTrailing && (prefix ? "group-has-[&>select]:pr-6 group-has-[&>select]:pl-0" : "group-has-[&>select]:pr-6 group-has-[&>select]:pl-3"),
|
||||||
|
),
|
||||||
|
leadingText: "pl-3",
|
||||||
|
},
|
||||||
|
md: {
|
||||||
|
input: cx(
|
||||||
|
// Apply padding styles when select element is passed as a child
|
||||||
|
hasLeading && "group-has-[&>select]:px-3 group-has-[&>select]:pl-3",
|
||||||
|
hasTrailing && (prefix ? "group-has-[&>select]:pr-6 group-has-[&>select]:pl-0" : "group-has-[&>select]:pr-6 group-has-[&>select]:pl-3"),
|
||||||
|
),
|
||||||
|
leadingText: "pl-3.5",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TextField
|
||||||
|
size={size}
|
||||||
|
aria-label={label || undefined}
|
||||||
|
inputClassName={cx(paddings[size].input)}
|
||||||
|
tooltipClassName={cx(hasTrailing && !hasLeading && "group-has-[&>select]:right-0")}
|
||||||
|
wrapperClassName={cx(
|
||||||
|
"z-10",
|
||||||
|
// Apply styles based on the presence of leading or trailing elements
|
||||||
|
hasLeading && "rounded-l-none",
|
||||||
|
hasTrailing && "rounded-r-none",
|
||||||
|
// When select element is passed as a child
|
||||||
|
"group-has-[&>select]:bg-transparent group-has-[&>select]:shadow-none group-has-[&>select]:ring-0 group-has-[&>select]:focus-within:ring-0",
|
||||||
|
// In `Input` component, there is "group-disabled" class so here we need to use "group-disabled:group-has-[&>select]" to avoid conflict
|
||||||
|
"group-disabled:group-has-[&>select]:bg-transparent",
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{({ isDisabled, isInvalid, isRequired }) => (
|
||||||
|
<>
|
||||||
|
{label && <Label isRequired={isRequired}>{label}</Label>}
|
||||||
|
|
||||||
|
<div
|
||||||
|
data-input-size={size}
|
||||||
|
className={cx(
|
||||||
|
"group relative flex h-max w-full flex-row justify-center rounded-lg bg-primary transition-all duration-100 ease-linear",
|
||||||
|
|
||||||
|
// Only apply focus ring when child is select and input is focused
|
||||||
|
"has-[&>select]:shadow-xs has-[&>select]:ring-1 has-[&>select]:ring-border-primary has-[&>select]:ring-inset has-[&>select]:has-[input:focus]:ring-2 has-[&>select]:has-[input:focus]:ring-border-brand",
|
||||||
|
|
||||||
|
isDisabled && "cursor-not-allowed has-[&>select]:bg-disabled_subtle has-[&>select]:ring-border-disabled",
|
||||||
|
isInvalid && "has-[&>select]:ring-border-error_subtle has-[&>select]:has-[input:focus]:ring-border-error",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{leadingAddon && <section data-leading={hasLeading || undefined}>{leadingAddon}</section>}
|
||||||
|
|
||||||
|
{prefix && (
|
||||||
|
<span className={cx("my-auto grow pr-2", paddings[size].leadingText)}>
|
||||||
|
<p className={cx("text-md text-tertiary", isDisabled && "text-disabled")}>{prefix}</p>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{children}
|
||||||
|
|
||||||
|
{trailingAddon && <section data-trailing={hasTrailing || undefined}>{trailingAddon}</section>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{hint && <HintText isInvalid={isInvalid}>{hint}</HintText>}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</TextField>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
InputGroup.Prefix = InputPrefix;
|
||||||
|
|
||||||
|
InputGroup.displayName = "InputGroup";
|
||||||
121
src/components/base/input/input-payment.tsx
Normal file
121
src/components/base/input/input-payment.tsx
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
import { useControlledState } from "@react-stately/utils";
|
||||||
|
import { HintText } from "@/components/base/input/hint-text";
|
||||||
|
import type { InputBaseProps } from "@/components/base/input/input";
|
||||||
|
import { InputBase, TextField } from "@/components/base/input/input";
|
||||||
|
import { Label } from "@/components/base/input/label";
|
||||||
|
import { AmexIcon, DiscoverIcon, MastercardIcon, UnionPayIcon, VisaIcon } from "@/components/foundations/payment-icons";
|
||||||
|
|
||||||
|
const cardTypes = [
|
||||||
|
{
|
||||||
|
name: "Visa",
|
||||||
|
pattern: /^4[0-9]{3,}$/, // Visa card numbers start with 4 and are 13 or 16 digits long
|
||||||
|
card: "visa",
|
||||||
|
icon: VisaIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "MasterCard",
|
||||||
|
pattern: /^5[1-5][0-9]{2,}$/, // MasterCard numbers start with 51-55 and are 16 digits long
|
||||||
|
card: "mastercard",
|
||||||
|
icon: MastercardIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "American Express",
|
||||||
|
pattern: /^3[47][0-9]{2,}$/, // American Express numbers start with 34 or 37 and are 15 digits long
|
||||||
|
card: "amex",
|
||||||
|
icon: AmexIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Discover",
|
||||||
|
pattern: /^6(?:011|5[0-9]{2}|4[4-9][0-9])[0-9]{12}$/, // Discover card numbers start with 6011 or 65 and are 16 digits long
|
||||||
|
card: "discover",
|
||||||
|
icon: DiscoverIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "UnionPay",
|
||||||
|
pattern: /^(62|88)[0-9]{14,17}$/, // UnionPay card numbers start with 62 or 88 and are between 15-19 digits long
|
||||||
|
card: "unionpay",
|
||||||
|
icon: UnionPayIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Unknown",
|
||||||
|
pattern: /.*/, // Fallback pattern for unknown cards
|
||||||
|
card: "unknown",
|
||||||
|
icon: MastercardIcon,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detect the card type based on the card number.
|
||||||
|
* @param number The card number to detect the type for.
|
||||||
|
* @returns The matching card type object.
|
||||||
|
*/
|
||||||
|
const detectCardType = (number: string) => {
|
||||||
|
// Remove all spaces
|
||||||
|
const sanitizedNumber = number.replace(/\D/g, "");
|
||||||
|
|
||||||
|
// Find the matching card type
|
||||||
|
const card = cardTypes.find((cardType) => cardType.pattern.test(sanitizedNumber));
|
||||||
|
|
||||||
|
return card || cardTypes[cardTypes.length - 1];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format the card number in groups of 4 digits (i.e. 1234 5678 9012 3456).
|
||||||
|
*/
|
||||||
|
export const formatCardNumber = (number: string) => {
|
||||||
|
// Remove non-numeric characters
|
||||||
|
const cleaned = number.replace(/\D/g, "");
|
||||||
|
|
||||||
|
// Format the card number in groups of 4 digits
|
||||||
|
const match = cleaned.match(/\d{1,4}/g);
|
||||||
|
|
||||||
|
if (match) {
|
||||||
|
return match.join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
return cleaned;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface PaymentInputProps extends Omit<InputBaseProps, "icon"> {}
|
||||||
|
|
||||||
|
export const PaymentInput = ({ onChange, value, defaultValue, className, maxLength = 19, label, hint, ...props }: PaymentInputProps) => {
|
||||||
|
const [cardNumber, setCardNumber] = useControlledState(value, defaultValue || "", (value) => {
|
||||||
|
// Remove all non-numeric characters
|
||||||
|
value = value.replace(/\D/g, "");
|
||||||
|
|
||||||
|
onChange?.(value || "");
|
||||||
|
});
|
||||||
|
|
||||||
|
const card = detectCardType(cardNumber);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TextField
|
||||||
|
aria-label={!label ? props?.placeholder : undefined}
|
||||||
|
{...props}
|
||||||
|
className={className}
|
||||||
|
inputMode="numeric"
|
||||||
|
maxLength={maxLength}
|
||||||
|
value={formatCardNumber(cardNumber)}
|
||||||
|
onChange={setCardNumber}
|
||||||
|
>
|
||||||
|
{({ isDisabled, isInvalid, isRequired }) => (
|
||||||
|
<>
|
||||||
|
{label && <Label isRequired={isRequired}>{label}</Label>}
|
||||||
|
|
||||||
|
<InputBase
|
||||||
|
{...props}
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
isInvalid={isInvalid}
|
||||||
|
icon={card.icon}
|
||||||
|
inputClassName="pl-13"
|
||||||
|
iconClassName="left-2.5 h-6 w-8.5"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{hint && <HintText isInvalid={isInvalid}>{hint}</HintText>}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</TextField>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
PaymentInput.displayName = "PaymentInput";
|
||||||
269
src/components/base/input/input.tsx
Normal file
269
src/components/base/input/input.tsx
Normal file
@@ -0,0 +1,269 @@
|
|||||||
|
import { type ComponentType, type HTMLAttributes, type ReactNode, type Ref, createContext, useContext } from "react";
|
||||||
|
import { HelpCircle, InfoCircle } from "@untitledui/icons";
|
||||||
|
import type { InputProps as AriaInputProps, TextFieldProps as AriaTextFieldProps } from "react-aria-components";
|
||||||
|
import { Group as AriaGroup, Input as AriaInput, TextField as AriaTextField } from "react-aria-components";
|
||||||
|
import { HintText } from "@/components/base/input/hint-text";
|
||||||
|
import { Label } from "@/components/base/input/label";
|
||||||
|
import { Tooltip, TooltipTrigger } from "@/components/base/tooltip/tooltip";
|
||||||
|
import { cx, sortCx } from "@/utils/cx";
|
||||||
|
|
||||||
|
export interface InputBaseProps extends TextFieldProps {
|
||||||
|
/** Tooltip message on hover. */
|
||||||
|
tooltip?: string;
|
||||||
|
/**
|
||||||
|
* Input size.
|
||||||
|
* @default "sm"
|
||||||
|
*/
|
||||||
|
size?: "sm" | "md";
|
||||||
|
/** Placeholder text. */
|
||||||
|
placeholder?: string;
|
||||||
|
/** Class name for the icon. */
|
||||||
|
iconClassName?: string;
|
||||||
|
/** Class name for the input. */
|
||||||
|
inputClassName?: string;
|
||||||
|
/** Class name for the input wrapper. */
|
||||||
|
wrapperClassName?: string;
|
||||||
|
/** Class name for the tooltip. */
|
||||||
|
tooltipClassName?: string;
|
||||||
|
/** Keyboard shortcut to display. */
|
||||||
|
shortcut?: string | boolean;
|
||||||
|
ref?: Ref<HTMLInputElement>;
|
||||||
|
groupRef?: Ref<HTMLDivElement>;
|
||||||
|
/** Icon component to display on the left side of the input. */
|
||||||
|
icon?: ComponentType<HTMLAttributes<HTMLOrSVGElement>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const InputBase = ({
|
||||||
|
ref,
|
||||||
|
tooltip,
|
||||||
|
shortcut,
|
||||||
|
groupRef,
|
||||||
|
size = "sm",
|
||||||
|
isInvalid,
|
||||||
|
isDisabled,
|
||||||
|
icon: Icon,
|
||||||
|
placeholder,
|
||||||
|
wrapperClassName,
|
||||||
|
tooltipClassName,
|
||||||
|
inputClassName,
|
||||||
|
iconClassName,
|
||||||
|
// Omit this prop to avoid invalid HTML attribute warning
|
||||||
|
isRequired: _isRequired,
|
||||||
|
...inputProps
|
||||||
|
}: Omit<InputBaseProps, "label" | "hint">) => {
|
||||||
|
// Check if the input has a leading icon or tooltip
|
||||||
|
const hasTrailingIcon = tooltip || isInvalid;
|
||||||
|
const hasLeadingIcon = Icon;
|
||||||
|
|
||||||
|
// If the input is inside a `TextFieldContext`, use its context to simplify applying styles
|
||||||
|
const context = useContext(TextFieldContext);
|
||||||
|
|
||||||
|
const inputSize = context?.size || size;
|
||||||
|
|
||||||
|
const sizes = sortCx({
|
||||||
|
sm: {
|
||||||
|
root: cx("px-3 py-2", hasTrailingIcon && "pr-9", hasLeadingIcon && "pl-10"),
|
||||||
|
iconLeading: "left-3",
|
||||||
|
iconTrailing: "right-3",
|
||||||
|
shortcut: "pr-2.5",
|
||||||
|
},
|
||||||
|
md: {
|
||||||
|
root: cx("px-3.5 py-2.5", hasTrailingIcon && "pr-9.5", hasLeadingIcon && "pl-10.5"),
|
||||||
|
iconLeading: "left-3.5",
|
||||||
|
iconTrailing: "right-3.5",
|
||||||
|
shortcut: "pr-3",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AriaGroup
|
||||||
|
{...{ isDisabled, isInvalid }}
|
||||||
|
ref={groupRef}
|
||||||
|
className={({ isFocusWithin, isDisabled, isInvalid }) =>
|
||||||
|
cx(
|
||||||
|
"relative flex w-full flex-row place-content-center place-items-center rounded-lg bg-primary shadow-xs ring-1 ring-primary transition-shadow duration-100 ease-linear ring-inset",
|
||||||
|
|
||||||
|
isFocusWithin && !isDisabled && "ring-2 ring-brand",
|
||||||
|
|
||||||
|
// Disabled state styles
|
||||||
|
isDisabled && "cursor-not-allowed bg-disabled_subtle ring-disabled",
|
||||||
|
"group-disabled:cursor-not-allowed group-disabled:bg-disabled_subtle group-disabled:ring-disabled",
|
||||||
|
|
||||||
|
// Invalid state styles
|
||||||
|
isInvalid && "ring-error_subtle",
|
||||||
|
"group-invalid:ring-error_subtle",
|
||||||
|
|
||||||
|
// Invalid state with focus-within styles
|
||||||
|
isInvalid && isFocusWithin && "ring-2 ring-error",
|
||||||
|
isFocusWithin && "group-invalid:ring-2 group-invalid:ring-error",
|
||||||
|
|
||||||
|
context?.wrapperClassName,
|
||||||
|
wrapperClassName,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{/* Leading icon and Payment icon */}
|
||||||
|
{Icon && (
|
||||||
|
<Icon
|
||||||
|
className={cx(
|
||||||
|
"pointer-events-none absolute size-5 text-fg-quaternary",
|
||||||
|
isDisabled && "text-fg-disabled",
|
||||||
|
sizes[inputSize].iconLeading,
|
||||||
|
context?.iconClassName,
|
||||||
|
iconClassName,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Input field */}
|
||||||
|
<AriaInput
|
||||||
|
{...(inputProps as AriaInputProps)}
|
||||||
|
ref={ref}
|
||||||
|
placeholder={placeholder}
|
||||||
|
className={cx(
|
||||||
|
"m-0 w-full bg-transparent text-md text-primary ring-0 outline-hidden placeholder:text-placeholder autofill:rounded-lg autofill:text-primary",
|
||||||
|
isDisabled && "cursor-not-allowed text-disabled",
|
||||||
|
sizes[inputSize].root,
|
||||||
|
context?.inputClassName,
|
||||||
|
inputClassName,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Tooltip and help icon */}
|
||||||
|
{tooltip && !isInvalid && (
|
||||||
|
<Tooltip title={tooltip} placement="top">
|
||||||
|
<TooltipTrigger
|
||||||
|
className={cx(
|
||||||
|
"absolute cursor-pointer text-fg-quaternary transition duration-200 hover:text-fg-quaternary_hover focus:text-fg-quaternary_hover",
|
||||||
|
sizes[inputSize].iconTrailing,
|
||||||
|
context?.tooltipClassName,
|
||||||
|
tooltipClassName,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<HelpCircle className="size-4" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Invalid icon */}
|
||||||
|
{isInvalid && (
|
||||||
|
<InfoCircle
|
||||||
|
className={cx(
|
||||||
|
"pointer-events-none absolute size-4 text-fg-error-secondary",
|
||||||
|
sizes[inputSize].iconTrailing,
|
||||||
|
context?.tooltipClassName,
|
||||||
|
tooltipClassName,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Shortcut */}
|
||||||
|
{shortcut && (
|
||||||
|
<div
|
||||||
|
className={cx(
|
||||||
|
"pointer-events-none absolute inset-y-0.5 right-0.5 z-10 flex items-center rounded-r-[inherit] bg-linear-to-r from-transparent to-bg-primary to-40% pl-8",
|
||||||
|
sizes[inputSize].shortcut,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={cx(
|
||||||
|
"pointer-events-none rounded px-1 py-px text-xs font-medium text-quaternary ring-1 ring-secondary select-none ring-inset",
|
||||||
|
isDisabled && "bg-transparent text-disabled",
|
||||||
|
)}
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
{typeof shortcut === "string" ? shortcut : "⌘K"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</AriaGroup>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
InputBase.displayName = "InputBase";
|
||||||
|
|
||||||
|
interface BaseProps {
|
||||||
|
/** Label text for the input */
|
||||||
|
label?: string;
|
||||||
|
/** Helper text displayed below the input */
|
||||||
|
hint?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TextFieldProps
|
||||||
|
extends BaseProps,
|
||||||
|
AriaTextFieldProps,
|
||||||
|
Pick<InputBaseProps, "size" | "wrapperClassName" | "inputClassName" | "iconClassName" | "tooltipClassName"> {
|
||||||
|
ref?: Ref<HTMLDivElement>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TextFieldContext = createContext<TextFieldProps>({});
|
||||||
|
|
||||||
|
export const TextField = ({ className, ...props }: TextFieldProps) => {
|
||||||
|
return (
|
||||||
|
<TextFieldContext.Provider value={props}>
|
||||||
|
<AriaTextField
|
||||||
|
{...props}
|
||||||
|
data-input-wrapper
|
||||||
|
className={(state) =>
|
||||||
|
cx("group flex h-max w-full flex-col items-start justify-start gap-1.5", typeof className === "function" ? className(state) : className)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</TextFieldContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
TextField.displayName = "TextField";
|
||||||
|
|
||||||
|
interface InputProps extends InputBaseProps, BaseProps {
|
||||||
|
/** Whether to hide required indicator from label */
|
||||||
|
hideRequiredIndicator?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Input = ({
|
||||||
|
size = "sm",
|
||||||
|
placeholder,
|
||||||
|
icon: Icon,
|
||||||
|
label,
|
||||||
|
hint,
|
||||||
|
shortcut,
|
||||||
|
hideRequiredIndicator,
|
||||||
|
className,
|
||||||
|
ref,
|
||||||
|
groupRef,
|
||||||
|
tooltip,
|
||||||
|
iconClassName,
|
||||||
|
inputClassName,
|
||||||
|
wrapperClassName,
|
||||||
|
tooltipClassName,
|
||||||
|
...props
|
||||||
|
}: InputProps) => {
|
||||||
|
return (
|
||||||
|
<TextField aria-label={!label ? placeholder : undefined} {...props} className={className}>
|
||||||
|
{({ isRequired, isInvalid }) => (
|
||||||
|
<>
|
||||||
|
{label && <Label isRequired={hideRequiredIndicator ? !hideRequiredIndicator : isRequired}>{label}</Label>}
|
||||||
|
|
||||||
|
<InputBase
|
||||||
|
{...{
|
||||||
|
ref,
|
||||||
|
groupRef,
|
||||||
|
size,
|
||||||
|
placeholder,
|
||||||
|
icon: Icon,
|
||||||
|
shortcut,
|
||||||
|
iconClassName,
|
||||||
|
inputClassName,
|
||||||
|
wrapperClassName,
|
||||||
|
tooltipClassName,
|
||||||
|
tooltip,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{hint && <HintText isInvalid={isInvalid}>{hint}</HintText>}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</TextField>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Input.displayName = "Input";
|
||||||
48
src/components/base/input/label.tsx
Normal file
48
src/components/base/input/label.tsx
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import type { ReactNode, Ref } from "react";
|
||||||
|
import { HelpCircle } from "@untitledui/icons";
|
||||||
|
import type { LabelProps as AriaLabelProps } from "react-aria-components";
|
||||||
|
import { Label as AriaLabel } from "react-aria-components";
|
||||||
|
import { Tooltip, TooltipTrigger } from "@/components/base/tooltip/tooltip";
|
||||||
|
import { cx } from "@/utils/cx";
|
||||||
|
|
||||||
|
interface LabelProps extends AriaLabelProps {
|
||||||
|
children: ReactNode;
|
||||||
|
isRequired?: boolean;
|
||||||
|
tooltip?: string;
|
||||||
|
tooltipDescription?: string;
|
||||||
|
ref?: Ref<HTMLLabelElement>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Label = ({ isRequired, tooltip, tooltipDescription, className, ...props }: LabelProps) => {
|
||||||
|
return (
|
||||||
|
<AriaLabel
|
||||||
|
// Used for conditionally hiding/showing the label element via CSS:
|
||||||
|
// <Input label="Visible only on mobile" className="lg:**:data-label:hidden" />
|
||||||
|
// or
|
||||||
|
// <Input label="Visible only on mobile" className="lg:label:hidden" />
|
||||||
|
data-label="true"
|
||||||
|
{...props}
|
||||||
|
className={cx("flex cursor-default items-center gap-0.5 text-sm font-medium text-secondary", className)}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
|
||||||
|
<span className={cx("hidden text-brand-tertiary", isRequired && "block", typeof isRequired === "undefined" && "group-required:block")}>*</span>
|
||||||
|
|
||||||
|
{tooltip && (
|
||||||
|
<Tooltip title={tooltip} description={tooltipDescription} placement="top">
|
||||||
|
<TooltipTrigger
|
||||||
|
// `TooltipTrigger` inherits the disabled state from the parent form field
|
||||||
|
// but we don't that. We want the tooltip be enabled even if the parent
|
||||||
|
// field is disabled.
|
||||||
|
isDisabled={false}
|
||||||
|
className="cursor-pointer text-fg-quaternary transition duration-200 hover:text-fg-quaternary_hover focus:text-fg-quaternary_hover"
|
||||||
|
>
|
||||||
|
<HelpCircle className="size-4" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</AriaLabel>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Label.displayName = "Label";
|
||||||
152
src/components/base/pin-input/pin-input.tsx
Normal file
152
src/components/base/pin-input/pin-input.tsx
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
import type { ComponentPropsWithRef } from "react";
|
||||||
|
import { createContext, useContext, useId } from "react";
|
||||||
|
import { OTPInput, OTPInputContext } from "input-otp";
|
||||||
|
import { cx } from "@/utils/cx";
|
||||||
|
|
||||||
|
type PinInputContextType = {
|
||||||
|
size: "sm" | "md" | "lg";
|
||||||
|
disabled: boolean;
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const PinInputContext = createContext<PinInputContextType>({
|
||||||
|
size: "sm",
|
||||||
|
id: "",
|
||||||
|
disabled: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const usePinInputContext = () => {
|
||||||
|
const context = useContext(PinInputContext);
|
||||||
|
|
||||||
|
if (!context) {
|
||||||
|
throw new Error("The 'usePinInputContext' hook must be used within a '<PinInput />'");
|
||||||
|
}
|
||||||
|
|
||||||
|
return context;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface RootProps extends ComponentPropsWithRef<"div"> {
|
||||||
|
size?: "sm" | "md" | "lg";
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Root = ({ className, size = "md", disabled = false, ...props }: RootProps) => {
|
||||||
|
const id = useId();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PinInputContext.Provider value={{ size, disabled, id }}>
|
||||||
|
<div role="group" className={cx("flex h-max flex-col gap-1.5", className)} {...props} />
|
||||||
|
</PinInputContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
Root.displayName = "Root";
|
||||||
|
|
||||||
|
type GroupProps = ComponentPropsWithRef<typeof OTPInput> & {
|
||||||
|
width?: number;
|
||||||
|
inputClassName?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Group = ({ inputClassName, containerClassName, width, maxLength = 4, ...props }: GroupProps) => {
|
||||||
|
const { id, size, disabled } = usePinInputContext();
|
||||||
|
|
||||||
|
const heights = {
|
||||||
|
sm: "h-16.5",
|
||||||
|
md: "h-20.5",
|
||||||
|
lg: "h-24.5",
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<OTPInput
|
||||||
|
{...props}
|
||||||
|
size={width}
|
||||||
|
maxLength={maxLength}
|
||||||
|
disabled={disabled}
|
||||||
|
id={"pin-input-" + id}
|
||||||
|
aria-label="Enter your pin"
|
||||||
|
aria-labelledby={"pin-input-label-" + id}
|
||||||
|
aria-describedby={"pin-input-description-" + id}
|
||||||
|
containerClassName={cx("flex flex-row gap-3", size === "sm" && "gap-2", heights[size], containerClassName)}
|
||||||
|
className={cx("w-full! disabled:cursor-not-allowed", inputClassName)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
Group.displayName = "Group";
|
||||||
|
|
||||||
|
const sizes = {
|
||||||
|
sm: "size-16 px-2 py-0.5 text-display-lg font-medium",
|
||||||
|
md: "size-20 px-2 py-2.5 text-display-lg font-medium",
|
||||||
|
lg: "size-24 px-2 py-3 text-display-xl font-medium",
|
||||||
|
};
|
||||||
|
|
||||||
|
const Slot = ({ index, className, ...props }: ComponentPropsWithRef<"div"> & { index: number }) => {
|
||||||
|
const { size, disabled } = usePinInputContext();
|
||||||
|
const { slots, isFocused } = useContext(OTPInputContext);
|
||||||
|
const slot = slots[index];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
{...props}
|
||||||
|
aria-label={"Enter digit " + (index + 1) + " of " + slots.length}
|
||||||
|
className={cx(
|
||||||
|
"relative flex items-center justify-center rounded-xl bg-primary text-center text-placeholder_subtle shadow-xs ring-1 ring-primary transition-[box-shadow,background-color] duration-100 ease-linear ring-inset",
|
||||||
|
sizes[size],
|
||||||
|
isFocused && slot?.isActive && "ring-2 ring-brand outline-2 outline-offset-2 outline-brand",
|
||||||
|
slot?.char && "text-brand-tertiary_alt ring-2 ring-brand",
|
||||||
|
disabled && "bg-disabled_subtle text-fg-disabled_subtle ring-disabled",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{slot?.char ? slot.char : slot?.hasFakeCaret ? <FakeCaret size={size} /> : 0}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
Slot.displayName = "Slot";
|
||||||
|
|
||||||
|
const FakeCaret = ({ size = "md" }: { size?: "sm" | "md" | "lg" }) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cx(
|
||||||
|
"pointer-events-none h-[1em] w-0.5 animate-caret-blink bg-fg-brand-primary",
|
||||||
|
size === "lg" ? "text-display-xl font-medium" : "text-display-lg font-medium",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Separator = (props: ComponentPropsWithRef<"p">) => {
|
||||||
|
return (
|
||||||
|
<div role="separator" {...props} className={cx("text-center text-display-xl font-medium text-placeholder_subtle", props.className)}>
|
||||||
|
-
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
Separator.displayName = "Separator";
|
||||||
|
|
||||||
|
const Label = ({ className, ...props }: ComponentPropsWithRef<"label">) => {
|
||||||
|
const { id } = usePinInputContext();
|
||||||
|
|
||||||
|
return <label {...props} htmlFor={"pin-input-" + id} id={"pin-input-label-" + id} className={cx("text-sm font-medium text-secondary", className)} />;
|
||||||
|
};
|
||||||
|
Label.displayName = "Label";
|
||||||
|
|
||||||
|
const Description = ({ className, ...props }: ComponentPropsWithRef<"p">) => {
|
||||||
|
const { id } = usePinInputContext();
|
||||||
|
|
||||||
|
return <p {...props} id={"pin-input-description-" + id} role="description" className={cx("text-sm text-tertiary", className)} />;
|
||||||
|
};
|
||||||
|
Description.displayName = "Description";
|
||||||
|
|
||||||
|
const PinInput = Root as typeof Root & {
|
||||||
|
Slot: typeof Slot;
|
||||||
|
Label: typeof Label;
|
||||||
|
Group: typeof Group;
|
||||||
|
Separator: typeof Separator;
|
||||||
|
Description: typeof Description;
|
||||||
|
};
|
||||||
|
PinInput.Slot = Slot;
|
||||||
|
PinInput.Label = Label;
|
||||||
|
PinInput.Group = Group;
|
||||||
|
PinInput.Separator = Separator;
|
||||||
|
PinInput.Description = Description;
|
||||||
|
|
||||||
|
export { PinInput };
|
||||||
174
src/components/base/progress-indicators/progress-circles.tsx
Normal file
174
src/components/base/progress-indicators/progress-circles.tsx
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
import { cx as clx, sortCx } from "@/utils/cx";
|
||||||
|
|
||||||
|
interface ProgressBarProps {
|
||||||
|
value: number;
|
||||||
|
min?: number;
|
||||||
|
max?: number;
|
||||||
|
size: "xxs" | "xs" | "sm" | "md" | "lg";
|
||||||
|
label?: string;
|
||||||
|
valueFormatter?: (value: number, valueInPercentage: number) => string | number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sizes = sortCx({
|
||||||
|
xxs: {
|
||||||
|
strokeWidth: 6,
|
||||||
|
radius: 29,
|
||||||
|
valueClass: "text-sm font-semibold text-primary",
|
||||||
|
labelClass: "text-xs font-medium text-tertiary",
|
||||||
|
halfCircleTextPosition: "absolute bottom-0.5 text-center",
|
||||||
|
},
|
||||||
|
xs: {
|
||||||
|
strokeWidth: 16,
|
||||||
|
radius: 72,
|
||||||
|
valueClass: "text-display-xs font-semibold text-primary",
|
||||||
|
labelClass: "text-xs font-medium text-tertiary",
|
||||||
|
halfCircleTextPosition: "absolute bottom-0.5 text-center",
|
||||||
|
},
|
||||||
|
sm: {
|
||||||
|
strokeWidth: 20,
|
||||||
|
radius: 90,
|
||||||
|
valueClass: "text-display-sm font-semibold text-primary",
|
||||||
|
labelClass: "text-xs font-medium text-tertiary",
|
||||||
|
halfCircleTextPosition: "absolute bottom-1 text-center",
|
||||||
|
},
|
||||||
|
md: {
|
||||||
|
strokeWidth: 24,
|
||||||
|
radius: 108,
|
||||||
|
valueClass: "text-display-md font-semibold text-primary",
|
||||||
|
labelClass: "text-sm font-medium text-tertiary",
|
||||||
|
halfCircleTextPosition: "absolute bottom-1 text-center",
|
||||||
|
},
|
||||||
|
lg: {
|
||||||
|
strokeWidth: 28,
|
||||||
|
radius: 126,
|
||||||
|
valueClass: "text-display-lg font-semibold text-primary",
|
||||||
|
labelClass: "text-sm font-medium text-tertiary",
|
||||||
|
halfCircleTextPosition: "absolute bottom-0 text-center",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const ProgressBarCircle = ({ value, min = 0, max = 100, size, label, valueFormatter }: ProgressBarProps) => {
|
||||||
|
const percentage = Math.round(((value - min) * 100) / (max - min));
|
||||||
|
|
||||||
|
const sizeConfig = sizes[size];
|
||||||
|
|
||||||
|
const { strokeWidth, radius, valueClass, labelClass } = sizeConfig;
|
||||||
|
|
||||||
|
const diameter = 2 * (radius + strokeWidth / 2);
|
||||||
|
const width = diameter;
|
||||||
|
const height = diameter;
|
||||||
|
const viewBox = `0 0 ${width} ${height}`;
|
||||||
|
const cx = diameter / 2;
|
||||||
|
const cy = diameter / 2;
|
||||||
|
|
||||||
|
const textPosition = label ? "absolute text-center" : "absolute text-primary";
|
||||||
|
const strokeDashoffset = 100 - percentage;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center gap-0.5">
|
||||||
|
<div role="progressbar" aria-valuenow={value} aria-valuemin={min} aria-valuemax={max} className="relative flex w-max items-center justify-center">
|
||||||
|
<svg className="-rotate-90" width={width} height={height} viewBox={viewBox}>
|
||||||
|
{/* Background circle */}
|
||||||
|
<circle
|
||||||
|
className="stroke-bg-quaternary"
|
||||||
|
cx={cx}
|
||||||
|
cy={cy}
|
||||||
|
r={radius}
|
||||||
|
fill="none"
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
pathLength="100"
|
||||||
|
strokeDasharray="100"
|
||||||
|
strokeLinecap="round"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Foreground circle */}
|
||||||
|
<circle
|
||||||
|
className="stroke-fg-brand-primary"
|
||||||
|
cx={cx}
|
||||||
|
cy={cy}
|
||||||
|
r={radius}
|
||||||
|
fill="none"
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
pathLength="100"
|
||||||
|
strokeDasharray="100"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeDashoffset={strokeDashoffset}
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
{label && size !== "xxs" ? (
|
||||||
|
<div className="absolute text-center">
|
||||||
|
<div className={labelClass}>{label}</div>
|
||||||
|
<div className={valueClass}>{valueFormatter ? valueFormatter(value, percentage) : `${percentage}%`}</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<span className={clx(textPosition, valueClass)}>{valueFormatter ? valueFormatter(value, percentage) : `${percentage}%`}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{label && size === "xxs" && <div className={labelClass}>{label}</div>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ProgressBarHalfCircle = ({ value, min = 0, max = 100, size, label, valueFormatter }: ProgressBarProps) => {
|
||||||
|
const percentage = Math.round(((value - min) * 100) / (max - min));
|
||||||
|
|
||||||
|
const sizeConfig = sizes[size];
|
||||||
|
|
||||||
|
const { strokeWidth, radius, valueClass, labelClass, halfCircleTextPosition } = sizeConfig;
|
||||||
|
|
||||||
|
const width = 2 * (radius + strokeWidth / 2);
|
||||||
|
const height = radius + strokeWidth;
|
||||||
|
const viewBox = `0 0 ${width} ${height}`;
|
||||||
|
const cx = "50%";
|
||||||
|
const cy = radius + strokeWidth / 2;
|
||||||
|
|
||||||
|
const strokeDashoffset = -50 - (100 - percentage) / 2;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center gap-0.5">
|
||||||
|
<div role="progressbar" aria-valuenow={value} aria-valuemin={min} aria-valuemax={max} className="relative flex w-max items-center justify-center">
|
||||||
|
<svg width={width} height={height} viewBox={viewBox}>
|
||||||
|
{/* Background half-circle */}
|
||||||
|
<circle
|
||||||
|
className="stroke-bg-quaternary"
|
||||||
|
cx={cx}
|
||||||
|
cy={cy}
|
||||||
|
r={radius}
|
||||||
|
fill="none"
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
pathLength="100"
|
||||||
|
strokeDasharray="100"
|
||||||
|
strokeDashoffset="-50"
|
||||||
|
strokeLinecap="round"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Foreground half-circle */}
|
||||||
|
<circle
|
||||||
|
className="origin-center -scale-x-100 stroke-fg-brand-primary"
|
||||||
|
cx={cx}
|
||||||
|
cy={cy}
|
||||||
|
r={radius}
|
||||||
|
fill="none"
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
pathLength="100"
|
||||||
|
strokeDasharray="100"
|
||||||
|
strokeDashoffset={strokeDashoffset}
|
||||||
|
strokeLinecap="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
{label && size !== "xxs" ? (
|
||||||
|
<div className={halfCircleTextPosition}>
|
||||||
|
<div className={labelClass}>{label}</div>
|
||||||
|
<div className={valueClass}>{valueFormatter ? valueFormatter(value, percentage) : `${percentage}%`}</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<span className={clx(halfCircleTextPosition, valueClass)}>{valueFormatter ? valueFormatter(value, percentage) : `${percentage}%`}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{label && size === "xxs" && <div className={labelClass}>{label}</div>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
121
src/components/base/progress-indicators/progress-indicators.tsx
Normal file
121
src/components/base/progress-indicators/progress-indicators.tsx
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
import { cx } from "@/utils/cx";
|
||||||
|
|
||||||
|
export interface ProgressBarProps {
|
||||||
|
/**
|
||||||
|
* The current value of the progress bar.
|
||||||
|
*/
|
||||||
|
value: number;
|
||||||
|
/**
|
||||||
|
* The minimum value of the progress bar.
|
||||||
|
* @default 0
|
||||||
|
*/
|
||||||
|
min?: number;
|
||||||
|
/**
|
||||||
|
* The maximum value of the progress bar.
|
||||||
|
* @default 100
|
||||||
|
*/
|
||||||
|
max?: number;
|
||||||
|
/**
|
||||||
|
* Optional additional CSS class names for the progress bar container.
|
||||||
|
*/
|
||||||
|
className?: string;
|
||||||
|
/**
|
||||||
|
* Optional additional CSS class names for the progress bar indicator element.
|
||||||
|
*/
|
||||||
|
progressClassName?: string;
|
||||||
|
/**
|
||||||
|
* Optional function to format the displayed value.
|
||||||
|
* It receives the raw value and the calculated percentage.
|
||||||
|
*/
|
||||||
|
valueFormatter?: (value: number, valueInPercentage: number) => string | number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A basic progress bar component.
|
||||||
|
*/
|
||||||
|
export const ProgressBarBase = ({ value, min = 0, max = 100, className, progressClassName }: ProgressBarProps) => {
|
||||||
|
const percentage = ((value - min) * 100) / (max - min);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
role="progressbar"
|
||||||
|
aria-valuenow={value}
|
||||||
|
aria-valuemin={min}
|
||||||
|
aria-valuemax={max}
|
||||||
|
className={cx("h-2 w-full overflow-hidden rounded-md bg-quaternary", className)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
// Use transform instead of width to avoid layout thrashing (and for smoother animation)
|
||||||
|
style={{ transform: `translateX(-${100 - percentage}%)` }}
|
||||||
|
className={cx("size-full rounded-md bg-fg-brand-primary transition duration-75 ease-linear", progressClassName)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
type ProgressBarLabelPosition = "right" | "bottom" | "top-floating" | "bottom-floating";
|
||||||
|
|
||||||
|
export interface ProgressIndicatorWithTextProps extends ProgressBarProps {
|
||||||
|
/**
|
||||||
|
* Specifies the layout of the text relative to the progress bar.
|
||||||
|
* - `right`: Text is displayed to the right of the progress bar.
|
||||||
|
* - `bottom`: Text is displayed below the progress bar, aligned to the right.
|
||||||
|
* - `top-floating`: Text is displayed in a floating tooltip above the progress indicator.
|
||||||
|
* - `bottom-floating`: Text is displayed in a floating tooltip below the progress indicator.
|
||||||
|
*/
|
||||||
|
labelPosition?: ProgressBarLabelPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A progress bar component that displays the value text in various configurable layouts.
|
||||||
|
*/
|
||||||
|
export const ProgressBar = ({ value, min = 0, max = 100, valueFormatter, labelPosition, className, progressClassName }: ProgressIndicatorWithTextProps) => {
|
||||||
|
const percentage = ((value - min) * 100) / (max - min);
|
||||||
|
const formattedValue = valueFormatter ? valueFormatter(value, percentage) : `${percentage.toFixed(0)}%`; // Default to rounded percentage
|
||||||
|
|
||||||
|
const baseProgressBar = <ProgressBarBase min={min} max={max} value={value} className={className} progressClassName={progressClassName} />;
|
||||||
|
|
||||||
|
switch (labelPosition) {
|
||||||
|
case "right":
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
{baseProgressBar}
|
||||||
|
<span className="shrink-0 text-sm font-medium text-secondary tabular-nums">{formattedValue}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
case "bottom":
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-end gap-2">
|
||||||
|
{baseProgressBar}
|
||||||
|
<span className="text-sm font-medium text-secondary tabular-nums">{formattedValue}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
case "top-floating":
|
||||||
|
return (
|
||||||
|
<div className="relative flex flex-col items-end gap-2">
|
||||||
|
{baseProgressBar}
|
||||||
|
<div
|
||||||
|
style={{ left: `${percentage}%` }}
|
||||||
|
className="absolute -top-2 -translate-x-1/2 -translate-y-full rounded-lg bg-primary_alt px-3 py-2 shadow-lg ring-1 ring-secondary_alt"
|
||||||
|
>
|
||||||
|
<div className="text-xs font-semibold text-secondary tabular-nums">{formattedValue}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
case "bottom-floating":
|
||||||
|
return (
|
||||||
|
<div className="relative flex flex-col items-end gap-2">
|
||||||
|
{baseProgressBar}
|
||||||
|
<div
|
||||||
|
style={{ left: `${percentage}%` }}
|
||||||
|
className="absolute -bottom-2 -translate-x-1/2 translate-y-full rounded-lg bg-primary_alt px-3 py-2 shadow-lg ring-1 ring-secondary_alt"
|
||||||
|
>
|
||||||
|
<div className="text-xs font-semibold text-secondary">{formattedValue}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
// Fallback or default case, could render the basic progress bar or throw an error
|
||||||
|
return baseProgressBar;
|
||||||
|
}
|
||||||
|
};
|
||||||
27
src/components/base/progress-indicators/simple-circle.tsx
Normal file
27
src/components/base/progress-indicators/simple-circle.tsx
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
export const CircleProgressBar = (props: { value: number; min?: 0; max?: 100 }) => {
|
||||||
|
const { value, min = 0, max = 100 } = props;
|
||||||
|
const percentage = ((value - min) * 100) / (max - min);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div role="progressbar" aria-valuenow={value} aria-valuemin={min} aria-valuemax={max} className="relative flex w-max items-center justify-center">
|
||||||
|
<span className="absolute text-sm font-medium text-primary">{percentage}%</span>
|
||||||
|
<svg className="size-16 -rotate-90" viewBox="0 0 60 60">
|
||||||
|
<circle className="stroke-bg-quaternary" cx="30" cy="30" r="26" fill="none" strokeWidth="6" />
|
||||||
|
<circle
|
||||||
|
className="stroke-fg-brand-primary"
|
||||||
|
style={{
|
||||||
|
strokeDashoffset: `calc(100 - ${percentage})`,
|
||||||
|
}}
|
||||||
|
cx="30"
|
||||||
|
cy="30"
|
||||||
|
r="26"
|
||||||
|
fill="none"
|
||||||
|
strokeWidth="6"
|
||||||
|
strokeDasharray="100"
|
||||||
|
pathLength="100"
|
||||||
|
strokeLinecap="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
127
src/components/base/radio-buttons/radio-buttons.tsx
Normal file
127
src/components/base/radio-buttons/radio-buttons.tsx
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
import { type ReactNode, type Ref, createContext, useContext } from "react";
|
||||||
|
import {
|
||||||
|
Radio as AriaRadio,
|
||||||
|
RadioGroup as AriaRadioGroup,
|
||||||
|
type RadioGroupProps as AriaRadioGroupProps,
|
||||||
|
type RadioProps as AriaRadioProps,
|
||||||
|
} from "react-aria-components";
|
||||||
|
import { cx } from "@/utils/cx";
|
||||||
|
|
||||||
|
export interface RadioGroupContextType {
|
||||||
|
size?: "sm" | "md";
|
||||||
|
}
|
||||||
|
|
||||||
|
const RadioGroupContext = createContext<RadioGroupContextType | null>(null);
|
||||||
|
|
||||||
|
export interface RadioButtonBaseProps {
|
||||||
|
size?: "sm" | "md";
|
||||||
|
className?: string;
|
||||||
|
isFocusVisible?: boolean;
|
||||||
|
isSelected?: boolean;
|
||||||
|
isDisabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RadioButtonBase = ({ className, isFocusVisible, isSelected, isDisabled, size = "sm" }: RadioButtonBaseProps) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cx(
|
||||||
|
"flex size-4 min-h-4 min-w-4 cursor-pointer appearance-none items-center justify-center rounded-full bg-primary ring-1 ring-primary ring-inset",
|
||||||
|
size === "md" && "size-5 min-h-5 min-w-5",
|
||||||
|
isSelected && !isDisabled && "bg-brand-solid ring-bg-brand-solid",
|
||||||
|
isDisabled && "cursor-not-allowed border-disabled bg-disabled_subtle",
|
||||||
|
isFocusVisible && "outline-2 outline-offset-2 outline-focus-ring",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={cx(
|
||||||
|
"size-1.5 rounded-full bg-fg-white opacity-0 transition-inherit-all",
|
||||||
|
size === "md" && "size-2",
|
||||||
|
isDisabled && "bg-fg-disabled_subtle",
|
||||||
|
isSelected && "opacity-100",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
RadioButtonBase.displayName = "RadioButtonBase";
|
||||||
|
|
||||||
|
interface RadioButtonProps extends AriaRadioProps {
|
||||||
|
size?: "sm" | "md";
|
||||||
|
label?: ReactNode;
|
||||||
|
hint?: ReactNode;
|
||||||
|
ref?: Ref<HTMLLabelElement>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RadioButton = ({ label, hint, className, size = "sm", ...ariaRadioProps }: RadioButtonProps) => {
|
||||||
|
const context = useContext(RadioGroupContext);
|
||||||
|
|
||||||
|
size = context?.size ?? size;
|
||||||
|
|
||||||
|
const sizes = {
|
||||||
|
sm: {
|
||||||
|
root: "gap-2",
|
||||||
|
textWrapper: "",
|
||||||
|
label: "text-sm font-medium",
|
||||||
|
hint: "text-sm",
|
||||||
|
},
|
||||||
|
md: {
|
||||||
|
root: "gap-3",
|
||||||
|
textWrapper: "gap-0.5",
|
||||||
|
label: "text-md font-medium",
|
||||||
|
hint: "text-md",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AriaRadio
|
||||||
|
{...ariaRadioProps}
|
||||||
|
className={(renderProps) =>
|
||||||
|
cx(
|
||||||
|
"flex items-start",
|
||||||
|
renderProps.isDisabled && "cursor-not-allowed",
|
||||||
|
sizes[size].root,
|
||||||
|
typeof className === "function" ? className(renderProps) : className,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{({ isSelected, isDisabled, isFocusVisible }) => (
|
||||||
|
<>
|
||||||
|
<RadioButtonBase
|
||||||
|
size={size}
|
||||||
|
isSelected={isSelected}
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
isFocusVisible={isFocusVisible}
|
||||||
|
className={label || hint ? "mt-0.5" : ""}
|
||||||
|
/>
|
||||||
|
{(label || hint) && (
|
||||||
|
<div className={cx("inline-flex flex-col", sizes[size].textWrapper)}>
|
||||||
|
{label && <p className={cx("text-secondary select-none", sizes[size].label)}>{label}</p>}
|
||||||
|
{hint && (
|
||||||
|
<span className={cx("text-tertiary", sizes[size].hint)} onClick={(event) => event.stopPropagation()}>
|
||||||
|
{hint}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</AriaRadio>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
RadioButton.displayName = "RadioButton";
|
||||||
|
|
||||||
|
interface RadioGroupProps extends RadioGroupContextType, AriaRadioGroupProps {
|
||||||
|
children: ReactNode;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RadioGroup = ({ children, className, size = "sm", ...props }: RadioGroupProps) => {
|
||||||
|
return (
|
||||||
|
<RadioGroupContext.Provider value={{ size }}>
|
||||||
|
<AriaRadioGroup {...props} className={cx("flex flex-col gap-4", className)}>
|
||||||
|
{children}
|
||||||
|
</AriaRadioGroup>
|
||||||
|
</RadioGroupContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
150
src/components/base/select/combobox.tsx
Normal file
150
src/components/base/select/combobox.tsx
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
import type { FocusEventHandler, PointerEventHandler, RefAttributes, RefObject } from "react";
|
||||||
|
import { useCallback, useContext, useRef, useState } from "react";
|
||||||
|
import { SearchLg as SearchIcon } from "@untitledui/icons";
|
||||||
|
import type { ComboBoxProps as AriaComboBoxProps, GroupProps as AriaGroupProps, ListBoxProps as AriaListBoxProps } from "react-aria-components";
|
||||||
|
import { ComboBox as AriaComboBox, Group as AriaGroup, Input as AriaInput, ListBox as AriaListBox, ComboBoxStateContext } from "react-aria-components";
|
||||||
|
import { HintText } from "@/components/base/input/hint-text";
|
||||||
|
import { Label } from "@/components/base/input/label";
|
||||||
|
import { Popover } from "@/components/base/select/popover";
|
||||||
|
import { type CommonProps, SelectContext, type SelectItemType, sizes } from "@/components/base/select/select";
|
||||||
|
import { useResizeObserver } from "@/hooks/use-resize-observer";
|
||||||
|
import { cx } from "@/utils/cx";
|
||||||
|
|
||||||
|
interface ComboBoxProps extends Omit<AriaComboBoxProps<SelectItemType>, "children" | "items">, RefAttributes<HTMLDivElement>, CommonProps {
|
||||||
|
shortcut?: boolean;
|
||||||
|
items?: SelectItemType[];
|
||||||
|
popoverClassName?: string;
|
||||||
|
shortcutClassName?: string;
|
||||||
|
children: AriaListBoxProps<SelectItemType>["children"];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ComboBoxValueProps extends AriaGroupProps {
|
||||||
|
size: "sm" | "md";
|
||||||
|
shortcut: boolean;
|
||||||
|
placeholder?: string;
|
||||||
|
shortcutClassName?: string;
|
||||||
|
onFocus?: FocusEventHandler;
|
||||||
|
onPointerEnter?: PointerEventHandler;
|
||||||
|
ref?: RefObject<HTMLDivElement | null>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ComboBoxValue = ({ size, shortcut, placeholder, shortcutClassName, ...otherProps }: ComboBoxValueProps) => {
|
||||||
|
const state = useContext(ComboBoxStateContext);
|
||||||
|
|
||||||
|
const value = state?.selectedItem?.value || null;
|
||||||
|
const inputValue = state?.inputValue || null;
|
||||||
|
|
||||||
|
const first = inputValue?.split(value?.supportingText)?.[0] || "";
|
||||||
|
const last = inputValue?.split(first)[1];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AriaGroup
|
||||||
|
{...otherProps}
|
||||||
|
className={({ isFocusWithin, isDisabled }) =>
|
||||||
|
cx(
|
||||||
|
"relative flex w-full items-center gap-2 rounded-lg bg-primary shadow-xs ring-1 ring-primary outline-hidden transition-shadow duration-100 ease-linear ring-inset",
|
||||||
|
isDisabled && "cursor-not-allowed bg-disabled_subtle",
|
||||||
|
isFocusWithin && "ring-2 ring-brand",
|
||||||
|
sizes[size].root,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{({ isDisabled }) => (
|
||||||
|
<>
|
||||||
|
<SearchIcon className="pointer-events-none size-5 shrink-0 text-fg-quaternary" />
|
||||||
|
|
||||||
|
<div className="relative flex w-full items-center gap-2">
|
||||||
|
{inputValue && (
|
||||||
|
<span className="absolute top-1/2 z-0 inline-flex w-full -translate-y-1/2 gap-2 truncate" aria-hidden="true">
|
||||||
|
<p className={cx("text-md font-medium text-primary", isDisabled && "text-disabled")}>{first}</p>
|
||||||
|
{last && <p className={cx("-ml-0.75 text-md text-tertiary", isDisabled && "text-disabled")}>{last}</p>}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<AriaInput
|
||||||
|
placeholder={placeholder}
|
||||||
|
className="z-10 w-full appearance-none bg-transparent text-md text-transparent caret-alpha-black/90 placeholder:text-placeholder focus:outline-hidden disabled:cursor-not-allowed disabled:text-disabled disabled:placeholder:text-disabled"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{shortcut && (
|
||||||
|
<div
|
||||||
|
className={cx(
|
||||||
|
"absolute inset-y-0.5 right-0.5 z-10 flex items-center rounded-r-[inherit] bg-linear-to-r from-transparent to-bg-primary to-40% pl-8",
|
||||||
|
isDisabled && "to-bg-disabled_subtle",
|
||||||
|
sizes[size].shortcut,
|
||||||
|
shortcutClassName,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={cx(
|
||||||
|
"pointer-events-none rounded px-1 py-px text-xs font-medium text-quaternary ring-1 ring-secondary select-none ring-inset",
|
||||||
|
isDisabled && "bg-transparent text-disabled",
|
||||||
|
)}
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
⌘K
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</AriaGroup>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ComboBox = ({ placeholder = "Search", shortcut = true, size = "sm", children, items, shortcutClassName, ...otherProps }: ComboBoxProps) => {
|
||||||
|
const placeholderRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [popoverWidth, setPopoverWidth] = useState("");
|
||||||
|
|
||||||
|
// Resize observer for popover width
|
||||||
|
const onResize = useCallback(() => {
|
||||||
|
if (!placeholderRef.current) return;
|
||||||
|
|
||||||
|
const divRect = placeholderRef.current?.getBoundingClientRect();
|
||||||
|
|
||||||
|
setPopoverWidth(divRect.width + "px");
|
||||||
|
}, [placeholderRef, setPopoverWidth]);
|
||||||
|
|
||||||
|
useResizeObserver({
|
||||||
|
ref: placeholderRef,
|
||||||
|
box: "border-box",
|
||||||
|
onResize,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SelectContext.Provider value={{ size }}>
|
||||||
|
<AriaComboBox menuTrigger="focus" {...otherProps}>
|
||||||
|
{(state) => (
|
||||||
|
<div className="flex flex-col gap-1.5">
|
||||||
|
{otherProps.label && (
|
||||||
|
<Label isRequired={state.isRequired} tooltip={otherProps.tooltip}>
|
||||||
|
{otherProps.label}
|
||||||
|
</Label>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<ComboBoxValue
|
||||||
|
ref={placeholderRef}
|
||||||
|
placeholder={placeholder}
|
||||||
|
shortcut={shortcut}
|
||||||
|
shortcutClassName={shortcutClassName}
|
||||||
|
size={size}
|
||||||
|
// This is a workaround to correctly calculating the trigger width
|
||||||
|
// while using ResizeObserver wasn't 100% reliable.
|
||||||
|
onFocus={onResize}
|
||||||
|
onPointerEnter={onResize}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Popover size={size} triggerRef={placeholderRef} style={{ width: popoverWidth }} className={otherProps.popoverClassName}>
|
||||||
|
<AriaListBox items={items} className="size-full outline-hidden">
|
||||||
|
{children}
|
||||||
|
</AriaListBox>
|
||||||
|
</Popover>
|
||||||
|
|
||||||
|
{otherProps.hint && <HintText isInvalid={state.isInvalid}>{otherProps.hint}</HintText>}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</AriaComboBox>
|
||||||
|
</SelectContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
361
src/components/base/select/multi-select.tsx
Normal file
361
src/components/base/select/multi-select.tsx
Normal file
@@ -0,0 +1,361 @@
|
|||||||
|
import type { FocusEventHandler, KeyboardEvent, PointerEventHandler, RefAttributes, RefObject } from "react";
|
||||||
|
import { createContext, useCallback, useContext, useRef, useState } from "react";
|
||||||
|
import { SearchLg } from "@untitledui/icons";
|
||||||
|
import { FocusScope, useFilter, useFocusManager } from "react-aria";
|
||||||
|
import type { ComboBoxProps as AriaComboBoxProps, GroupProps as AriaGroupProps, ListBoxProps as AriaListBoxProps, Key } from "react-aria-components";
|
||||||
|
import { ComboBox as AriaComboBox, Group as AriaGroup, Input as AriaInput, ListBox as AriaListBox, ComboBoxStateContext } from "react-aria-components";
|
||||||
|
import type { ListData } from "react-stately";
|
||||||
|
import { useListData } from "react-stately";
|
||||||
|
import { Avatar } from "@/components/base/avatar/avatar";
|
||||||
|
import type { IconComponentType } from "@/components/base/badges/badge-types";
|
||||||
|
import { HintText } from "@/components/base/input/hint-text";
|
||||||
|
import { Label } from "@/components/base/input/label";
|
||||||
|
import { Popover } from "@/components/base/select/popover";
|
||||||
|
import { type SelectItemType, sizes } from "@/components/base/select/select";
|
||||||
|
import { TagCloseX } from "@/components/base/tags/base-components/tag-close-x";
|
||||||
|
import { useResizeObserver } from "@/hooks/use-resize-observer";
|
||||||
|
import { cx } from "@/utils/cx";
|
||||||
|
import { SelectItem } from "./select-item";
|
||||||
|
|
||||||
|
interface ComboBoxValueProps extends AriaGroupProps {
|
||||||
|
size: "sm" | "md";
|
||||||
|
shortcut?: boolean;
|
||||||
|
isDisabled?: boolean;
|
||||||
|
placeholder?: string;
|
||||||
|
shortcutClassName?: string;
|
||||||
|
placeholderIcon?: IconComponentType | null;
|
||||||
|
ref?: RefObject<HTMLDivElement | null>;
|
||||||
|
onFocus?: FocusEventHandler;
|
||||||
|
onPointerEnter?: PointerEventHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ComboboxContext = createContext<{
|
||||||
|
size: "sm" | "md";
|
||||||
|
selectedKeys: Key[];
|
||||||
|
selectedItems: ListData<SelectItemType>;
|
||||||
|
onRemove: (keys: Set<Key>) => void;
|
||||||
|
onInputChange: (value: string) => void;
|
||||||
|
}>({
|
||||||
|
size: "sm",
|
||||||
|
selectedKeys: [],
|
||||||
|
selectedItems: {} as ListData<SelectItemType>,
|
||||||
|
onRemove: () => {},
|
||||||
|
onInputChange: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
interface MultiSelectProps extends Omit<AriaComboBoxProps<SelectItemType>, "children" | "items">, RefAttributes<HTMLDivElement> {
|
||||||
|
hint?: string;
|
||||||
|
label?: string;
|
||||||
|
tooltip?: string;
|
||||||
|
size?: "sm" | "md";
|
||||||
|
placeholder?: string;
|
||||||
|
shortcut?: boolean;
|
||||||
|
items?: SelectItemType[];
|
||||||
|
popoverClassName?: string;
|
||||||
|
shortcutClassName?: string;
|
||||||
|
selectedItems: ListData<SelectItemType>;
|
||||||
|
placeholderIcon?: IconComponentType | null;
|
||||||
|
children: AriaListBoxProps<SelectItemType>["children"];
|
||||||
|
onItemCleared?: (key: Key) => void;
|
||||||
|
onItemInserted?: (key: Key) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MultiSelectBase = ({
|
||||||
|
items,
|
||||||
|
children,
|
||||||
|
size = "sm",
|
||||||
|
selectedItems,
|
||||||
|
onItemCleared,
|
||||||
|
onItemInserted,
|
||||||
|
shortcut,
|
||||||
|
placeholder = "Search",
|
||||||
|
// Omit these props to avoid conflicts with the `Select` component
|
||||||
|
name: _name,
|
||||||
|
className: _className,
|
||||||
|
...props
|
||||||
|
}: MultiSelectProps) => {
|
||||||
|
const { contains } = useFilter({ sensitivity: "base" });
|
||||||
|
const selectedKeys = selectedItems.items.map((item) => item.id);
|
||||||
|
|
||||||
|
const filter = useCallback(
|
||||||
|
(item: SelectItemType, filterText: string) => {
|
||||||
|
return !selectedKeys.includes(item.id) && contains(item.label || item.supportingText || "", filterText);
|
||||||
|
},
|
||||||
|
[contains, selectedKeys],
|
||||||
|
);
|
||||||
|
|
||||||
|
const accessibleList = useListData({
|
||||||
|
initialItems: items,
|
||||||
|
filter,
|
||||||
|
});
|
||||||
|
|
||||||
|
const onRemove = useCallback(
|
||||||
|
(keys: Set<Key>) => {
|
||||||
|
const key = keys.values().next().value;
|
||||||
|
|
||||||
|
if (!key) return;
|
||||||
|
|
||||||
|
selectedItems.remove(key);
|
||||||
|
onItemCleared?.(key);
|
||||||
|
},
|
||||||
|
[selectedItems, onItemCleared],
|
||||||
|
);
|
||||||
|
|
||||||
|
const onSelectionChange = (id: Key | null) => {
|
||||||
|
if (!id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const item = accessibleList.getItem(id);
|
||||||
|
|
||||||
|
if (!item) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!selectedKeys.includes(id as string)) {
|
||||||
|
selectedItems.append(item);
|
||||||
|
onItemInserted?.(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
accessibleList.setFilterText("");
|
||||||
|
};
|
||||||
|
|
||||||
|
const onInputChange = (value: string) => {
|
||||||
|
accessibleList.setFilterText(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const placeholderRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [popoverWidth, setPopoverWidth] = useState("");
|
||||||
|
|
||||||
|
// Resize observer for popover width
|
||||||
|
const onResize = useCallback(() => {
|
||||||
|
if (!placeholderRef.current) return;
|
||||||
|
let divRect = placeholderRef.current?.getBoundingClientRect();
|
||||||
|
setPopoverWidth(divRect.width + "px");
|
||||||
|
}, [placeholderRef, setPopoverWidth]);
|
||||||
|
|
||||||
|
useResizeObserver({
|
||||||
|
ref: placeholderRef,
|
||||||
|
onResize: onResize,
|
||||||
|
box: "border-box",
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ComboboxContext.Provider
|
||||||
|
value={{
|
||||||
|
size,
|
||||||
|
selectedKeys,
|
||||||
|
selectedItems,
|
||||||
|
onInputChange,
|
||||||
|
onRemove,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<AriaComboBox
|
||||||
|
allowsEmptyCollection
|
||||||
|
menuTrigger="focus"
|
||||||
|
items={accessibleList.items}
|
||||||
|
onInputChange={onInputChange}
|
||||||
|
inputValue={accessibleList.filterText}
|
||||||
|
// This keeps the combobox popover open and the input value unchanged when an item is selected.
|
||||||
|
selectedKey={null}
|
||||||
|
onSelectionChange={onSelectionChange}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{(state) => (
|
||||||
|
<div className="flex flex-col gap-1.5">
|
||||||
|
{props.label && (
|
||||||
|
<Label isRequired={state.isRequired} tooltip={props.tooltip}>
|
||||||
|
{props.label}
|
||||||
|
</Label>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<MultiSelectTagsValue
|
||||||
|
size={size}
|
||||||
|
shortcut={shortcut}
|
||||||
|
ref={placeholderRef}
|
||||||
|
placeholder={placeholder}
|
||||||
|
// This is a workaround to correctly calculating the trigger width
|
||||||
|
// while using ResizeObserver wasn't 100% reliable.
|
||||||
|
onFocus={onResize}
|
||||||
|
onPointerEnter={onResize}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Popover size={"md"} triggerRef={placeholderRef} style={{ width: popoverWidth }} className={props?.popoverClassName}>
|
||||||
|
<AriaListBox selectionMode="multiple" className="size-full outline-hidden">
|
||||||
|
{children}
|
||||||
|
</AriaListBox>
|
||||||
|
</Popover>
|
||||||
|
|
||||||
|
{props.hint && <HintText isInvalid={state.isInvalid}>{props.hint}</HintText>}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</AriaComboBox>
|
||||||
|
</ComboboxContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const InnerMultiSelect = ({ isDisabled, shortcut, shortcutClassName, placeholder }: Omit<MultiSelectProps, "selectedItems" | "children">) => {
|
||||||
|
const focusManager = useFocusManager();
|
||||||
|
const comboBoxContext = useContext(ComboboxContext);
|
||||||
|
const comboBoxStateContext = useContext(ComboBoxStateContext);
|
||||||
|
|
||||||
|
const handleInputKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
const isCaretAtStart = event.currentTarget.selectionStart === 0 && event.currentTarget.selectionEnd === 0;
|
||||||
|
|
||||||
|
if (!isCaretAtStart && event.currentTarget.value !== "") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (event.key) {
|
||||||
|
case "Backspace":
|
||||||
|
case "ArrowLeft":
|
||||||
|
focusManager?.focusPrevious({ wrap: false, tabbable: false });
|
||||||
|
break;
|
||||||
|
case "ArrowRight":
|
||||||
|
focusManager?.focusNext({ wrap: false, tabbable: false });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Ensure dropdown opens on click even if input is already focused
|
||||||
|
const handleInputMouseDown = (_event: React.MouseEvent<HTMLInputElement>) => {
|
||||||
|
if (comboBoxStateContext && !comboBoxStateContext.isOpen) {
|
||||||
|
comboBoxStateContext.open();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTagKeyDown = (event: KeyboardEvent<HTMLButtonElement>, value: Key) => {
|
||||||
|
// Do nothing when tab is clicked to move focus from the tag to the input element.
|
||||||
|
if (event.key === "Tab") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const isFirstTag = comboBoxContext?.selectedItems?.items?.[0]?.id === value;
|
||||||
|
|
||||||
|
switch (event.key) {
|
||||||
|
case " ":
|
||||||
|
case "Enter":
|
||||||
|
case "Backspace":
|
||||||
|
if (isFirstTag) {
|
||||||
|
focusManager?.focusNext({ wrap: false, tabbable: false });
|
||||||
|
} else {
|
||||||
|
focusManager?.focusPrevious({ wrap: false, tabbable: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
comboBoxContext.onRemove(new Set([value]));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "ArrowLeft":
|
||||||
|
focusManager?.focusPrevious({ wrap: false, tabbable: false });
|
||||||
|
break;
|
||||||
|
case "ArrowRight":
|
||||||
|
focusManager?.focusNext({ wrap: false, tabbable: false });
|
||||||
|
break;
|
||||||
|
case "Escape":
|
||||||
|
comboBoxStateContext?.close();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const isSelectionEmpty = comboBoxContext?.selectedItems?.items?.length === 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative flex w-full flex-1 flex-row flex-wrap items-center justify-start gap-1.5">
|
||||||
|
{!isSelectionEmpty &&
|
||||||
|
comboBoxContext?.selectedItems?.items?.map((value) => (
|
||||||
|
<span key={value.id} className="flex items-center rounded-md bg-primary py-0.5 pr-1 pl-1.25 ring-1 ring-primary ring-inset">
|
||||||
|
<Avatar size="xxs" alt={value?.label} src={value?.avatarUrl} />
|
||||||
|
|
||||||
|
<p className="ml-1.25 truncate text-sm font-medium whitespace-nowrap text-secondary select-none">{value?.label}</p>
|
||||||
|
|
||||||
|
<TagCloseX
|
||||||
|
size="md"
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
className="ml-0.75"
|
||||||
|
// For workaround, onKeyDown is added to the button
|
||||||
|
onKeyDown={(event) => handleTagKeyDown(event, value.id)}
|
||||||
|
onPress={() => comboBoxContext.onRemove(new Set([value.id]))}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<div className={cx("relative flex min-w-[20%] flex-1 flex-row items-center", !isSelectionEmpty && "ml-0.5", shortcut && "min-w-[30%]")}>
|
||||||
|
<AriaInput
|
||||||
|
placeholder={placeholder}
|
||||||
|
onKeyDown={handleInputKeyDown}
|
||||||
|
onMouseDown={handleInputMouseDown}
|
||||||
|
className="w-full flex-[1_0_0] appearance-none bg-transparent text-md text-ellipsis text-primary caret-alpha-black/90 outline-none placeholder:text-placeholder focus:outline-hidden disabled:cursor-not-allowed disabled:text-disabled disabled:placeholder:text-disabled"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{shortcut && (
|
||||||
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
|
className={cx(
|
||||||
|
"absolute inset-y-0.5 right-0.5 z-10 flex items-center rounded-r-[inherit] bg-linear-to-r from-transparent to-bg-primary to-40% pl-8",
|
||||||
|
shortcutClassName,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={cx(
|
||||||
|
"pointer-events-none rounded px-1 py-px text-xs font-medium text-quaternary ring-1 ring-secondary select-none ring-inset",
|
||||||
|
isDisabled && "bg-transparent text-disabled",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
⌘K
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MultiSelectTagsValue = ({
|
||||||
|
size,
|
||||||
|
shortcut,
|
||||||
|
placeholder,
|
||||||
|
shortcutClassName,
|
||||||
|
placeholderIcon: Icon = SearchLg,
|
||||||
|
// Omit this prop to avoid invalid HTML attribute warning
|
||||||
|
isDisabled: _isDisabled,
|
||||||
|
...otherProps
|
||||||
|
}: ComboBoxValueProps) => {
|
||||||
|
return (
|
||||||
|
<AriaGroup
|
||||||
|
{...otherProps}
|
||||||
|
className={({ isFocusWithin, isDisabled }) =>
|
||||||
|
cx(
|
||||||
|
"relative flex w-full items-center gap-2 rounded-lg bg-primary shadow-xs ring-1 ring-primary outline-hidden transition duration-100 ease-linear ring-inset",
|
||||||
|
isDisabled && "cursor-not-allowed bg-disabled_subtle",
|
||||||
|
isFocusWithin && "ring-2 ring-brand",
|
||||||
|
sizes[size].root,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{({ isDisabled }) => (
|
||||||
|
<>
|
||||||
|
{Icon && <Icon className="pointer-events-none size-5 text-fg-quaternary" />}
|
||||||
|
<FocusScope contain={false} autoFocus={false} restoreFocus={false}>
|
||||||
|
<InnerMultiSelect
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
size={size}
|
||||||
|
shortcut={shortcut}
|
||||||
|
shortcutClassName={shortcutClassName}
|
||||||
|
placeholder={placeholder}
|
||||||
|
/>
|
||||||
|
</FocusScope>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</AriaGroup>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const MultiSelect = MultiSelectBase as typeof MultiSelectBase & {
|
||||||
|
Item: typeof SelectItem;
|
||||||
|
};
|
||||||
|
|
||||||
|
MultiSelect.Item = SelectItem;
|
||||||
|
|
||||||
|
export { MultiSelect as MultiSelect };
|
||||||
32
src/components/base/select/popover.tsx
Normal file
32
src/components/base/select/popover.tsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import type { RefAttributes } from "react";
|
||||||
|
import type { PopoverProps as AriaPopoverProps } from "react-aria-components";
|
||||||
|
import { Popover as AriaPopover } from "react-aria-components";
|
||||||
|
import { cx } from "@/utils/cx";
|
||||||
|
|
||||||
|
interface PopoverProps extends AriaPopoverProps, RefAttributes<HTMLElement> {
|
||||||
|
size: "sm" | "md";
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Popover = (props: PopoverProps) => {
|
||||||
|
return (
|
||||||
|
<AriaPopover
|
||||||
|
placement="bottom"
|
||||||
|
containerPadding={0}
|
||||||
|
offset={4}
|
||||||
|
{...props}
|
||||||
|
className={(state) =>
|
||||||
|
cx(
|
||||||
|
"max-h-64! w-(--trigger-width) origin-(--trigger-anchor-point) overflow-x-hidden overflow-y-auto rounded-lg bg-primary py-1 shadow-lg ring-1 ring-secondary_alt outline-hidden will-change-transform",
|
||||||
|
|
||||||
|
state.isEntering &&
|
||||||
|
"duration-150 ease-out animate-in fade-in placement-right:slide-in-from-left-0.5 placement-top:slide-in-from-bottom-0.5 placement-bottom:slide-in-from-top-0.5",
|
||||||
|
state.isExiting &&
|
||||||
|
"duration-100 ease-in animate-out fade-out placement-right:slide-out-to-left-0.5 placement-top:slide-out-to-bottom-0.5 placement-bottom:slide-out-to-top-0.5",
|
||||||
|
props.size === "md" && "max-h-80!",
|
||||||
|
|
||||||
|
typeof props.className === "function" ? props.className(state) : props.className,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
95
src/components/base/select/select-item.tsx
Normal file
95
src/components/base/select/select-item.tsx
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
import { isValidElement, useContext } from "react";
|
||||||
|
import { Check } from "@untitledui/icons";
|
||||||
|
import type { ListBoxItemProps as AriaListBoxItemProps } from "react-aria-components";
|
||||||
|
import { ListBoxItem as AriaListBoxItem, Text as AriaText } from "react-aria-components";
|
||||||
|
import { Avatar } from "@/components/base/avatar/avatar";
|
||||||
|
import { cx } from "@/utils/cx";
|
||||||
|
import { isReactComponent } from "@/utils/is-react-component";
|
||||||
|
import type { SelectItemType } from "./select";
|
||||||
|
import { SelectContext } from "./select";
|
||||||
|
|
||||||
|
const sizes = {
|
||||||
|
sm: "p-2 pr-2.5",
|
||||||
|
md: "p-2.5 pl-2",
|
||||||
|
};
|
||||||
|
|
||||||
|
interface SelectItemProps extends Omit<AriaListBoxItemProps<SelectItemType>, "id">, SelectItemType {}
|
||||||
|
|
||||||
|
export const SelectItem = ({ label, id, value, avatarUrl, supportingText, isDisabled, icon: Icon, className, children, ...props }: SelectItemProps) => {
|
||||||
|
const { size } = useContext(SelectContext);
|
||||||
|
|
||||||
|
const labelOrChildren = label || (typeof children === "string" ? children : "");
|
||||||
|
const textValue = supportingText ? labelOrChildren + " " + supportingText : labelOrChildren;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AriaListBoxItem
|
||||||
|
id={id}
|
||||||
|
value={
|
||||||
|
value ?? {
|
||||||
|
id,
|
||||||
|
label: labelOrChildren,
|
||||||
|
avatarUrl,
|
||||||
|
supportingText,
|
||||||
|
isDisabled,
|
||||||
|
icon: Icon,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
textValue={textValue}
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
{...props}
|
||||||
|
className={(state) => cx("w-full px-1.5 py-px outline-hidden", typeof className === "function" ? className(state) : className)}
|
||||||
|
>
|
||||||
|
{(state) => (
|
||||||
|
<div
|
||||||
|
className={cx(
|
||||||
|
"flex cursor-pointer items-center gap-2 rounded-md outline-hidden select-none",
|
||||||
|
state.isSelected && "bg-active",
|
||||||
|
state.isDisabled && "cursor-not-allowed",
|
||||||
|
state.isFocused && "bg-primary_hover",
|
||||||
|
state.isFocusVisible && "ring-2 ring-focus-ring ring-inset",
|
||||||
|
|
||||||
|
// Icon styles
|
||||||
|
"*:data-icon:size-5 *:data-icon:shrink-0 *:data-icon:text-fg-quaternary",
|
||||||
|
state.isDisabled && "*:data-icon:text-fg-disabled",
|
||||||
|
|
||||||
|
sizes[size],
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{avatarUrl ? (
|
||||||
|
<Avatar aria-hidden="true" size="xs" src={avatarUrl} alt={label} />
|
||||||
|
) : isReactComponent(Icon) ? (
|
||||||
|
<Icon data-icon aria-hidden="true" />
|
||||||
|
) : isValidElement(Icon) ? (
|
||||||
|
Icon
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<div className="flex w-full min-w-0 flex-1 flex-wrap gap-x-2">
|
||||||
|
<AriaText
|
||||||
|
slot="label"
|
||||||
|
className={cx("truncate text-md font-medium whitespace-nowrap text-primary", state.isDisabled && "text-disabled")}
|
||||||
|
>
|
||||||
|
{label || (typeof children === "function" ? children(state) : children)}
|
||||||
|
</AriaText>
|
||||||
|
|
||||||
|
{supportingText && (
|
||||||
|
<AriaText slot="description" className={cx("text-md whitespace-nowrap text-tertiary", state.isDisabled && "text-disabled")}>
|
||||||
|
{supportingText}
|
||||||
|
</AriaText>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{state.isSelected && (
|
||||||
|
<Check
|
||||||
|
aria-hidden="true"
|
||||||
|
className={cx(
|
||||||
|
"ml-auto text-fg-brand-primary",
|
||||||
|
size === "sm" ? "size-4 stroke-[2.5px]" : "size-5",
|
||||||
|
state.isDisabled && "text-fg-disabled",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</AriaListBoxItem>
|
||||||
|
);
|
||||||
|
};
|
||||||
67
src/components/base/select/select-native.tsx
Normal file
67
src/components/base/select/select-native.tsx
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import { type SelectHTMLAttributes, useId } from "react";
|
||||||
|
import { ChevronDown } from "@untitledui/icons";
|
||||||
|
import { HintText } from "@/components/base/input/hint-text";
|
||||||
|
import { Label } from "@/components/base/input/label";
|
||||||
|
import { cx } from "@/utils/cx";
|
||||||
|
|
||||||
|
interface NativeSelectProps extends SelectHTMLAttributes<HTMLSelectElement> {
|
||||||
|
label?: string;
|
||||||
|
hint?: string;
|
||||||
|
selectClassName?: string;
|
||||||
|
options: { label: string; value: string; disabled?: boolean }[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NativeSelect = ({ label, hint, options, className, selectClassName, ...props }: NativeSelectProps) => {
|
||||||
|
const id = useId();
|
||||||
|
const selectId = `select-native-${id}`;
|
||||||
|
const hintId = `select-native-hint-${id}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cx("w-full in-data-input-wrapper:w-max", className)}>
|
||||||
|
{label && (
|
||||||
|
<Label htmlFor={selectId} id={selectId} className="mb-1.5">
|
||||||
|
{label}
|
||||||
|
</Label>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="relative grid w-full items-center">
|
||||||
|
<select
|
||||||
|
{...props}
|
||||||
|
id={selectId}
|
||||||
|
aria-describedby={hintId}
|
||||||
|
aria-labelledby={selectId}
|
||||||
|
className={cx(
|
||||||
|
"appearance-none rounded-lg bg-primary px-3.5 py-2.5 text-md font-medium text-primary shadow-xs ring-1 ring-primary outline-hidden transition duration-100 ease-linear ring-inset placeholder:text-fg-quaternary focus-visible:ring-2 focus-visible:ring-brand disabled:cursor-not-allowed disabled:bg-disabled_subtle disabled:text-disabled",
|
||||||
|
// Styles when the select is within an `InputGroup`
|
||||||
|
"in-data-input-wrapper:flex in-data-input-wrapper:h-full in-data-input-wrapper:gap-1 in-data-input-wrapper:bg-inherit in-data-input-wrapper:px-3 in-data-input-wrapper:py-2 in-data-input-wrapper:font-normal in-data-input-wrapper:text-tertiary in-data-input-wrapper:shadow-none in-data-input-wrapper:ring-transparent",
|
||||||
|
// Styles for the select when `TextField` is disabled
|
||||||
|
"in-data-input-wrapper:group-disabled:pointer-events-none in-data-input-wrapper:group-disabled:cursor-not-allowed in-data-input-wrapper:group-disabled:bg-transparent in-data-input-wrapper:group-disabled:text-disabled",
|
||||||
|
// Common styles for sizes and border radius within `InputGroup`
|
||||||
|
"in-data-input-wrapper:in-data-leading:rounded-r-none in-data-input-wrapper:in-data-trailing:rounded-l-none in-data-input-wrapper:in-data-[input-size=md]:py-2.5 in-data-input-wrapper:in-data-leading:in-data-[input-size=md]:pl-3.5 in-data-input-wrapper:in-data-[input-size=sm]:py-2 in-data-input-wrapper:in-data-[input-size=sm]:pl-3",
|
||||||
|
// For "leading" dropdown within `InputGroup`
|
||||||
|
"in-data-input-wrapper:in-data-leading:in-data-[input-size=md]:pr-4.5 in-data-input-wrapper:in-data-leading:in-data-[input-size=sm]:pr-4.5",
|
||||||
|
// For "trailing" dropdown within `InputGroup`
|
||||||
|
"in-data-input-wrapper:in-data-trailing:in-data-[input-size=md]:pr-8 in-data-input-wrapper:in-data-trailing:in-data-[input-size=sm]:pr-7.5",
|
||||||
|
selectClassName,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{options.map((opt) => (
|
||||||
|
<option key={opt.value} value={opt.value}>
|
||||||
|
{opt.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<ChevronDown
|
||||||
|
aria-hidden="true"
|
||||||
|
className="pointer-events-none absolute right-3.5 size-5 text-fg-quaternary in-data-input-wrapper:right-0 in-data-input-wrapper:size-4 in-data-input-wrapper:stroke-[2.625px] in-data-input-wrapper:in-data-trailing:in-data-[input-size=sm]:right-3"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{hint && (
|
||||||
|
<HintText className="mt-2" id={hintId}>
|
||||||
|
{hint}
|
||||||
|
</HintText>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
144
src/components/base/select/select.tsx
Normal file
144
src/components/base/select/select.tsx
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
import type { FC, ReactNode, Ref, RefAttributes } from "react";
|
||||||
|
import { createContext, isValidElement } from "react";
|
||||||
|
import { ChevronDown } from "@untitledui/icons";
|
||||||
|
import type { SelectProps as AriaSelectProps } from "react-aria-components";
|
||||||
|
import { Button as AriaButton, ListBox as AriaListBox, Select as AriaSelect, SelectValue as AriaSelectValue } from "react-aria-components";
|
||||||
|
import { Avatar } from "@/components/base/avatar/avatar";
|
||||||
|
import { HintText } from "@/components/base/input/hint-text";
|
||||||
|
import { Label } from "@/components/base/input/label";
|
||||||
|
import { cx } from "@/utils/cx";
|
||||||
|
import { isReactComponent } from "@/utils/is-react-component";
|
||||||
|
import { ComboBox } from "./combobox";
|
||||||
|
import { Popover } from "./popover";
|
||||||
|
import { SelectItem } from "./select-item";
|
||||||
|
|
||||||
|
export type SelectItemType = {
|
||||||
|
id: string;
|
||||||
|
label?: string;
|
||||||
|
avatarUrl?: string;
|
||||||
|
isDisabled?: boolean;
|
||||||
|
supportingText?: string;
|
||||||
|
icon?: FC | ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface CommonProps {
|
||||||
|
hint?: string;
|
||||||
|
label?: string;
|
||||||
|
tooltip?: string;
|
||||||
|
size?: "sm" | "md";
|
||||||
|
placeholder?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SelectProps extends Omit<AriaSelectProps<SelectItemType>, "children" | "items">, RefAttributes<HTMLDivElement>, CommonProps {
|
||||||
|
items?: SelectItemType[];
|
||||||
|
popoverClassName?: string;
|
||||||
|
placeholderIcon?: FC | ReactNode;
|
||||||
|
children: ReactNode | ((item: SelectItemType) => ReactNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SelectValueProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
size: "sm" | "md";
|
||||||
|
isFocused: boolean;
|
||||||
|
isDisabled: boolean;
|
||||||
|
placeholder?: string;
|
||||||
|
ref?: Ref<HTMLButtonElement>;
|
||||||
|
placeholderIcon?: FC | ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const sizes = {
|
||||||
|
sm: { root: "py-2 px-3", shortcut: "pr-2.5" },
|
||||||
|
md: { root: "py-2.5 px-3.5", shortcut: "pr-3" },
|
||||||
|
};
|
||||||
|
|
||||||
|
const SelectValue = ({ isOpen, isFocused, isDisabled, size, placeholder, placeholderIcon, ref }: SelectValueProps) => {
|
||||||
|
return (
|
||||||
|
<AriaButton
|
||||||
|
ref={ref}
|
||||||
|
className={cx(
|
||||||
|
"relative flex w-full cursor-pointer items-center rounded-lg bg-primary shadow-xs ring-1 ring-primary outline-hidden transition duration-100 ease-linear ring-inset",
|
||||||
|
(isFocused || isOpen) && "ring-2 ring-brand",
|
||||||
|
isDisabled && "cursor-not-allowed bg-disabled_subtle text-disabled",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<AriaSelectValue<SelectItemType>
|
||||||
|
className={cx(
|
||||||
|
"flex h-max w-full items-center justify-start gap-2 truncate text-left align-middle",
|
||||||
|
|
||||||
|
// Icon styles
|
||||||
|
"*:data-icon:size-5 *:data-icon:shrink-0 *:data-icon:text-fg-quaternary in-disabled:*:data-icon:text-fg-disabled",
|
||||||
|
|
||||||
|
sizes[size].root,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{(state) => {
|
||||||
|
const Icon = state.selectedItem?.icon || placeholderIcon;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{state.selectedItem?.avatarUrl ? (
|
||||||
|
<Avatar size="xs" src={state.selectedItem.avatarUrl} alt={state.selectedItem.label} />
|
||||||
|
) : isReactComponent(Icon) ? (
|
||||||
|
<Icon data-icon aria-hidden="true" />
|
||||||
|
) : isValidElement(Icon) ? (
|
||||||
|
Icon
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{state.selectedItem ? (
|
||||||
|
<section className="flex w-full gap-2 truncate">
|
||||||
|
<p className="truncate text-md font-medium text-primary">{state.selectedItem?.label}</p>
|
||||||
|
{state.selectedItem?.supportingText && <p className="text-md text-tertiary">{state.selectedItem?.supportingText}</p>}
|
||||||
|
</section>
|
||||||
|
) : (
|
||||||
|
<p className={cx("text-md text-placeholder", isDisabled && "text-disabled")}>{placeholder}</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<ChevronDown
|
||||||
|
aria-hidden="true"
|
||||||
|
className={cx("ml-auto shrink-0 text-fg-quaternary", size === "sm" ? "size-4 stroke-[2.5px]" : "size-5")}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</AriaSelectValue>
|
||||||
|
</AriaButton>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SelectContext = createContext<{ size: "sm" | "md" }>({ size: "sm" });
|
||||||
|
|
||||||
|
const Select = ({ placeholder = "Select", placeholderIcon, size = "sm", children, items, label, hint, tooltip, className, ...rest }: SelectProps) => {
|
||||||
|
return (
|
||||||
|
<SelectContext.Provider value={{ size }}>
|
||||||
|
<AriaSelect {...rest} className={(state) => cx("flex flex-col gap-1.5", typeof className === "function" ? className(state) : className)}>
|
||||||
|
{(state) => (
|
||||||
|
<>
|
||||||
|
{label && (
|
||||||
|
<Label isRequired={state.isRequired} tooltip={tooltip}>
|
||||||
|
{label}
|
||||||
|
</Label>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<SelectValue {...state} {...{ size, placeholder }} placeholderIcon={placeholderIcon} />
|
||||||
|
|
||||||
|
<Popover size={size} className={rest.popoverClassName}>
|
||||||
|
<AriaListBox items={items} className="size-full outline-hidden">
|
||||||
|
{children}
|
||||||
|
</AriaListBox>
|
||||||
|
</Popover>
|
||||||
|
|
||||||
|
{hint && <HintText isInvalid={state.isInvalid}>{hint}</HintText>}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</AriaSelect>
|
||||||
|
</SelectContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const _Select = Select as typeof Select & {
|
||||||
|
ComboBox: typeof ComboBox;
|
||||||
|
Item: typeof SelectItem;
|
||||||
|
};
|
||||||
|
_Select.ComboBox = ComboBox;
|
||||||
|
_Select.Item = SelectItem;
|
||||||
|
|
||||||
|
export { _Select as Select };
|
||||||
73
src/components/base/slider/slider.tsx
Normal file
73
src/components/base/slider/slider.tsx
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import type { SliderProps as AriaSliderProps } from "react-aria-components";
|
||||||
|
import {
|
||||||
|
Label as AriaLabel,
|
||||||
|
Slider as AriaSlider,
|
||||||
|
SliderOutput as AriaSliderOutput,
|
||||||
|
SliderThumb as AriaSliderThumb,
|
||||||
|
SliderTrack as AriaSliderTrack,
|
||||||
|
} from "react-aria-components";
|
||||||
|
import { cx, sortCx } from "@/utils/cx";
|
||||||
|
|
||||||
|
const styles = sortCx({
|
||||||
|
default: "hidden",
|
||||||
|
bottom: "absolute top-2 left-1/2 -translate-x-1/2 translate-y-full text-md font-medium text-primary",
|
||||||
|
"top-floating":
|
||||||
|
"absolute -top-2 left-1/2 -translate-x-1/2 -translate-y-full rounded-lg bg-primary px-3 py-2 text-xs font-semibold text-secondary shadow-lg ring-1 ring-secondary_alt",
|
||||||
|
});
|
||||||
|
|
||||||
|
interface SliderProps extends AriaSliderProps {
|
||||||
|
labelPosition?: keyof typeof styles;
|
||||||
|
labelFormatter?: (value: number) => string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Slider = ({ labelPosition = "default", minValue = 0, maxValue = 100, labelFormatter, formatOptions, ...rest }: SliderProps) => {
|
||||||
|
// Format thumb value as percentage by default.
|
||||||
|
const defaultFormatOptions: Intl.NumberFormatOptions = {
|
||||||
|
style: "percent",
|
||||||
|
maximumFractionDigits: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AriaSlider {...rest} {...{ minValue, maxValue }} formatOptions={formatOptions ?? defaultFormatOptions}>
|
||||||
|
<AriaLabel />
|
||||||
|
<AriaSliderTrack className="relative h-6 w-full">
|
||||||
|
{({ state: { values, getThumbValue, getThumbPercent, getFormattedValue } }) => {
|
||||||
|
const left = values.length === 1 ? 0 : getThumbPercent(0);
|
||||||
|
const width = values.length === 1 ? getThumbPercent(0) : getThumbPercent(1) - left;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<span className="absolute top-1/2 h-2 w-full -translate-y-1/2 rounded-full bg-quaternary" />
|
||||||
|
<span
|
||||||
|
className="absolute top-1/2 h-2 w-full -translate-y-1/2 rounded-full bg-brand-solid"
|
||||||
|
style={{
|
||||||
|
left: `${left * 100}%`,
|
||||||
|
width: `${width * 100}%`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{values.map((_, index) => {
|
||||||
|
return (
|
||||||
|
<AriaSliderThumb
|
||||||
|
key={index}
|
||||||
|
index={index}
|
||||||
|
className={({ isFocusVisible, isDragging }) =>
|
||||||
|
cx(
|
||||||
|
"top-1/2 box-border size-6 cursor-grab rounded-full bg-slider-handle-bg shadow-md ring-2 ring-slider-handle-border ring-inset",
|
||||||
|
isFocusVisible && "outline-2 outline-offset-2 outline-focus-ring",
|
||||||
|
isDragging && "cursor-grabbing",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<AriaSliderOutput className={cx("whitespace-nowrap", styles[labelPosition])}>
|
||||||
|
{labelFormatter ? labelFormatter(getThumbValue(index)) : getFormattedValue(getThumbValue(index) / 100)}
|
||||||
|
</AriaSliderOutput>
|
||||||
|
</AriaSliderThumb>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</AriaSliderTrack>
|
||||||
|
</AriaSlider>
|
||||||
|
);
|
||||||
|
};
|
||||||
43
src/components/base/tags/base-components/tag-checkbox.tsx
Normal file
43
src/components/base/tags/base-components/tag-checkbox.tsx
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { cx } from "@/utils/cx";
|
||||||
|
|
||||||
|
interface TagCheckboxProps {
|
||||||
|
size?: "sm" | "md" | "lg";
|
||||||
|
className?: string;
|
||||||
|
isFocused?: boolean;
|
||||||
|
isSelected?: boolean;
|
||||||
|
isDisabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TagCheckbox = ({ className, isFocused, isSelected, isDisabled, size = "sm" }: TagCheckboxProps) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cx(
|
||||||
|
"flex cursor-pointer appearance-none items-center justify-center rounded bg-primary ring-1 ring-primary ring-inset",
|
||||||
|
size === "sm" && "size-3.5",
|
||||||
|
size === "md" && "size-4",
|
||||||
|
size === "lg" && "size-4.5",
|
||||||
|
isSelected && "bg-brand-solid ring-bg-brand-solid",
|
||||||
|
isDisabled && "cursor-not-allowed bg-disabled_subtle ring-disabled",
|
||||||
|
isFocused && "outline-2 outline-offset-2 outline-focus-ring",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
viewBox="0 0 14 14"
|
||||||
|
fill="none"
|
||||||
|
className={cx(
|
||||||
|
"pointer-events-none absolute text-fg-white opacity-0 transition-inherit-all",
|
||||||
|
size === "sm" && "size-2.5",
|
||||||
|
size === "md" && "size-3",
|
||||||
|
size === "lg" && "size-3.5",
|
||||||
|
isSelected && "opacity-100",
|
||||||
|
isDisabled && "text-fg-disabled_subtle",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<path d="M11.6666 3.5L5.24992 9.91667L2.33325 7" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
TagCheckbox.displayName = "TagCheckbox";
|
||||||
32
src/components/base/tags/base-components/tag-close-x.tsx
Normal file
32
src/components/base/tags/base-components/tag-close-x.tsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import type { RefAttributes } from "react";
|
||||||
|
import { XClose } from "@untitledui/icons";
|
||||||
|
import { Button as AriaButton, type ButtonProps as AriaButtonProps } from "react-aria-components";
|
||||||
|
import { cx } from "@/utils/cx";
|
||||||
|
|
||||||
|
interface TagCloseXProps extends AriaButtonProps, RefAttributes<HTMLButtonElement> {
|
||||||
|
size?: "sm" | "md" | "lg";
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = {
|
||||||
|
sm: { root: "p-0.5", icon: "size-2.5" },
|
||||||
|
md: { root: "p-0.5", icon: "size-3" },
|
||||||
|
lg: { root: "p-0.75", icon: "size-3.5" },
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TagCloseX = ({ size = "md", className, ...otherProps }: TagCloseXProps) => {
|
||||||
|
return (
|
||||||
|
<AriaButton
|
||||||
|
slot="remove"
|
||||||
|
aria-label="Remove this tag"
|
||||||
|
className={cx(
|
||||||
|
"flex cursor-pointer rounded-[3px] text-fg-quaternary outline-transparent transition duration-100 ease-linear hover:bg-primary_hover hover:text-fg-quaternary_hover focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus-ring disabled:cursor-not-allowed",
|
||||||
|
styles[size].root,
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...otherProps}
|
||||||
|
>
|
||||||
|
<XClose className={cx("transition-inherit-all", styles[size].icon)} strokeWidth="3" />
|
||||||
|
</AriaButton>
|
||||||
|
);
|
||||||
|
};
|
||||||
161
src/components/base/tags/tags.tsx
Normal file
161
src/components/base/tags/tags.tsx
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
import { type PropsWithChildren, type RefAttributes, createContext, useContext } from "react";
|
||||||
|
import {
|
||||||
|
Tag as AriaTag,
|
||||||
|
TagGroup as AriaTagGroup,
|
||||||
|
type TagGroupProps as AriaTagGroupProps,
|
||||||
|
TagList as AriaTagList,
|
||||||
|
type TagProps as AriaTagProps,
|
||||||
|
} from "react-aria-components";
|
||||||
|
import { Avatar } from "@/components/base/avatar/avatar";
|
||||||
|
import { Dot } from "@/components/foundations/dot-icon";
|
||||||
|
import { cx } from "@/utils/cx";
|
||||||
|
import { TagCheckbox } from "./base-components/tag-checkbox";
|
||||||
|
import { TagCloseX } from "./base-components/tag-close-x";
|
||||||
|
|
||||||
|
export interface TagItem {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
count?: number;
|
||||||
|
avatarSrc?: string;
|
||||||
|
avatarContrastBorder?: boolean;
|
||||||
|
dot?: boolean;
|
||||||
|
dotClassName?: string;
|
||||||
|
isDisabled?: boolean;
|
||||||
|
onClose?: (id: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TagGroupContext = createContext<{
|
||||||
|
selectionMode: "none" | "single" | "multiple";
|
||||||
|
size: "sm" | "md" | "lg";
|
||||||
|
}>({
|
||||||
|
selectionMode: "none",
|
||||||
|
size: "sm",
|
||||||
|
});
|
||||||
|
|
||||||
|
interface TagGroupProps extends AriaTagGroupProps, RefAttributes<HTMLDivElement> {
|
||||||
|
label: string;
|
||||||
|
size?: "sm" | "md" | "lg";
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TagGroup = ({ label, selectionMode = "none", size = "sm", children, ...otherProps }: TagGroupProps) => {
|
||||||
|
return (
|
||||||
|
<TagGroupContext.Provider value={{ selectionMode, size }}>
|
||||||
|
<AriaTagGroup aria-label={label} selectionMode={selectionMode} disallowEmptySelection={selectionMode === "single"} {...otherProps}>
|
||||||
|
{children}
|
||||||
|
</AriaTagGroup>
|
||||||
|
</TagGroupContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TagList = AriaTagList;
|
||||||
|
|
||||||
|
const styles = {
|
||||||
|
sm: {
|
||||||
|
root: {
|
||||||
|
base: "px-2 py-0.75 text-xs font-medium",
|
||||||
|
withCheckbox: "pl-1.25",
|
||||||
|
withAvatar: "pl-1",
|
||||||
|
withDot: "pl-1.5",
|
||||||
|
withCount: "pr-1",
|
||||||
|
withClose: "pr-1",
|
||||||
|
},
|
||||||
|
content: "gap-1",
|
||||||
|
count: "px-1 text-xs font-medium",
|
||||||
|
},
|
||||||
|
md: {
|
||||||
|
root: {
|
||||||
|
base: "px-2.25 py-0.5 text-sm font-medium",
|
||||||
|
withCheckbox: "pl-1",
|
||||||
|
withAvatar: "pl-1.25",
|
||||||
|
withDot: "pl-1.75",
|
||||||
|
withCount: "pr-0.75",
|
||||||
|
withClose: "pr-1",
|
||||||
|
},
|
||||||
|
content: "gap-1.25",
|
||||||
|
count: "px-1.25 text-xs font-medium",
|
||||||
|
},
|
||||||
|
lg: {
|
||||||
|
root: {
|
||||||
|
base: "px-2.5 py-1 text-sm font-medium",
|
||||||
|
withCheckbox: "pl-1.25",
|
||||||
|
withAvatar: "pl-1.75",
|
||||||
|
withDot: "pl-2.25",
|
||||||
|
withCount: "pr-1",
|
||||||
|
withClose: "pr-1",
|
||||||
|
},
|
||||||
|
content: "gap-1.5",
|
||||||
|
count: "px-1.5 text-sm font-medium",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
interface TagProps extends AriaTagProps, RefAttributes<object>, Omit<TagItem, "label" | "id"> {}
|
||||||
|
|
||||||
|
export const Tag = ({
|
||||||
|
id,
|
||||||
|
avatarSrc,
|
||||||
|
avatarContrastBorder,
|
||||||
|
dot,
|
||||||
|
dotClassName,
|
||||||
|
isDisabled,
|
||||||
|
count,
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
onClose,
|
||||||
|
}: PropsWithChildren<TagProps>) => {
|
||||||
|
const context = useContext(TagGroupContext);
|
||||||
|
|
||||||
|
const leadingContent = avatarSrc ? (
|
||||||
|
<Avatar size="xxs" src={avatarSrc} alt="Avatar" contrastBorder={avatarContrastBorder} />
|
||||||
|
) : dot ? (
|
||||||
|
<Dot className={cx("text-fg-success-secondary", dotClassName)} size="sm" />
|
||||||
|
) : null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AriaTag
|
||||||
|
id={id}
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
textValue={typeof children === "string" ? children : undefined}
|
||||||
|
className={(state) =>
|
||||||
|
cx(
|
||||||
|
"flex cursor-default items-center gap-0.75 rounded-md bg-primary text-secondary ring-1 ring-primary ring-inset focus:outline-hidden focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus-ring",
|
||||||
|
styles[context.size].root.base,
|
||||||
|
|
||||||
|
// With avatar
|
||||||
|
avatarSrc && styles[context.size].root.withAvatar,
|
||||||
|
// With X button
|
||||||
|
(onClose || state.allowsRemoving) && styles[context.size].root.withClose,
|
||||||
|
// With dot
|
||||||
|
dot && styles[context.size].root.withDot,
|
||||||
|
// With count
|
||||||
|
typeof count === "number" && styles[context.size].root.withCount,
|
||||||
|
// With checkbox
|
||||||
|
context.selectionMode !== "none" && styles[context.size].root.withCheckbox,
|
||||||
|
// Disabled
|
||||||
|
isDisabled && "cursor-not-allowed",
|
||||||
|
|
||||||
|
typeof className === "function" ? className(state) : className,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{({ isSelected, isDisabled, allowsRemoving }) => (
|
||||||
|
<>
|
||||||
|
<div className={cx("flex items-center gap-1", styles[context.size].content)}>
|
||||||
|
{context.selectionMode !== "none" && <TagCheckbox size={context.size} isSelected={isSelected} isDisabled={isDisabled} />}
|
||||||
|
|
||||||
|
{leadingContent}
|
||||||
|
|
||||||
|
{children}
|
||||||
|
|
||||||
|
{typeof count === "number" && (
|
||||||
|
<span className={cx("flex items-center justify-center rounded-[3px] bg-tertiary text-center", styles[context.size].count)}>
|
||||||
|
{count}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{(onClose || allowsRemoving) && <TagCloseX size={context.size} onPress={() => id && onClose?.(id.toString())} />}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</AriaTag>
|
||||||
|
);
|
||||||
|
};
|
||||||
109
src/components/base/textarea/textarea.tsx
Normal file
109
src/components/base/textarea/textarea.tsx
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import type { ReactNode, Ref } from "react";
|
||||||
|
import React from "react";
|
||||||
|
import type { TextAreaProps as AriaTextAreaProps, TextFieldProps as AriaTextFieldProps } from "react-aria-components";
|
||||||
|
import { TextArea as AriaTextArea, TextField as AriaTextField } from "react-aria-components";
|
||||||
|
import { HintText } from "@/components/base/input/hint-text";
|
||||||
|
import { Label } from "@/components/base/input/label";
|
||||||
|
import { cx } from "@/utils/cx";
|
||||||
|
|
||||||
|
// Creates a data URL for an SVG resize handle with a given color.
|
||||||
|
const getResizeHandleBg = (color: string) => {
|
||||||
|
return `url(data:image/svg+xml;base64,${btoa(`<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M10 2L2 10" stroke="${color}" stroke-linecap="round"/><path d="M11 7L7 11" stroke="${color}" stroke-linecap="round"/></svg>`)})`;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface TextAreaBaseProps extends AriaTextAreaProps {
|
||||||
|
ref?: Ref<HTMLTextAreaElement>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TextAreaBase = ({ className, ...props }: TextAreaBaseProps) => {
|
||||||
|
return (
|
||||||
|
<AriaTextArea
|
||||||
|
{...props}
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
"--resize-handle-bg": getResizeHandleBg("#D5D7DA"),
|
||||||
|
"--resize-handle-bg-dark": getResizeHandleBg("#373A41"),
|
||||||
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
|
className={(state) =>
|
||||||
|
cx(
|
||||||
|
"w-full scroll-py-3 rounded-lg bg-primary px-3.5 py-3 text-md text-primary shadow-xs ring-1 ring-primary transition duration-100 ease-linear ring-inset placeholder:text-placeholder autofill:rounded-lg autofill:text-primary focus:outline-hidden",
|
||||||
|
|
||||||
|
// Resize handle
|
||||||
|
"[&::-webkit-resizer]:bg-(image:--resize-handle-bg) [&::-webkit-resizer]:bg-contain dark:[&::-webkit-resizer]:bg-(image:--resize-handle-bg-dark)",
|
||||||
|
|
||||||
|
state.isFocused && !state.isDisabled && "ring-2 ring-brand",
|
||||||
|
state.isDisabled && "cursor-not-allowed bg-disabled_subtle text-disabled ring-disabled",
|
||||||
|
state.isInvalid && "ring-error_subtle",
|
||||||
|
state.isInvalid && state.isFocused && "ring-2 ring-error",
|
||||||
|
|
||||||
|
typeof className === "function" ? className(state) : className,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
TextAreaBase.displayName = "TextAreaBase";
|
||||||
|
|
||||||
|
interface TextFieldProps extends AriaTextFieldProps {
|
||||||
|
/** Label text for the textarea */
|
||||||
|
label?: string;
|
||||||
|
/** Helper text displayed below the textarea */
|
||||||
|
hint?: ReactNode;
|
||||||
|
/** Tooltip message displayed after the label. */
|
||||||
|
tooltip?: string;
|
||||||
|
/** Class name for the textarea wrapper */
|
||||||
|
textAreaClassName?: TextAreaBaseProps["className"];
|
||||||
|
/** Ref for the textarea wrapper */
|
||||||
|
ref?: Ref<HTMLDivElement>;
|
||||||
|
/** Ref for the textarea */
|
||||||
|
textAreaRef?: TextAreaBaseProps["ref"];
|
||||||
|
/** Whether to hide required indicator from label. */
|
||||||
|
hideRequiredIndicator?: boolean;
|
||||||
|
/** Placeholder text. */
|
||||||
|
placeholder?: string;
|
||||||
|
/** Visible height of textarea in rows . */
|
||||||
|
rows?: number;
|
||||||
|
/** Visible width of textarea in columns. */
|
||||||
|
cols?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TextArea = ({
|
||||||
|
label,
|
||||||
|
hint,
|
||||||
|
tooltip,
|
||||||
|
textAreaRef,
|
||||||
|
hideRequiredIndicator,
|
||||||
|
textAreaClassName,
|
||||||
|
placeholder,
|
||||||
|
className,
|
||||||
|
rows,
|
||||||
|
cols,
|
||||||
|
...props
|
||||||
|
}: TextFieldProps) => {
|
||||||
|
return (
|
||||||
|
<AriaTextField
|
||||||
|
{...props}
|
||||||
|
className={(state) =>
|
||||||
|
cx("group flex h-max w-full flex-col items-start justify-start gap-1.5", typeof className === "function" ? className(state) : className)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{({ isInvalid, isRequired }) => (
|
||||||
|
<>
|
||||||
|
{label && (
|
||||||
|
<Label isRequired={hideRequiredIndicator ? !hideRequiredIndicator : isRequired} tooltip={tooltip}>
|
||||||
|
{label}
|
||||||
|
</Label>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<TextAreaBase placeholder={placeholder} className={textAreaClassName} ref={textAreaRef} rows={rows} cols={cols} />
|
||||||
|
|
||||||
|
{hint && <HintText isInvalid={isInvalid}>{hint}</HintText>}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</AriaTextField>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
TextArea.displayName = "TextArea";
|
||||||
138
src/components/base/toggle/toggle.tsx
Normal file
138
src/components/base/toggle/toggle.tsx
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
import type { ReactNode } from "react";
|
||||||
|
import type { SwitchProps as AriaSwitchProps } from "react-aria-components";
|
||||||
|
import { Switch as AriaSwitch } from "react-aria-components";
|
||||||
|
import { cx } from "@/utils/cx";
|
||||||
|
|
||||||
|
interface ToggleBaseProps {
|
||||||
|
size?: "sm" | "md";
|
||||||
|
slim?: boolean;
|
||||||
|
className?: string;
|
||||||
|
isHovered?: boolean;
|
||||||
|
isFocusVisible?: boolean;
|
||||||
|
isSelected?: boolean;
|
||||||
|
isDisabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ToggleBase = ({ className, isHovered, isDisabled, isFocusVisible, isSelected, slim, size = "sm" }: ToggleBaseProps) => {
|
||||||
|
const styles = {
|
||||||
|
default: {
|
||||||
|
sm: {
|
||||||
|
root: "h-5 w-9 p-0.5",
|
||||||
|
switch: cx("size-4", isSelected && "translate-x-4"),
|
||||||
|
},
|
||||||
|
md: {
|
||||||
|
root: "h-6 w-11 p-0.5",
|
||||||
|
switch: cx("size-5", isSelected && "translate-x-5"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
slim: {
|
||||||
|
sm: {
|
||||||
|
root: "h-4 w-8",
|
||||||
|
switch: cx("size-4", isSelected && "translate-x-4"),
|
||||||
|
},
|
||||||
|
md: {
|
||||||
|
root: "h-5 w-10",
|
||||||
|
switch: cx("size-5", isSelected && "translate-x-5"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const classes = slim ? styles.slim[size] : styles.default[size];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cx(
|
||||||
|
"cursor-pointer rounded-full bg-tertiary outline-focus-ring transition duration-150 ease-linear",
|
||||||
|
isSelected && "bg-brand-solid",
|
||||||
|
isSelected && isHovered && "bg-brand-solid_hover",
|
||||||
|
isDisabled && "cursor-not-allowed bg-disabled",
|
||||||
|
isFocusVisible && "outline-2 outline-offset-2",
|
||||||
|
|
||||||
|
slim && "ring-1 ring-secondary ring-inset",
|
||||||
|
slim && isSelected && "ring-transparent",
|
||||||
|
classes.root,
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
transition: "transform 0.15s ease-in-out, translate 0.15s ease-in-out, border-color 0.1s linear, background-color 0.1s linear",
|
||||||
|
}}
|
||||||
|
className={cx(
|
||||||
|
"rounded-full bg-fg-white shadow-sm",
|
||||||
|
isDisabled && "bg-toggle-button-fg_disabled",
|
||||||
|
|
||||||
|
slim && "shadow-xs",
|
||||||
|
slim && "border border-toggle-border",
|
||||||
|
slim && isSelected && "border-toggle-slim-border_pressed",
|
||||||
|
slim && isSelected && isHovered && "border-toggle-slim-border_pressed-hover",
|
||||||
|
|
||||||
|
classes.switch,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ToggleProps extends AriaSwitchProps {
|
||||||
|
size?: "sm" | "md";
|
||||||
|
label?: string;
|
||||||
|
hint?: ReactNode;
|
||||||
|
slim?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Toggle = ({ label, hint, className, size = "sm", slim, ...ariaSwitchProps }: ToggleProps) => {
|
||||||
|
const sizes = {
|
||||||
|
sm: {
|
||||||
|
root: "gap-2",
|
||||||
|
textWrapper: "",
|
||||||
|
label: "text-sm font-medium",
|
||||||
|
hint: "text-sm",
|
||||||
|
},
|
||||||
|
md: {
|
||||||
|
root: "gap-3",
|
||||||
|
textWrapper: "gap-0.5",
|
||||||
|
label: "text-md font-medium",
|
||||||
|
hint: "text-md",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AriaSwitch
|
||||||
|
{...ariaSwitchProps}
|
||||||
|
className={(renderProps) =>
|
||||||
|
cx(
|
||||||
|
"flex w-max items-start",
|
||||||
|
renderProps.isDisabled && "cursor-not-allowed",
|
||||||
|
sizes[size].root,
|
||||||
|
typeof className === "function" ? className(renderProps) : className,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{({ isSelected, isDisabled, isFocusVisible, isHovered }) => (
|
||||||
|
<>
|
||||||
|
<ToggleBase
|
||||||
|
slim={slim}
|
||||||
|
size={size}
|
||||||
|
isHovered={isHovered}
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
isFocusVisible={isFocusVisible}
|
||||||
|
isSelected={isSelected}
|
||||||
|
className={slim ? "mt-0.5" : ""}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{(label || hint) && (
|
||||||
|
<div className={cx("flex flex-col", sizes[size].textWrapper)}>
|
||||||
|
{label && <p className={cx("text-secondary select-none", sizes[size].label)}>{label}</p>}
|
||||||
|
{hint && (
|
||||||
|
<span className={cx("text-tertiary", sizes[size].hint)} onClick={(event) => event.stopPropagation()}>
|
||||||
|
{hint}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</AriaSwitch>
|
||||||
|
);
|
||||||
|
};
|
||||||
107
src/components/base/tooltip/tooltip.tsx
Normal file
107
src/components/base/tooltip/tooltip.tsx
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
import type { ReactNode } from "react";
|
||||||
|
import type {
|
||||||
|
ButtonProps as AriaButtonProps,
|
||||||
|
TooltipProps as AriaTooltipProps,
|
||||||
|
TooltipTriggerComponentProps as AriaTooltipTriggerComponentProps,
|
||||||
|
} from "react-aria-components";
|
||||||
|
import { Button as AriaButton, OverlayArrow as AriaOverlayArrow, Tooltip as AriaTooltip, TooltipTrigger as AriaTooltipTrigger } from "react-aria-components";
|
||||||
|
import { cx } from "@/utils/cx";
|
||||||
|
|
||||||
|
interface TooltipProps extends AriaTooltipTriggerComponentProps, Omit<AriaTooltipProps, "children"> {
|
||||||
|
/**
|
||||||
|
* The title of the tooltip.
|
||||||
|
*/
|
||||||
|
title: ReactNode;
|
||||||
|
/**
|
||||||
|
* The description of the tooltip.
|
||||||
|
*/
|
||||||
|
description?: ReactNode;
|
||||||
|
/**
|
||||||
|
* Whether to show the arrow on the tooltip.
|
||||||
|
*
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
arrow?: boolean;
|
||||||
|
/**
|
||||||
|
* Delay in milliseconds before the tooltip is shown.
|
||||||
|
*
|
||||||
|
* @default 300
|
||||||
|
*/
|
||||||
|
delay?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Tooltip = ({
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
children,
|
||||||
|
arrow = false,
|
||||||
|
delay = 300,
|
||||||
|
closeDelay = 0,
|
||||||
|
trigger,
|
||||||
|
isDisabled,
|
||||||
|
isOpen,
|
||||||
|
defaultOpen,
|
||||||
|
offset = 6,
|
||||||
|
crossOffset,
|
||||||
|
placement = "top",
|
||||||
|
onOpenChange,
|
||||||
|
...tooltipProps
|
||||||
|
}: TooltipProps) => {
|
||||||
|
const isTopOrBottomLeft = ["top left", "top end", "bottom left", "bottom end"].includes(placement);
|
||||||
|
const isTopOrBottomRight = ["top right", "top start", "bottom right", "bottom start"].includes(placement);
|
||||||
|
// Set negative cross offset for left and right placement to visually balance the tooltip.
|
||||||
|
const calculatedCrossOffset = isTopOrBottomLeft ? -12 : isTopOrBottomRight ? 12 : 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AriaTooltipTrigger {...{ trigger, delay, closeDelay, isDisabled, isOpen, defaultOpen, onOpenChange }}>
|
||||||
|
{children}
|
||||||
|
|
||||||
|
<AriaTooltip
|
||||||
|
{...tooltipProps}
|
||||||
|
offset={offset}
|
||||||
|
placement={placement}
|
||||||
|
crossOffset={crossOffset ?? calculatedCrossOffset}
|
||||||
|
className={({ isEntering, isExiting }) => cx(isEntering && "ease-out animate-in", isExiting && "ease-in animate-out")}
|
||||||
|
>
|
||||||
|
{({ isEntering, isExiting }) => (
|
||||||
|
<div
|
||||||
|
className={cx(
|
||||||
|
"z-50 flex max-w-xs origin-(--trigger-anchor-point) flex-col items-start gap-1 rounded-lg bg-primary-solid px-3 shadow-lg will-change-transform",
|
||||||
|
description ? "py-3" : "py-2",
|
||||||
|
|
||||||
|
isEntering &&
|
||||||
|
"ease-out animate-in fade-in zoom-in-95 in-placement-left:slide-in-from-right-0.5 in-placement-right:slide-in-from-left-0.5 in-placement-top:slide-in-from-bottom-0.5 in-placement-bottom:slide-in-from-top-0.5",
|
||||||
|
isExiting &&
|
||||||
|
"ease-in animate-out fade-out zoom-out-95 in-placement-left:slide-out-to-right-0.5 in-placement-right:slide-out-to-left-0.5 in-placement-top:slide-out-to-bottom-0.5 in-placement-bottom:slide-out-to-top-0.5",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span className="text-xs font-semibold text-white">{title}</span>
|
||||||
|
|
||||||
|
{description && <span className="text-xs font-medium text-tooltip-supporting-text">{description}</span>}
|
||||||
|
|
||||||
|
{arrow && (
|
||||||
|
<AriaOverlayArrow>
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 100 100"
|
||||||
|
className="size-2.5 fill-bg-primary-solid in-placement-left:-rotate-90 in-placement-right:rotate-90 in-placement-top:rotate-0 in-placement-bottom:rotate-180"
|
||||||
|
>
|
||||||
|
<path d="M0,0 L35.858,35.858 Q50,50 64.142,35.858 L100,0 Z" />
|
||||||
|
</svg>
|
||||||
|
</AriaOverlayArrow>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</AriaTooltip>
|
||||||
|
</AriaTooltipTrigger>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface TooltipTriggerProps extends AriaButtonProps {}
|
||||||
|
|
||||||
|
export const TooltipTrigger = ({ children, className, ...buttonProps }: TooltipTriggerProps) => {
|
||||||
|
return (
|
||||||
|
<AriaButton {...buttonProps} className={(values) => cx("h-max w-max outline-hidden", typeof className === "function" ? className(values) : className)}>
|
||||||
|
{children}
|
||||||
|
</AriaButton>
|
||||||
|
);
|
||||||
|
};
|
||||||
22
src/components/foundations/dot-icon.tsx
Normal file
22
src/components/foundations/dot-icon.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import type { HTMLAttributes } from "react";
|
||||||
|
|
||||||
|
const sizes = {
|
||||||
|
sm: {
|
||||||
|
wh: 8,
|
||||||
|
c: 4,
|
||||||
|
r: 2.5,
|
||||||
|
},
|
||||||
|
md: {
|
||||||
|
wh: 10,
|
||||||
|
c: 5,
|
||||||
|
r: 4,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Dot = ({ size = "md", ...props }: HTMLAttributes<HTMLOrSVGElement> & { size?: "sm" | "md" }) => {
|
||||||
|
return (
|
||||||
|
<svg width={sizes[size].wh} height={sizes[size].wh} viewBox={`0 0 ${sizes[size].wh} ${sizes[size].wh}`} fill="none" {...props}>
|
||||||
|
<circle cx={sizes[size].c} cy={sizes[size].c} r={sizes[size].r} fill="currentColor" stroke="currentColor" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
||||||
154
src/components/foundations/featured-icon/featured-icon.tsx
Normal file
154
src/components/foundations/featured-icon/featured-icon.tsx
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
import type { FC, ReactNode, Ref } from "react";
|
||||||
|
import { isValidElement } from "react";
|
||||||
|
import { cx, sortCx } from "@/utils/cx";
|
||||||
|
import { isReactComponent } from "@/utils/is-react-component";
|
||||||
|
|
||||||
|
const iconsSizes = {
|
||||||
|
sm: "*:data-icon:size-4",
|
||||||
|
md: "*:data-icon:size-5",
|
||||||
|
lg: "*:data-icon:size-6",
|
||||||
|
xl: "*:data-icon:size-7",
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = sortCx({
|
||||||
|
light: {
|
||||||
|
base: "rounded-full",
|
||||||
|
sizes: {
|
||||||
|
sm: "size-8",
|
||||||
|
md: "size-10",
|
||||||
|
lg: "size-12",
|
||||||
|
xl: "size-14",
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
brand: "bg-brand-secondary text-featured-icon-light-fg-brand",
|
||||||
|
gray: "bg-tertiary text-featured-icon-light-fg-gray",
|
||||||
|
error: "bg-error-secondary text-featured-icon-light-fg-error",
|
||||||
|
warning: "bg-warning-secondary text-featured-icon-light-fg-warning",
|
||||||
|
success: "bg-success-secondary text-featured-icon-light-fg-success",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
gradient: {
|
||||||
|
base: "rounded-full text-fg-white before:absolute before:inset-0 before:size-full before:rounded-full before:border before:mask-b-from-0% after:absolute after:block after:rounded-full",
|
||||||
|
sizes: {
|
||||||
|
sm: "size-8 after:size-6 *:data-icon:size-4",
|
||||||
|
md: "size-10 after:size-7 *:data-icon:size-4",
|
||||||
|
lg: "size-12 after:size-8 *:data-icon:size-5",
|
||||||
|
xl: "size-14 after:size-10 *:data-icon:size-5",
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
brand: "before:border-utility-brand-200 before:bg-utility-brand-50 after:bg-brand-solid",
|
||||||
|
gray: "before:border-utility-gray-200 before:bg-utility-gray-50 after:bg-secondary-solid",
|
||||||
|
error: "before:border-utility-error-200 before:bg-utility-error-50 after:bg-error-solid",
|
||||||
|
warning: "before:border-utility-warning-200 before:bg-utility-warning-50 after:bg-warning-solid",
|
||||||
|
success: "before:border-utility-success-200 before:bg-utility-success-50 after:bg-success-solid",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
dark: {
|
||||||
|
base: "text-fg-white shadow-xs-skeumorphic before:absolute before:inset-px before:border before:border-white/12 before:mask-b-from-0%",
|
||||||
|
sizes: {
|
||||||
|
sm: "size-8 rounded-md before:rounded-[5px]",
|
||||||
|
md: "size-10 rounded-lg before:rounded-[7px]",
|
||||||
|
lg: "size-12 rounded-[10px] before:rounded-[9px]",
|
||||||
|
xl: "size-14 rounded-xl before:rounded-[11px]",
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
brand: "bg-brand-solid before:border-utility-brand-200/12",
|
||||||
|
gray: "bg-secondary-solid before:border-utility-gray-200/12",
|
||||||
|
error: "bg-error-solid before:border-utility-error-200/12",
|
||||||
|
warning: "bg-warning-solid before:border-utility-warning-200/12",
|
||||||
|
success: "bg-success-solid before:border-utility-success-200/12",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
modern: {
|
||||||
|
base: "bg-primary shadow-xs-skeumorphic ring-1 ring-inset",
|
||||||
|
sizes: {
|
||||||
|
sm: "size-8 rounded-md",
|
||||||
|
md: "size-10 rounded-lg",
|
||||||
|
lg: "size-12 rounded-[10px]",
|
||||||
|
xl: "size-14 rounded-xl",
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
brand: "",
|
||||||
|
gray: "text-fg-secondary ring-primary",
|
||||||
|
error: "",
|
||||||
|
warning: "",
|
||||||
|
success: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"modern-neue": {
|
||||||
|
base: [
|
||||||
|
"bg-primary_alt ring-1 ring-inset before:absolute before:inset-1",
|
||||||
|
// Shadow
|
||||||
|
"before:shadow-[0px_1px_2px_0px_rgba(0,0,0,0.1),0px_3px_3px_0px_rgba(0,0,0,0.09),1px_8px_5px_0px_rgba(0,0,0,0.05),2px_21px_6px_0px_rgba(0,0,0,0),0px_0px_0px_1px_rgba(0,0,0,0.08),1px_13px_5px_0px_rgba(0,0,0,0.01),0px_-2px_2px_0px_rgba(0,0,0,0.13)_inset] before:ring-1 before:ring-secondary_alt",
|
||||||
|
].join(" "),
|
||||||
|
sizes: {
|
||||||
|
sm: "size-8 rounded-[8px] before:rounded-[4px]",
|
||||||
|
md: "size-10 rounded-[10px] before:rounded-[6px]",
|
||||||
|
lg: "size-12 rounded-[12px] before:rounded-[8px]",
|
||||||
|
xl: "size-14 rounded-[14px] before:rounded-[10px]",
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
brand: "",
|
||||||
|
gray: "text-fg-secondary ring-primary",
|
||||||
|
error: "",
|
||||||
|
warning: "",
|
||||||
|
success: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
outline: {
|
||||||
|
base: "before:absolute before:rounded-full before:border-2 after:absolute after:rounded-full after:border-2",
|
||||||
|
sizes: {
|
||||||
|
sm: "size-4 before:size-6 after:size-8.5",
|
||||||
|
md: "size-5 before:size-7 after:size-9.5",
|
||||||
|
lg: "size-6 before:size-8 after:size-10.5",
|
||||||
|
xl: "size-7 before:size-9 after:size-11.5",
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
brand: "text-fg-brand-primary before:border-fg-brand-primary/30 after:border-fg-brand-primary/10",
|
||||||
|
gray: "text-fg-tertiary before:border-fg-tertiary/30 after:border-fg-tertiary/10",
|
||||||
|
error: "text-fg-error-primary before:border-fg-error-primary/30 after:border-fg-error-primary/10",
|
||||||
|
warning: "text-fg-warning-primary before:border-fg-warning-primary/30 after:border-fg-warning-primary/10",
|
||||||
|
success: "text-fg-success-primary before:border-fg-success-primary/30 after:border-fg-success-primary/10",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
interface FeaturedIconProps {
|
||||||
|
ref?: Ref<HTMLDivElement>;
|
||||||
|
children?: ReactNode;
|
||||||
|
className?: string;
|
||||||
|
icon?: FC<{ className?: string }> | ReactNode;
|
||||||
|
size?: "sm" | "md" | "lg" | "xl";
|
||||||
|
color: "brand" | "gray" | "success" | "warning" | "error";
|
||||||
|
theme?: "light" | "gradient" | "dark" | "outline" | "modern" | "modern-neue";
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FeaturedIcon = (props: FeaturedIconProps) => {
|
||||||
|
const { size = "sm", theme: variant = "light", color = "brand", icon: Icon, ...otherProps } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
{...otherProps}
|
||||||
|
data-featured-icon
|
||||||
|
className={cx(
|
||||||
|
"relative flex shrink-0 items-center justify-center",
|
||||||
|
|
||||||
|
iconsSizes[size],
|
||||||
|
styles[variant].base,
|
||||||
|
styles[variant].sizes[size],
|
||||||
|
styles[variant].colors[color],
|
||||||
|
|
||||||
|
props.className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{isReactComponent(Icon) && <Icon data-icon className="z-1" />}
|
||||||
|
{isValidElement(Icon) && <div className="z-1">{Icon}</div>}
|
||||||
|
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
170
src/components/foundations/logo/untitledui-logo-minimal.tsx
Normal file
170
src/components/foundations/logo/untitledui-logo-minimal.tsx
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
import type { SVGProps } from "react";
|
||||||
|
import { useId } from "react";
|
||||||
|
import { cx } from "@/utils/cx";
|
||||||
|
|
||||||
|
export const UntitledLogoMinimal = (props: SVGProps<SVGSVGElement>) => {
|
||||||
|
const id = useId();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<svg viewBox="0 0 38 38" fill="none" {...props} className={cx("size-8 origin-center scale-[1.2]", props.className)}>
|
||||||
|
<g filter={`url(#filter0-${id}`}>
|
||||||
|
<g clipPath={`url(#clip0-${id}`}>
|
||||||
|
<path
|
||||||
|
d="M3 14.8C3 10.3196 3 8.07937 3.87195 6.36808C4.63893 4.86278 5.86278 3.63893 7.36808 2.87195C9.07937 2 11.3196 2 15.8 2H22.2C26.6804 2 28.9206 2 30.6319 2.87195C32.1372 3.63893 33.3611 4.86278 34.1281 6.36808C35 8.07937 35 10.3196 35 14.8V21.2C35 25.6804 35 27.9206 34.1281 29.6319C33.3611 31.1372 32.1372 32.3611 30.6319 33.1281C28.9206 34 26.6804 34 22.2 34H15.8C11.3196 34 9.07937 34 7.36808 33.1281C5.86278 32.3611 4.63893 31.1372 3.87195 29.6319C3 27.9206 3 25.6804 3 21.2V14.8Z"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M3 14.8C3 10.3196 3 8.07937 3.87195 6.36808C4.63893 4.86278 5.86278 3.63893 7.36808 2.87195C9.07937 2 11.3196 2 15.8 2H22.2C26.6804 2 28.9206 2 30.6319 2.87195C32.1372 3.63893 33.3611 4.86278 34.1281 6.36808C35 8.07937 35 10.3196 35 14.8V21.2C35 25.6804 35 27.9206 34.1281 29.6319C33.3611 31.1372 32.1372 32.3611 30.6319 33.1281C28.9206 34 26.6804 34 22.2 34H15.8C11.3196 34 9.07937 34 7.36808 33.1281C5.86278 32.3611 4.63893 31.1372 3.87195 29.6319C3 27.9206 3 25.6804 3 21.2V14.8Z"
|
||||||
|
fill={`url(#paint0_linear-${id}`}
|
||||||
|
fillOpacity="0.2"
|
||||||
|
/>
|
||||||
|
<g opacity="0.14" clipPath={`url(#clip1-${id}`}>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M18.9612 2H19.0388V3.96123C20.8929 3.96625 22.6625 4.33069 24.2816 4.98855V2H24.3592V5.02038C25.7339 5.58859 26.9986 6.36882 28.1126 7.32031H29.602V2H29.6796V7.32031H35V7.39798H29.6796V8.88728C30.6311 10.0013 31.4113 11.266 31.9796 12.6406H35V12.7183H32.0114C32.6693 14.3373 33.0337 16.1069 33.0388 17.9609H35V18.0386H33.0388C33.0338 19.8927 32.6694 21.6622 32.0116 23.2812H35V23.3589H31.9798C31.4115 24.7337 30.6312 25.9986 29.6796 27.1128V28.6016H35V28.6792H29.6796V34H29.602V28.6792H28.1132C26.999 29.6309 25.7341 30.4113 24.3592 30.9797V34H24.2816V31.0115C22.6625 31.6693 20.8929 32.0338 19.0388 32.0388V34H18.9612V32.0388C17.1071 32.0338 15.3375 31.6693 13.7184 31.0115V34H13.6408V30.9797C12.2659 30.4113 11.001 29.6309 9.88678 28.6792H8.39804V34H8.32037V28.6792H3V28.6016H8.32037V27.1128C7.36877 25.9986 6.58847 24.7337 6.02023 23.3589H3V23.2812H5.9884C5.3306 21.6622 4.96621 19.8927 4.96122 18.0386H3V17.9609H4.96122C4.96627 16.1069 5.33073 14.3373 5.9886 12.7183H3V12.6406H6.02044C6.58866 11.266 7.36889 10.0013 8.32037 8.88728V7.39798H3V7.32031H8.32037V2H8.39804V7.32031H9.88736C11.0014 6.36882 12.2661 5.58859 13.6408 5.02038V2H13.7184V4.98855C15.3375 4.33069 17.1071 3.96626 18.9612 3.96123V2ZM18.9612 4.0389C17.1062 4.04396 15.3364 4.41075 13.7184 5.07245V7.32031H18.9612V4.0389ZM13.6408 5.10449C12.3137 5.65662 11.0902 6.40763 10.0074 7.32031H13.6408V5.10449ZM9.79719 7.39798H8.39804V8.79711C8.8311 8.29865 9.29872 7.83103 9.79719 7.39798ZM8.39804 8.91598C8.86452 8.37206 9.37213 7.86446 9.91606 7.39798H13.6408V12.6406H8.39804V8.91598ZM8.32037 9.00733C7.4077 10.0901 6.65669 11.3136 6.10454 12.6406H8.32037V9.00733ZM6.0725 12.7183C5.41078 14.3362 5.04397 16.106 5.03889 17.9609H8.32037V12.7183H6.0725ZM5.03889 18.0386C5.04391 19.8935 5.41065 21.6633 6.0723 23.2812H8.32037V18.0386H5.03889ZM6.10434 23.3589C6.6565 24.6861 7.40759 25.9098 8.32037 26.9927V23.3589H6.10434ZM8.39804 27.2029V28.6016H9.79662C9.29837 28.1686 8.83093 27.7012 8.39804 27.2029ZM9.91548 28.6016C9.37178 28.1352 8.86436 27.6278 8.39804 27.0841V23.3589H13.6408V28.6016H9.91548ZM10.0068 28.6792C11.0898 29.5921 12.3135 30.3433 13.6408 30.8955V28.6792H10.0068ZM13.7184 30.9276C15.3364 31.5893 17.1062 31.9561 18.9612 31.9611V28.6792H13.7184V30.9276ZM19.0388 31.9611C20.8937 31.9561 22.6636 31.5893 24.2816 30.9276V28.6792H19.0388V31.9611ZM24.3592 30.8955C25.6865 30.3433 26.9102 29.5921 27.9932 28.6792H24.3592V30.8955ZM28.2034 28.6016H29.602V27.2029C29.1691 27.7012 28.7016 28.1686 28.2034 28.6016ZM29.602 27.0841C29.1356 27.6278 28.6282 28.1352 28.0845 28.6016H24.3592V23.3589H29.602V27.0841ZM29.6796 26.9927C30.5924 25.9098 31.3435 24.6861 31.8957 23.3589H29.6796V26.9927ZM31.9277 23.2812C32.5894 21.6633 32.9561 19.8935 32.9611 18.0386H29.6796V23.2812H31.9277ZM32.9611 17.9609C32.956 16.1061 32.5892 14.3362 31.9275 12.7183H29.6796V17.9609H32.9611ZM31.8955 12.6406C31.3433 11.3136 30.5923 10.0901 29.6796 9.00733V12.6406H31.8955ZM29.602 8.79711V7.39798H28.2028C28.7013 7.83103 29.1689 8.29865 29.602 8.79711ZM28.0839 7.39798C28.6279 7.86446 29.1355 8.37206 29.602 8.91598V12.6406H24.3592V7.39798H28.0839ZM27.9926 7.32031C26.9098 6.40763 25.6863 5.65662 24.3592 5.10449V7.32031H27.9926ZM24.2816 5.07245C22.6636 4.41074 20.8937 4.04395 19.0388 4.0389V7.32031H24.2816V5.07245ZM13.7184 7.39798H18.9612V12.6406H13.7184V7.39798ZM24.2816 7.39798H19.0388V12.6406H24.2816V7.39798ZM13.6408 23.2812H8.39804V18.0386H13.6408V23.2812ZM13.7184 23.3589V28.6016H18.9612V23.3589H13.7184ZM18.9612 23.2812H13.7184V18.0386H18.9612V23.2812ZM19.0388 23.3589V28.6016H24.2816V23.3589H19.0388ZM24.2816 23.2812H19.0388V18.0386H24.2816V23.2812ZM29.602 23.2812H24.3592V18.0386H29.602V23.2812ZM13.7184 12.7183H18.9612V17.9609H13.7184V12.7183ZM8.39804 12.7183L13.6408 12.7183V17.9609H8.39804V12.7183ZM24.2816 12.7183H19.0388V17.9609H24.2816V12.7183ZM24.3592 17.9609V12.7183L29.602 12.7183V17.9609H24.3592Z"
|
||||||
|
fill="#0A0D12"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
<g filter={`url(#filter1_dd-${id}`}>
|
||||||
|
<rect x="11" y="10" width="16" height="16" rx="8" fill={`url(#paint1_linear-${id}`} />
|
||||||
|
<rect x="11" y="10" width="16" height="16" rx="8" fill={`url(#paint2_radial-${id}`} fillOpacity="0.08" />
|
||||||
|
<rect x="11" y="10" width="16" height="16" rx="8" fill={`url(#paint3_radial-${id}`} fillOpacity="0.18" />
|
||||||
|
<rect x="11" y="10" width="16" height="16" rx="8" fill={`url(#paint4_radial-${id}`} fillOpacity="0.05" />
|
||||||
|
<path
|
||||||
|
d="M23.8 14.0414C23.8 15.3898 21.651 14.5297 19 14.5297C16.349 14.5297 14.2 15.3898 14.2 14.0414C14.2 12.693 16.349 11.6 19 11.6C21.651 11.6 23.8 12.693 23.8 14.0414Z"
|
||||||
|
fill={`url(#paint5_linear-${id}`}
|
||||||
|
fillOpacity="0.4"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<path
|
||||||
|
d="M3.1 14.8C3.1 12.5581 3.10008 10.8828 3.20866 9.55376C3.31715 8.22593 3.53345 7.25268 3.96105 6.41348C4.71845 4.92699 5.92699 3.71845 7.41348 2.96105C8.25268 2.53345 9.22593 2.31715 10.5538 2.20866C11.8828 2.10008 13.5581 2.1 15.8 2.1H22.2C24.4419 2.1 26.1172 2.10008 27.4462 2.20866C28.7741 2.31715 29.7473 2.53345 30.5865 2.96105C32.073 3.71845 33.2816 4.92699 34.039 6.41348C34.4665 7.25268 34.6828 8.22593 34.7913 9.55376C34.8999 10.8828 34.9 12.5581 34.9 14.8V21.2C34.9 23.4419 34.8999 25.1172 34.7913 26.4462C34.6828 27.7741 34.4665 28.7473 34.039 29.5865C33.2816 31.073 32.073 32.2816 30.5865 33.039C29.7473 33.4665 28.7741 33.6828 27.4462 33.7913C26.1172 33.8999 24.4419 33.9 22.2 33.9H15.8C13.5581 33.9 11.8828 33.8999 10.5538 33.7913C9.22593 33.6828 8.25268 33.4665 7.41348 33.039C5.92699 32.2816 4.71845 31.073 3.96105 29.5865C3.53345 28.7473 3.31715 27.7741 3.20866 26.4462C3.10008 25.1172 3.1 23.4419 3.1 21.2V14.8Z"
|
||||||
|
stroke="#0A0D12"
|
||||||
|
strokeOpacity="0.12"
|
||||||
|
strokeWidth="0.2"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
<image
|
||||||
|
href="data:image/webp;base64,UklGRoYHAABXRUJQVlA4WAoAAAAQAAAAnwAATwAAQUxQSIcFAAABD9D/iAjY3/9PbSp7ed+ZTNJJKWUSCpsenYbSDay1rPukpGxgrV3WtfCsu16XSSmc9NgTuLa+Qa+7e1h3d3ff/em6RvQ/jjUcsJXcxqnp73y0Dd1iS99JJiIk8ActCkLbexBTRwGAiRepAc0DWAxNoPI469jGE+uApSJKwSaxwH/SDuvFRgSL+iF90f0/uOinRfp2jRkl3Nff8aS4scgmTK8WSJAVeVMRv8MPmIiL0AL18jbT0iUQ4DLPPF/UJ4mbkQ4c4NfrQGJI7ocYCmQLUqQGLS9i1ZjJ9YD0wfkBRF/XQbJCuVeo1XGMePjwSf2n48dncwVJ5gjukhRfrTlAVTy4rpscDVXOijJypxQS6iZgBm9Q/kbbeQC8WMyky2I1RSACacETMY7ld3aOWZWw7g04bJcNNfCX6IsL4cz9WT+HbkzgHlngSQGIuajVbyZkyV32WwfZg2CY/K69d78dyr19p3jzASrzrYmkoHQM/2cGci2NjEfmfsjszTI6mhxAeqmb/h72AaqBsGWdCrha2VmpQtyCx/EEGm/dvEnAt/gBqYPCDEh3R6bFAQoJcUvQRIdkuD2Zk4H+uHUhGxHpl7F/DNHVZnf7DX/F7JqyqhhOZ4Vy4aVATT89pT6Oio4sfs5ULH9Ur0ZpYPL1Eq+om9+wDFkhQ0IDF7EBFfA8yWGAgWQ8lWnm0cnzknwjxU6eQkAfVsiZBxhw0hjPFwx7uwcLkAZYD2lOyDqorXqpgjlI9zdQX61m7fly+X4FlyixZi0OifzLtlA26N2N9+N5d8NP3f3h7AG4EfiMPaLDdgJOUEz0LipV00L4ouO+0F5826ZwaV2BSUWQ03dWSTzXqKZVC9pM6caAo/Xxu9V2Y5hwD0eAaPXq6ndNlvNQYS4NcAJpT6UHVigUPfSX5tJxATt1ZVukUft1iDcX/swp1q9mPN+Jn8hNz22sgVH/Hj6e+N/5DPU6IkcEMIhhCYffRA+HtZbBngZ4epF4vzCAu6OSk+D6xA2piTzCJVPIGnseVYeMjMgTPejNPc6yYypzXj4G8AFZeiu3+MlN8E5Pzm+e6XoV043lWOqKp7XjesRhra133Li79Z2UoDiwizOksgRVaMAsj3o/INvNRZ3HSA/vuYkPsoNbBgIopPsHfiPSdJY2Szy752RKphatmd/pACuOi57f3tVk30vJ3DAMf07h1UMzknO2Ez8B24dV7YxgK87zMIKdB+w8BCp59/G29K5GW5NM97V3NwBS9BcIN/91ETsQNwRQcWhAQloc1ngPoAcX3t3GgqXP8f88Sj3S07Ub09YQWbJ1MHRgEpl10ayV96mWyfsGaBONZoORvD9zPei0Ad6XtGZytQ6WDxJN61//aXV1BosiPeNS69b2B+XS+dc66dgPjB9mfyNvfPFhuUQRyOu1pHN+Dxw7u7YMP7Gb0DGL4nRcJQDEufQ+UMcP6F6a3RX/ZtPJN8P/yb/g4YsEpOOEk72GY2VNq7AlCJ/n96M3Rq6J5sXXNmrgfHjypfv3WSO/pOuElp+ckuHDNSLmsfmHdmSj9j+Nmosz8FeFuu1+0QMOQlMnTxfNAkTfoe0Rijtpq38Mpj+W9b3yUeHlfcazjLbQQhds194PAPDZwgj71n+VRxDXHscni/Wky7ywckRU4/JqDpVmuOed3eBwCb10sD55/TfECEJNPJPYeG6UuvcrtswWvQOGXDft/1xrxR7Az/JQWtisbGGbQrgbQjx2rJRUh0am0G/Yj0Cu/NQy7f8JtQVbh+ZCgIt7gOEADvJFKwEr7/GrgQql3CM/urtBzm9nMnCj53WEWr3/5BJROomYBUwsRXInvP5O3DqZfVoD9WRpEsUH4yx6BI4JaaMQZUYZ8F7y0KBvD/PTDSR/CgBWUDgg2AEAAFATAJ0BKqAAUAA+KRKHQqGhCj4CcgwBQlnAM4spE3mv/5foxj7CIWswa6AqkXFI+Yh0P/x5D3Ezvirfp0TGLFgI7x9F5fO5IHYoatU6Evly836v+YqU0oO2cfVypfHDsDw29g1p0iydYsDT1vJNxjinCDWybXmWiyxS9UEu/eRrG49iyvR67gMIAazf0vPgtrbCHQlTv5rJV7PPFdfirZtz+MAA/v5sJNKJKeJ/hzR4FAhZT+MvOLxH42j6POGLQ5xx2In90EX/MqvckpWMe6weKHop6T9BUAz/qmeY0d8DTiGxEk9YU598XXHiiNGKd3N9bWu4tL9X/OTRVy1Rn7PiG3QoRIX3J93WJsqnemrvJRVh5OFlmDx8B2NZglxXEALZRATlzsBOth3ETpiwt4j0QGUWf9bqg+8o+N9xVYJQdwytiTyuepp1FCA/u6R5xx93RhFIuyDILKBC2Y5InXCxD6GMe2LENm7ZJ/grDJ4/Sw87hS1FBNG9/Q83pgBC8DNlOmf4pz//jBR6YIzM9rp4182sAr4cYbiEejZB40FUN1LWRAjTwjz+qDQMg6IT9yo01SmHMkGkr4vQZayROK4PIIkRRSAlJALZq89W30VJUep6YrggAAA="
|
||||||
|
x="0"
|
||||||
|
y="19"
|
||||||
|
width="38"
|
||||||
|
height="19"
|
||||||
|
transform="scale(0.84) translate(0, -1.5)"
|
||||||
|
className="origin-center"
|
||||||
|
preserveAspectRatio="xMidYMax slice"
|
||||||
|
clipPath={`url(#imageClip-${id})`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<defs>
|
||||||
|
<clipPath id={`imageClip-${id}`}>
|
||||||
|
<path d="M 0 19 L 38 19 L 38 28.88 A 9.12 9.12 0 0 1 28.88 38 L 9.12 38 A 9.12 9.12 0 0 1 0 28.88 Z" />
|
||||||
|
</clipPath>
|
||||||
|
<filter id={`filter0-${id}`} x="0" y="0" width="38" height="38" filterUnits="userSpaceOnUse" colorInterpolationFilters="sRGB">
|
||||||
|
<feFlood floodOpacity="0" result="BackgroundImageFix" />
|
||||||
|
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
|
||||||
|
<feOffset dy="1" />
|
||||||
|
<feGaussianBlur stdDeviation="1" />
|
||||||
|
<feColorMatrix type="matrix" values="0 0 0 0 0.0392157 0 0 0 0 0.0509804 0 0 0 0 0.0705882 0 0 0 0.06 0" />
|
||||||
|
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow" />
|
||||||
|
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
|
||||||
|
<feOffset dy="1" />
|
||||||
|
<feGaussianBlur stdDeviation="1.5" />
|
||||||
|
<feColorMatrix type="matrix" values="0 0 0 0 0.0392157 0 0 0 0 0.0509804 0 0 0 0 0.0705882 0 0 0 0.1 0" />
|
||||||
|
<feBlend mode="normal" in2="effect1_dropShadow" result="effect2_dropShadow" />
|
||||||
|
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
|
||||||
|
<feMorphology radius="0.5" operator="erode" in="SourceAlpha" result="effect3_dropShadow" />
|
||||||
|
<feOffset dy="1" />
|
||||||
|
<feGaussianBlur stdDeviation="0.5" />
|
||||||
|
<feComposite in2="hardAlpha" operator="out" />
|
||||||
|
<feColorMatrix type="matrix" values="0 0 0 0 0.0392157 0 0 0 0 0.0509804 0 0 0 0 0.0705882 0 0 0 0.13 0" />
|
||||||
|
<feBlend mode="normal" in2="effect2_dropShadow" result="effect3_dropShadow" />
|
||||||
|
<feBlend mode="normal" in="SourceGraphic" in2="effect3_dropShadow" result="shape" />
|
||||||
|
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
|
||||||
|
<feOffset dy="-0.5" />
|
||||||
|
<feGaussianBlur stdDeviation="0.25" />
|
||||||
|
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" />
|
||||||
|
<feColorMatrix type="matrix" values="0 0 0 0 0.0392157 0 0 0 0 0.0509804 0 0 0 0 0.0705882 0 0 0 0.1 0" />
|
||||||
|
<feBlend mode="normal" in2="shape" result="effect4_innerShadow" />
|
||||||
|
</filter>
|
||||||
|
<filter id={`filter1_dd-${id}`} x="8" y="8" width="22" height="22" filterUnits="userSpaceOnUse" colorInterpolationFilters="sRGB">
|
||||||
|
<feFlood floodOpacity="0" result="BackgroundImageFix" />
|
||||||
|
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
|
||||||
|
<feOffset dy="1" />
|
||||||
|
<feGaussianBlur stdDeviation="1" />
|
||||||
|
<feColorMatrix type="matrix" values="0 0 0 0 0.0392157 0 0 0 0 0.0509804 0 0 0 0 0.0705882 0 0 0 0.06 0" />
|
||||||
|
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow" />
|
||||||
|
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
|
||||||
|
<feOffset dy="1" />
|
||||||
|
<feGaussianBlur stdDeviation="1.5" />
|
||||||
|
<feColorMatrix type="matrix" values="0 0 0 0 0.0392157 0 0 0 0 0.0509804 0 0 0 0 0.0705882 0 0 0 0.1 0" />
|
||||||
|
<feBlend mode="normal" in2="effect1_dropShadow" result="effect2_dropShadow" />
|
||||||
|
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow" result="shape" />
|
||||||
|
</filter>
|
||||||
|
<filter id={`filter2_b-${id}`} x="-2" y="13" width="42" height="26" filterUnits="userSpaceOnUse" colorInterpolationFilters="sRGB">
|
||||||
|
<feFlood floodOpacity="0" result="BackgroundImageFix" />
|
||||||
|
<feGaussianBlur in="BackgroundImageFix" stdDeviation="2.5" />
|
||||||
|
<feComposite in2="SourceAlpha" operator="in" result="effect1_backgroundBlur" />
|
||||||
|
<feBlend mode="normal" in="SourceGraphic" in2="effect1_backgroundBlur" result="shape" />
|
||||||
|
</filter>
|
||||||
|
<linearGradient id={`paint0_linear-${id}`} x1="19" y1="2" x2="19" y2="34" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stopColor="white" />
|
||||||
|
<stop offset="1" stopColor="#0A0D12" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id={`paint1_linear-${id}`} x1="15" y1="26" x2="23" y2="10" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stopColor="#53389E" />
|
||||||
|
<stop offset="1" stopColor="#6941C6" />
|
||||||
|
</linearGradient>
|
||||||
|
<radialGradient
|
||||||
|
id={`paint2_radial-${id}`}
|
||||||
|
cx="0"
|
||||||
|
cy="0"
|
||||||
|
r="1"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
gradientTransform="translate(19 10) rotate(90) scale(12)"
|
||||||
|
>
|
||||||
|
<stop stopColor="white" stopOpacity="0" />
|
||||||
|
<stop offset="0.5" stopColor="white" stopOpacity="0" />
|
||||||
|
<stop offset="0.99" stopColor="white" />
|
||||||
|
<stop offset="1" stopColor="white" stopOpacity="0" />
|
||||||
|
</radialGradient>
|
||||||
|
<radialGradient
|
||||||
|
id={`paint3_radial-${id}`}
|
||||||
|
cx="0"
|
||||||
|
cy="0"
|
||||||
|
r="1"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
gradientTransform="translate(19 18) rotate(90) scale(8)"
|
||||||
|
>
|
||||||
|
<stop offset="0.746599" stopColor="white" stopOpacity="0" />
|
||||||
|
<stop offset="1" stopColor="white" />
|
||||||
|
</radialGradient>
|
||||||
|
<radialGradient
|
||||||
|
id={`paint4_radial-${id}`}
|
||||||
|
cx="0"
|
||||||
|
cy="0"
|
||||||
|
r="1"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
gradientTransform="translate(19 14.6) rotate(90) scale(7)"
|
||||||
|
>
|
||||||
|
<stop stopColor="white" />
|
||||||
|
<stop offset="1" stopColor="white" stopOpacity="0" />
|
||||||
|
</radialGradient>
|
||||||
|
<linearGradient id={`paint5_linear-${id}`} x1="19" y1="11.6" x2="19" y2="14.8" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stopColor="white" />
|
||||||
|
<stop offset="1" stopColor="white" stopOpacity="0.1" />
|
||||||
|
</linearGradient>
|
||||||
|
<clipPath id={`clip0-${id}`}>
|
||||||
|
<path
|
||||||
|
d="M3 14.8C3 10.3196 3 8.07937 3.87195 6.36808C4.63893 4.86278 5.86278 3.63893 7.36808 2.87195C9.07937 2 11.3196 2 15.8 2H22.2C26.6804 2 28.9206 2 30.6319 2.87195C32.1372 3.63893 33.3611 4.86278 34.1281 6.36808C35 8.07937 35 10.3196 35 14.8V21.2C35 25.6804 35 27.9206 34.1281 29.6319C33.3611 31.1372 32.1372 32.3611 30.6319 33.1281C28.9206 34 26.6804 34 22.2 34H15.8C11.3196 34 9.07937 34 7.36808 33.1281C5.86278 32.3611 4.63893 31.1372 3.87195 29.6319C3 27.9206 3 25.6804 3 21.2V14.8Z"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
</clipPath>
|
||||||
|
<clipPath id={`clip1-${id}`}>
|
||||||
|
<rect width="32" height="32" fill="white" transform="translate(3 2)" />
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
||||||
58
src/components/foundations/logo/untitledui-logo.tsx
Normal file
58
src/components/foundations/logo/untitledui-logo.tsx
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import type { HTMLAttributes } from "react";
|
||||||
|
import { cx } from "@/utils/cx";
|
||||||
|
import { UntitledLogoMinimal } from "./untitledui-logo-minimal";
|
||||||
|
|
||||||
|
export const UntitledLogo = (props: HTMLAttributes<HTMLOrSVGElement>) => {
|
||||||
|
return (
|
||||||
|
<div {...props} className={cx("flex h-8 w-max items-center justify-start overflow-visible", props.className)}>
|
||||||
|
{/* Minimal logo */}
|
||||||
|
<UntitledLogoMinimal className="aspect-square h-full w-auto shrink-0" />
|
||||||
|
|
||||||
|
{/* Gap that adjusts to the height of the container */}
|
||||||
|
<div className="aspect-[0.3] h-full" />
|
||||||
|
|
||||||
|
{/* Logomark */}
|
||||||
|
<svg viewBox="0 0 97 32" fill="none" className="aspect-[3] h-full shrink-0">
|
||||||
|
<path
|
||||||
|
d="M33.9101 10.2372C34.2321 10.5355 34.6179 10.6847 35.0678 10.6847C35.5176 10.6847 35.9011 10.5355 36.2183 10.2372C36.5403 9.9342 36.7013 9.57199 36.7013 9.15058C36.7013 8.73392 36.5403 8.37644 36.2183 8.07814C35.9011 7.77511 35.5176 7.6236 35.0678 7.6236C34.6179 7.6236 34.2321 7.77511 33.9101 8.07814C33.5928 8.37644 33.4342 8.73392 33.4342 9.15058C33.4342 9.57199 33.5928 9.9342 33.9101 10.2372Z"
|
||||||
|
className="fill-fg-primary"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M11.2997 20.6847C11.8063 19.8892 12.0597 18.9612 12.0597 17.9006V8.45456H8.98438V17.6378C8.98438 18.1918 8.86127 18.6842 8.61506 19.1151C8.37358 19.5459 8.0303 19.8845 7.58523 20.1307C7.14489 20.3769 6.62642 20.5 6.02983 20.5C5.43797 20.5 4.91951 20.3769 4.47443 20.1307C4.02936 19.8845 3.68371 19.5459 3.4375 19.1151C3.19602 18.6842 3.07528 18.1918 3.07528 17.6378V8.45456H0V17.9006C0 18.9612 0.250947 19.8892 0.752841 20.6847C1.25473 21.4801 1.95786 22.1004 2.86222 22.5455C3.76657 22.9858 4.82244 23.206 6.02983 23.206C7.23248 23.206 8.28599 22.9858 9.19034 22.5455C10.0947 22.1004 10.7978 21.4801 11.2997 20.6847Z"
|
||||||
|
className="fill-fg-primary"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M18.3589 12.51C17.7907 12.8793 17.3859 13.3812 17.1444 14.0156H17.0165V12.0909H14.133V23H17.1586V16.6932C17.1633 16.2244 17.2509 15.8244 17.4214 15.4929C17.5966 15.1567 17.838 14.9011 18.1458 14.7259C18.4583 14.5507 18.8182 14.4631 19.2254 14.4631C19.8314 14.4631 20.3073 14.6525 20.6529 15.0313C20.9986 15.4053 21.169 15.9262 21.1643 16.5938V23H24.1898V16.054C24.1898 15.2065 24.0336 14.4773 23.7211 13.8665C23.4086 13.251 22.9706 12.7775 22.4072 12.446C21.8437 12.1146 21.1832 11.9489 20.4256 11.9489C19.616 11.9489 18.9271 12.1359 18.3589 12.51Z"
|
||||||
|
className="fill-fg-primary"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M27.3463 21.821C27.0433 21.3523 26.8941 20.7604 26.8989 20.0455V14.3637H25.4074V12.0909H26.8989V9.47729H29.9244V12.0909H31.977V14.3637H29.9244V19.6477C29.9244 19.9271 29.967 20.1449 30.0523 20.3012C30.1375 20.4527 30.2559 20.5592 30.4074 20.6208C30.5636 20.6823 30.7436 20.7131 30.9472 20.7131C31.0892 20.7131 31.2313 20.7012 31.3733 20.6776C31.5153 20.6492 31.6242 20.6279 31.7 20.6137L32.1759 22.8651C32.0243 22.9124 31.8113 22.9669 31.5366 23.0284C31.262 23.0947 30.9282 23.135 30.5352 23.1492C29.8061 23.1776 29.1669 23.0805 28.6176 22.858C28.0731 22.6354 27.6493 22.2898 27.3463 21.821Z"
|
||||||
|
className="fill-fg-primary"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M39.769 21.821C39.4659 21.3523 39.3168 20.7604 39.3215 20.0455V14.3637H37.83V12.0909H39.3215V9.47729H42.3471V12.0909H44.3996V14.3637H42.3471V19.6477C42.3471 19.9271 42.3897 20.1449 42.4749 20.3012C42.5602 20.4527 42.6785 20.5592 42.83 20.6208C42.9863 20.6823 43.1662 20.7131 43.3698 20.7131C43.5119 20.7131 43.6539 20.7012 43.796 20.6776C43.938 20.6492 44.0469 20.6279 44.1227 20.6137L44.5985 22.8651C44.447 22.9124 44.2339 22.9669 43.9593 23.0284C43.6847 23.0947 43.3509 23.135 42.9579 23.1492C42.2287 23.1776 41.5895 23.0805 41.0403 22.858C40.4958 22.6354 40.072 22.2898 39.769 21.821Z"
|
||||||
|
className="fill-fg-primary"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M56.2257 23.2131C55.1035 23.2131 54.1376 22.9858 53.328 22.5313C52.5231 22.072 51.9028 21.4233 51.4672 20.5852C51.0316 19.7424 50.8138 18.7458 50.8138 17.5952C50.8138 16.473 51.0316 15.4882 51.4672 14.6406C51.9028 13.7931 52.516 13.1326 53.3067 12.6591C54.1021 12.1856 55.0349 11.9489 56.105 11.9489C56.8247 11.9489 57.4946 12.0649 58.1149 12.2969C58.7399 12.5242 59.2844 12.8674 59.7484 13.3267C60.2172 13.786 60.5818 14.3637 60.8422 15.0597C61.1026 15.751 61.2328 16.5606 61.2328 17.4887V18.3196H53.8038V18.3267C53.8038 18.8665 53.9033 19.3329 54.1021 19.7259C54.3057 20.1189 54.5922 20.4219 54.9615 20.635C55.3308 20.848 55.7688 20.9546 56.2754 20.9546C56.6116 20.9546 56.9194 20.9072 57.1987 20.8125C57.4781 20.7178 57.7172 20.5758 57.916 20.3864C58.1149 20.197 58.2664 19.965 58.3706 19.6904L61.1689 19.875C61.0268 20.5474 60.7357 21.1345 60.2953 21.6364C59.8597 22.1335 59.2963 22.5218 58.605 22.8012C57.9184 23.0758 57.1253 23.2131 56.2257 23.2131ZM54.1092 15.3722C53.9258 15.6954 53.8249 16.0529 53.8067 16.4446H58.3848C58.3848 16.009 58.2901 15.6231 58.1007 15.2869C57.9113 14.9508 57.6485 14.688 57.3124 14.4986C56.9809 14.3045 56.595 14.2074 56.1547 14.2074C55.6954 14.2074 55.2882 14.3139 54.9331 14.527C54.5827 14.7353 54.3081 15.0171 54.1092 15.3722Z"
|
||||||
|
className="fill-fg-primary"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M64.5757 22.5384C65.2481 22.9645 65.9985 23.1776 66.8271 23.1776C67.4143 23.1776 67.9114 23.0805 68.3186 22.8864C68.7305 22.6923 69.0643 22.4484 69.32 22.1548C69.5804 21.8566 69.7817 21.5559 69.9237 21.2529H70.0516V23H73.0345V8.45456H70.0161V13.9233H69.9237C69.7911 13.6298 69.597 13.3315 69.3413 13.0284C69.0904 12.7207 68.7589 12.465 68.347 12.2614C67.9398 12.053 67.4308 11.9489 66.82 11.9489C66.0198 11.9489 65.2836 12.1572 64.6112 12.5739C63.9436 12.9858 63.4086 13.6084 63.0061 14.4418C62.6036 15.2704 62.4024 16.3097 62.4024 17.5597C62.4024 18.7765 62.5965 19.804 62.9848 20.6421C63.3778 21.4754 63.9081 22.1075 64.5757 22.5384ZM69.0217 20.3722C68.6856 20.6373 68.2736 20.7699 67.7859 20.7699C67.2888 20.7699 66.8698 20.635 66.5288 20.3651C66.1927 20.0905 65.9346 19.7117 65.7547 19.2287C65.5795 18.741 65.4919 18.1799 65.4919 17.5455C65.4919 16.9157 65.5795 16.3618 65.7547 15.8835C65.9299 15.4053 66.1879 15.0313 66.5288 14.7614C66.8698 14.4915 67.2888 14.3566 67.7859 14.3566C68.2736 14.3566 68.6879 14.4868 69.0288 14.7472C69.3698 15.0076 69.6302 15.3769 69.8101 15.8551C69.99 16.3334 70.08 16.8968 70.08 17.5455C70.08 18.1941 69.9876 18.76 69.803 19.2429C69.6231 19.7259 69.3626 20.1023 69.0217 20.3722Z"
|
||||||
|
className="fill-fg-primary"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M88.0229 19.1151C88.2691 18.6842 88.3922 18.1918 88.3922 17.6378V8.45456H91.4675V17.9006C91.4675 18.9612 91.2142 19.8892 90.7075 20.6847C90.2056 21.4801 89.5025 22.1004 88.5982 22.5455C87.6938 22.9858 86.6403 23.206 85.4376 23.206C84.2303 23.206 83.1744 22.9858 82.27 22.5455C81.3657 22.1004 80.6625 21.4801 80.1607 20.6847C79.6588 19.8892 79.4078 18.9612 79.4078 17.9006V8.45456H82.4831V17.6378C82.4831 18.1918 82.6038 18.6842 82.8453 19.1151C83.0915 19.5459 83.4372 19.8845 83.8822 20.1307C84.3273 20.3769 84.8458 20.5 85.4376 20.5C86.0342 20.5 86.5527 20.3769 86.993 20.1307C87.4381 19.8845 87.7814 19.5459 88.0229 19.1151Z"
|
||||||
|
className="fill-fg-primary"
|
||||||
|
/>
|
||||||
|
<path d="M33.5479 12.0909V23H36.5734V12.0909H33.5479Z" className="fill-fg-primary" />
|
||||||
|
<path d="M49.2305 23V8.45456H46.2049V23H49.2305Z" className="fill-fg-primary" />
|
||||||
|
<path d="M96.6729 23V8.45456H93.5977V23H96.6729Z" className="fill-fg-primary" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
17
src/components/foundations/payment-icons/amex-icon.tsx
Normal file
17
src/components/foundations/payment-icons/amex-icon.tsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import type { SVGProps } from "react";
|
||||||
|
|
||||||
|
const AmexIcon = (props: SVGProps<SVGSVGElement>) => {
|
||||||
|
return (
|
||||||
|
<svg width="34" height="24" viewBox="0 0 34 24" fill="none" {...props}>
|
||||||
|
<path d="M0 4C0 1.79086 1.79086 0 4 0H30C32.2091 0 34 1.79086 34 4V20C34 22.2091 32.2091 24 30 24H4C1.79086 24 0 22.2091 0 20V4Z" fill="#1F72CD" />
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M6.09517 8.5L2.91406 15.7467H6.7223L7.19441 14.5913H8.27355L8.74566 15.7467H12.9375V14.8649L13.311 15.7467H15.4793L15.8528 14.8462V15.7467H24.5706L25.6307 14.6213L26.6232 15.7467L31.1009 15.7561L27.9097 12.1436L31.1009 8.5H26.6927L25.6608 9.60463L24.6995 8.5H15.2156L14.4013 10.3704L13.5678 8.5H9.7675V9.35186L9.34474 8.5H6.09517ZM6.83205 9.52905H8.68836L10.7984 14.4431V9.52905H12.8319L14.4617 13.0524L15.9637 9.52905H17.987V14.7291H16.7559L16.7458 10.6544L14.9509 14.7291H13.8495L12.0446 10.6544V14.7291H9.51179L9.03162 13.5633H6.43745L5.95827 14.728H4.60123L6.83205 9.52905ZM24.1196 9.52905H19.1134V14.726H24.0421L25.6307 13.0036L27.1618 14.726H28.7624L26.436 12.1426L28.7624 9.52905H27.2313L25.6507 11.2316L24.1196 9.52905ZM7.73508 10.4089L6.8804 12.4856H8.58876L7.73508 10.4089ZM20.3497 11.555V10.6057V10.6048H23.4734L24.8364 12.1229L23.413 13.6493H20.3497V12.613H23.0808V11.555H20.3497Z"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AmexIcon;
|
||||||
25
src/components/foundations/payment-icons/apple-pay-icon.tsx
Normal file
25
src/components/foundations/payment-icons/apple-pay-icon.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import type { SVGProps } from "react";
|
||||||
|
|
||||||
|
const ApplePayIcon = (props: SVGProps<SVGSVGElement>) => {
|
||||||
|
return (
|
||||||
|
<svg width="34" height="24" viewBox="0 0 34 24" fill="none" {...props}>
|
||||||
|
<path
|
||||||
|
d="M0.5 4C0.5 2.067 2.067 0.5 4 0.5H30C31.933 0.5 33.5 2.067 33.5 4V20C33.5 21.933 31.933 23.5 30 23.5H4C2.067 23.5 0.5 21.933 0.5 20V4Z"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M0.5 4C0.5 2.067 2.067 0.5 4 0.5H30C31.933 0.5 33.5 2.067 33.5 4V20C33.5 21.933 31.933 23.5 30 23.5H4C2.067 23.5 0.5 21.933 0.5 20V4Z"
|
||||||
|
className="stroke-border-secondary"
|
||||||
|
strokeWidth="0.75"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M9.44921 8.34316C9.16382 8.69506 8.70721 8.97261 8.2506 8.93296C8.19353 8.45715 8.41707 7.95161 8.67867 7.63936C8.96406 7.27755 9.46348 7.01983 9.86777 7C9.91533 7.49563 9.72983 7.98135 9.44921 8.34316ZM9.86297 9.02712C9.46071 9.003 9.09366 9.15319 8.79718 9.2745C8.60639 9.35256 8.44483 9.41867 8.32191 9.41867C8.18397 9.41867 8.01574 9.34903 7.82685 9.27084L7.82685 9.27084C7.57935 9.16838 7.29638 9.05124 6.99964 9.05686C6.31948 9.06677 5.68688 9.46823 5.33967 10.1076C4.62621 11.3863 5.15417 13.2796 5.84384 14.3205C6.18155 14.8359 6.58584 15.4009 7.11855 15.3811C7.35291 15.3719 7.5215 15.2973 7.69597 15.2202C7.89683 15.1314 8.10549 15.0391 8.43131 15.0391C8.74582 15.0391 8.94536 15.129 9.1369 15.2152C9.31903 15.2973 9.49393 15.376 9.75358 15.3712C10.3053 15.3613 10.6525 14.8557 10.9902 14.3403C11.3547 13.7871 11.5148 13.2471 11.5391 13.1652L11.542 13.1557C11.5414 13.1551 11.5369 13.153 11.5289 13.1492C11.4071 13.0911 10.476 12.6469 10.467 11.4557C10.4581 10.4559 11.2056 9.94935 11.3233 9.86961L11.3233 9.8696C11.3304 9.86476 11.3353 9.86149 11.3374 9.85978C10.8618 9.12625 10.1198 9.04695 9.86297 9.02712ZM13.6824 15.3167V7.5898H16.4649C17.9013 7.5898 18.9049 8.62071 18.9049 10.1274C18.9049 11.6341 17.8822 12.675 16.4268 12.675H14.8334V15.3167H13.6824ZM14.8333 8.60088H16.1603C17.1592 8.60088 17.7299 9.15599 17.7299 10.1324C17.7299 11.1088 17.1592 11.6688 16.1556 11.6688H14.8333V8.60088ZM22.7053 14.3898C22.4009 14.9945 21.7302 15.3761 21.0072 15.3761C19.9371 15.3761 19.1903 14.712 19.1903 13.7108C19.1903 12.7196 19.9133 12.1496 21.2498 12.0653L22.6862 11.9761V11.5499C22.6862 10.9204 22.2915 10.5784 21.5875 10.5784C21.0072 10.5784 20.5839 10.8907 20.4983 11.3665H19.4614C19.4947 10.3653 20.3984 9.63675 21.6208 9.63675C22.9383 9.63675 23.7945 10.3554 23.7945 11.4706V15.3167H22.729V14.3898H22.7053ZM21.3163 14.4592C20.7027 14.4592 20.3127 14.1519 20.3127 13.6811C20.3127 13.1954 20.6885 12.9129 21.4067 12.8683L22.6861 12.784V13.2202C22.6861 13.9438 22.0964 14.4592 21.3163 14.4592ZM27.3284 15.619C26.867 16.9721 26.3391 17.4181 25.2166 17.4181C25.131 17.4181 24.8456 17.4082 24.779 17.3884V16.4616C24.8503 16.4715 25.0263 16.4814 25.1167 16.4814C25.6256 16.4814 25.911 16.2584 26.087 15.6785L26.1916 15.3365L24.2415 9.7111H25.4449L26.8004 14.2759H26.8242L28.1798 9.7111H29.3499L27.3284 15.619Z"
|
||||||
|
fill="black"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ApplePayIcon;
|
||||||
32
src/components/foundations/payment-icons/discover-icon.tsx
Normal file
32
src/components/foundations/payment-icons/discover-icon.tsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import type { SVGProps } from "react";
|
||||||
|
|
||||||
|
const DiscoverIcon = (props: SVGProps<SVGSVGElement>) => {
|
||||||
|
return (
|
||||||
|
<svg width="34" height="24" viewBox="0 0 34 24" fill="none" {...props}>
|
||||||
|
<path
|
||||||
|
d="M0.5 4C0.5 2.067 2.067 0.5 4 0.5H30C31.933 0.5 33.5 2.067 33.5 4V20C33.5 21.933 31.933 23.5 30 23.5H4C2.067 23.5 0.5 21.933 0.5 20V4Z"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M0.5 4C0.5 2.067 2.067 0.5 4 0.5H30C31.933 0.5 33.5 2.067 33.5 4V20C33.5 21.933 31.933 23.5 30 23.5H4C2.067 23.5 0.5 21.933 0.5 20V4Z"
|
||||||
|
className="stroke-border-secondary"
|
||||||
|
strokeWidth="0.75"
|
||||||
|
/>
|
||||||
|
<path d="M14 23L33 17.25V20C33 21.6569 31.6569 23 30 23H14Z" fill="#FD6020" />
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M29.3937 9.11084C30.439 9.11084 31.0139 9.59438 31.0139 10.5077C31.0662 11.2062 30.5958 11.7972 29.9686 11.9046L31.3797 13.8925H30.2822L29.0801 11.9584H28.9756V13.8925H28.0871V9.11084H29.3937ZM28.9756 11.3137H29.2369C29.8118 11.3137 30.0731 11.045 30.0731 10.5615C30.0731 10.1317 29.8118 9.86304 29.2369 9.86304H28.9756V11.3137ZM25.0034 13.8925H27.5122V13.0866H25.8919V11.7972H27.4599V10.9913H25.8919V9.91674H27.5122V9.11084H25.0034V13.8925ZM22.3902 12.3345L21.1881 9.11084H20.2474L22.1812 14H22.6515L24.5853 9.11084H23.6446L22.3902 12.3345ZM11.7805 11.5286C11.7805 12.8717 12.8258 14 14.1324 14C14.5505 14 14.9164 13.8925 15.2822 13.7314V12.6568C15.0209 12.9792 14.655 13.1941 14.2369 13.1941C13.4007 13.1941 12.7212 12.5494 12.7212 11.6897V11.5823C12.669 10.7227 13.3484 9.97048 14.1847 9.91675C14.6028 9.91675 15.0209 10.1317 15.2822 10.454V9.37948C14.9686 9.16458 14.5505 9.11085 14.1847 9.11085C12.8258 9.0034 11.7805 10.1317 11.7805 11.5286ZM10.1603 10.9376C9.63762 10.7227 9.48082 10.6152 9.48082 10.3466C9.53309 10.0242 9.79441 9.75557 10.108 9.8093C10.3693 9.8093 10.6306 9.97048 10.8397 10.1854L11.3101 9.54066C10.9442 9.2183 10.4739 9.00339 10.0035 9.00339C9.27176 8.94967 8.64459 9.54066 8.59232 10.2928V10.3466C8.59232 10.9913 8.85365 11.3674 9.68988 11.636C9.89894 11.6897 10.108 11.7972 10.3171 11.9046C10.4739 12.0121 10.5784 12.1733 10.5784 12.3882C10.5784 12.7643 10.2648 13.0866 9.95121 13.0866H9.89894C9.48082 13.0866 9.11497 12.818 8.95818 12.4419L8.38326 13.0329C8.69685 13.6239 9.32403 13.9463 9.95121 13.9463C10.7874 14 11.4669 13.3553 11.5191 12.4956V12.3345C11.4669 11.6897 11.2056 11.3674 10.1603 10.9376ZM7.12892 13.8925H8.01742V9.11084H7.12892V13.8925ZM3 9.11086H4.30662H4.56794C5.8223 9.16458 6.81532 10.2391 6.76306 11.5286C6.76306 12.227 6.44947 12.8717 5.92682 13.3553C5.45644 13.7314 4.88153 13.9463 4.30662 13.8926H3V9.11086ZM4.14983 13.0866C4.56794 13.1404 5.03833 12.9792 5.35191 12.7105C5.6655 12.3882 5.8223 11.9584 5.8223 11.4748C5.8223 11.045 5.6655 10.6152 5.35191 10.2928C5.03833 10.0242 4.56794 9.86302 4.14983 9.91674H3.8885V13.0866H4.14983Z"
|
||||||
|
fill="black"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M17.9481 9C16.6415 9 15.5439 10.0745 15.5439 11.4714C15.5439 12.8146 16.5892 13.9429 17.9481 13.9966C19.307 14.0503 20.3523 12.9221 20.4046 11.5252C20.3523 10.1283 19.307 9 17.9481 9V9Z"
|
||||||
|
fill="#FD6020"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DiscoverIcon;
|
||||||
8
src/components/foundations/payment-icons/index.tsx
Normal file
8
src/components/foundations/payment-icons/index.tsx
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export { default as AmexIcon } from "./amex-icon";
|
||||||
|
export { default as ApplePayIcon } from "./apple-pay-icon";
|
||||||
|
export { default as DiscoverIcon } from "./discover-icon";
|
||||||
|
export { default as MastercardIcon } from "./mastercard-icon";
|
||||||
|
export { default as VisaIcon } from "./visa-icon";
|
||||||
|
export { default as PayPalIcon } from "./paypal-icon";
|
||||||
|
export { default as StripeIcon } from "./stripe-icon";
|
||||||
|
export { default as UnionPayIcon } from "./union-pay-icon";
|
||||||
37
src/components/foundations/payment-icons/mastercard-icon.tsx
Normal file
37
src/components/foundations/payment-icons/mastercard-icon.tsx
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import type { SVGProps } from "react";
|
||||||
|
|
||||||
|
const MastercardIcon = (props: SVGProps<SVGSVGElement>) => {
|
||||||
|
return (
|
||||||
|
<svg width="34" height="24" viewBox="0 0 34 24" fill="none" {...props}>
|
||||||
|
<path
|
||||||
|
d="M0.5 4C0.5 2.067 2.067 0.5 4 0.5H30C31.933 0.5 33.5 2.067 33.5 4V20C33.5 21.933 31.933 23.5 30 23.5H4C2.067 23.5 0.5 21.933 0.5 20V4Z"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M0.5 4C0.5 2.067 2.067 0.5 4 0.5H30C31.933 0.5 33.5 2.067 33.5 4V20C33.5 21.933 31.933 23.5 30 23.5H4C2.067 23.5 0.5 21.933 0.5 20V4Z"
|
||||||
|
className="stroke-border-secondary"
|
||||||
|
strokeWidth="0.75"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M17.179 16.8294C15.9949 17.8275 14.459 18.43 12.7807 18.43C9.03582 18.43 6 15.4303 6 11.73C6 8.02966 9.03582 5.02997 12.7807 5.02997C14.459 5.02997 15.9949 5.63247 17.179 6.63051C18.363 5.63247 19.8989 5.02997 21.5773 5.02997C25.3221 5.02997 28.358 8.02966 28.358 11.73C28.358 15.4303 25.3221 18.43 21.5773 18.43C19.8989 18.43 18.363 17.8275 17.179 16.8294Z"
|
||||||
|
fill="#ED0006"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M17.1787 16.8294C18.6366 15.6005 19.5611 13.7719 19.5611 11.73C19.5611 9.68801 18.6366 7.85941 17.1787 6.63051C18.3628 5.63247 19.8987 5.02997 21.577 5.02997C25.3219 5.02997 28.3577 8.02966 28.3577 11.73C28.3577 15.4303 25.3219 18.43 21.577 18.43C19.8987 18.43 18.3628 17.8275 17.1787 16.8294Z"
|
||||||
|
fill="#F9A000"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M17.1793 16.8294C18.6372 15.6005 19.5616 13.7719 19.5616 11.73C19.5616 9.68805 18.6372 7.85946 17.1793 6.63055C15.7213 7.85946 14.7969 9.68805 14.7969 11.73C14.7969 13.7719 15.7213 15.6005 17.1793 16.8294Z"
|
||||||
|
fill="#FF5E00"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MastercardIcon;
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user