feat: add authenticated settings page.
This commit is contained in:
1
shadcn-admin/.env.example
Normal file
1
shadcn-admin/.env.example
Normal file
@@ -0,0 +1 @@
|
||||
VITE_CLERK_PUBLISHABLE_KEY=
|
||||
128
shadcn-admin/.github/CODE_OF_CONDUCT.md
vendored
Normal file
128
shadcn-admin/.github/CODE_OF_CONDUCT.md
vendored
Normal file
@@ -0,0 +1,128 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, religion, or sexual identity
|
||||
and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the
|
||||
overall community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or
|
||||
advances of any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email
|
||||
address, without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official e-mail address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at
|
||||
.
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series
|
||||
of actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or
|
||||
permanent ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within
|
||||
the community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.0, available at
|
||||
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||
|
||||
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||
enforcement ladder](https://github.com/mozilla/diversity).
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
https://www.contributor-covenant.org/faq. Translations are available at
|
||||
https://www.contributor-covenant.org/translations.
|
||||
101
shadcn-admin/.github/CONTRIBUTING.md
vendored
Normal file
101
shadcn-admin/.github/CONTRIBUTING.md
vendored
Normal file
@@ -0,0 +1,101 @@
|
||||
# Contributing to Shadcn-Admin
|
||||
|
||||
Thank you for considering contributing to **shadcn-admin**! Every contribution is valuable, whether it's reporting bugs, suggesting improvements, adding features, or refining README.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Getting Started](#getting-started)
|
||||
2. [How to Contribute](#how-to-contribute)
|
||||
3. [Code Standards](#code-standards)
|
||||
4. [Pull Request Guidelines](#pull-request-guidelines)
|
||||
5. [Reporting Issues](#reporting-issues)
|
||||
6. [Community Guidelines](#community-guidelines)
|
||||
|
||||
---
|
||||
|
||||
## Getting Started
|
||||
|
||||
1. **Fork** the repository.
|
||||
2. **Clone** your fork:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/your-username/shadcn-admin.git
|
||||
```
|
||||
|
||||
3. **Install dependencies:**
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
4. **Run the project locally:**
|
||||
|
||||
```bash
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
5. Create a new branch for your contribution:
|
||||
|
||||
```bash
|
||||
git checkout -b feature/your-feature
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## How to Contribute
|
||||
|
||||
- **Feature Requests:** Open an issue or start a discussion to discuss the feature before implementation.
|
||||
- **Bug Fixes:** Provide clear reproduction steps in your issue.
|
||||
- **Documentation:** Improvements to the documentation (README) are always appreciated.
|
||||
|
||||
> **Note:** Pull Requests adding new features without a prior issue or discussion will **not be accepted**.
|
||||
|
||||
---
|
||||
|
||||
## Code Standards
|
||||
|
||||
- Follow the existing **ESLint** and **Prettier** configurations.
|
||||
- Ensure your code is **type-safe** with **TypeScript**.
|
||||
- Maintain consistency with the existing code structure.
|
||||
|
||||
> **Tips!** Before submitting your changes, run the following commands:
|
||||
|
||||
```bash
|
||||
pnpm lint && pnpm format && pnpm knip && pnpm build
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pull Request Guidelines
|
||||
|
||||
- **Follow the [PR Template](./PULL_REQUEST_TEMPLATE.md):**
|
||||
- Description
|
||||
- Types of changes
|
||||
- Checklist
|
||||
- Further comments
|
||||
- Related Issue
|
||||
- Ensure your changes pass **CI checks**.
|
||||
- Keep PRs **focused** and **concise**.
|
||||
- Reference related issues in your PR description.
|
||||
|
||||
---
|
||||
|
||||
## Reporting Issues
|
||||
|
||||
- Clearly describe the issue.
|
||||
- Provide reproduction steps if applicable.
|
||||
- Include screenshots or code examples if relevant.
|
||||
|
||||
---
|
||||
|
||||
## Community Guidelines
|
||||
|
||||
- Be respectful and constructive.
|
||||
- Follow the [Code of Conduct](./CODE_OF_CONDUCT.md).
|
||||
- Stay on topic in discussions.
|
||||
|
||||
---
|
||||
|
||||
Thank you for helping make **shadcn-admin** better! 🚀
|
||||
|
||||
If you have any questions, feel free to reach out via [Discussions](https://github.com/satnaing/shadcn-admin/discussions).
|
||||
14
shadcn-admin/.github/FUNDING.yml
vendored
Normal file
14
shadcn-admin/.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
github: [satnaing]
|
||||
buy_me_a_coffee: satnaing
|
||||
|
||||
# patreon: # Replace with a single Patreon username
|
||||
# open_collective: # Replace with a single Open Collective username
|
||||
# ko_fi: # Replace with a single Ko-fi username
|
||||
# tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
# community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
# liberapay: # Replace with a single Liberapay username
|
||||
# issuehunt: # Replace with a single IssueHunt username
|
||||
# lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
||||
# polar: # Replace with a single Polar username
|
||||
# thanks_dev: # Replace with a single thanks.dev username
|
||||
# custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||
5
shadcn-admin/.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
5
shadcn-admin/.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Shadcn-Admin Discussions
|
||||
url: https://github.com/satnaing/shadcn-admin/discussions
|
||||
about: Please ask and answer questions here.
|
||||
19
shadcn-admin/.github/ISSUE_TEMPLATE/✨-feature-request.md
vendored
Normal file
19
shadcn-admin/.github/ISSUE_TEMPLATE/✨-feature-request.md
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
---
|
||||
name: "✨ Feature Request"
|
||||
about: Suggest an idea for improving Shadcn-Admin
|
||||
title: "[Feature Request]: "
|
||||
labels: enhancement
|
||||
assignees: ""
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
27
shadcn-admin/.github/ISSUE_TEMPLATE/🐞-bug-report.md
vendored
Normal file
27
shadcn-admin/.github/ISSUE_TEMPLATE/🐞-bug-report.md
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
---
|
||||
name: "\U0001F41E Bug report"
|
||||
about: Report a bug or unexpected behavior in Shadcn-Admin
|
||||
title: "[BUG]: "
|
||||
labels: bug
|
||||
assignees: ""
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
27
shadcn-admin/.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
27
shadcn-admin/.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
## Description
|
||||
|
||||
<!-- A clear and concise description of what the pull request does. Include any relevant motivation and background. -->
|
||||
|
||||
## Types of changes
|
||||
|
||||
<!-- What types of changes does your code introduce to AstroPaper? Put an `x` in the boxes that apply -->
|
||||
|
||||
- [ ] Bug Fix (non-breaking change which fixes an issue)
|
||||
- [ ] New Feature (non-breaking change which adds functionality)
|
||||
- [ ] Others (any other types not listed above)
|
||||
|
||||
## Checklist
|
||||
|
||||
<!-- Please follow this checklist and put an x in each of the boxes, like this: [x]. You can also fill these out after creating the PR. This is simply a reminder of what we are going to look for before merging your code. -->
|
||||
|
||||
- [ ] I have read the [Contributing Guide](https://github.com/satnaing/shadcn-admin/blob/main/.github/CONTRIBUTING.md)
|
||||
|
||||
## Further comments
|
||||
|
||||
<!-- If this is a relatively large or complex change, kick off the discussion by explaining why you chose the solution you did and what alternatives you considered, etc... -->
|
||||
|
||||
## Related Issue
|
||||
|
||||
<!-- If this PR is related to an existing issue, link to it here. -->
|
||||
|
||||
Closes: #<!-- Issue number, if applicable -->
|
||||
41
shadcn-admin/.github/workflows/ci.yml
vendored
Normal file
41
shadcn-admin/.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
name: Continuous Integration
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
install-lint-build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Install pnpm
|
||||
run: npm install -g pnpm
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Lint the code
|
||||
run: pnpm lint
|
||||
|
||||
# - name: Analyze unused files and dependencies
|
||||
# run: pnpm knip
|
||||
|
||||
- name: Run Prettier check
|
||||
run: pnpm format:check
|
||||
|
||||
- name: Build the project
|
||||
run: pnpm build
|
||||
29
shadcn-admin/.github/workflows/stale.yml
vendored
Normal file
29
shadcn-admin/.github/workflows/stale.yml
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
name: Close inactive issues/PR
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '38 18 * * *'
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- uses: actions/stale@v5
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
days-before-issue-stale: 120
|
||||
days-before-issue-close: 120
|
||||
stale-issue-label: "stale"
|
||||
stale-issue-message: "This issue is stale because it has been open for 120 days with no activity."
|
||||
close-issue-message: "This issue was closed because it has been inactive for 120 days since being marked as stale."
|
||||
days-before-pr-stale: 120
|
||||
days-before-pr-close: 120
|
||||
stale-pr-label: "stale"
|
||||
stale-pr-message: "This PR is stale because it has been open for 120 days with no activity."
|
||||
close-pr-message: "This PR was closed because it has been inactive for 120 days since being marked as stale."
|
||||
operations-per-run: 0
|
||||
26
shadcn-admin/.gitignore
vendored
Normal file
26
shadcn-admin/.gitignore
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
.env
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
18
shadcn-admin/.prettierignore
Normal file
18
shadcn-admin/.prettierignore
Normal file
@@ -0,0 +1,18 @@
|
||||
# Ignore everything
|
||||
/*
|
||||
|
||||
# Except these files & folders
|
||||
!/src
|
||||
!index.html
|
||||
!package.json
|
||||
!tailwind.config.js
|
||||
!tsconfig.json
|
||||
!tsconfig.node.json
|
||||
!vite.config.ts
|
||||
!.prettierrc
|
||||
!README.md
|
||||
!eslint.config.js
|
||||
!postcss.config.js
|
||||
|
||||
# Ignore auto generated routeTree.gen.ts
|
||||
/src/routeTree.gen.ts
|
||||
50
shadcn-admin/.prettierrc
Normal file
50
shadcn-admin/.prettierrc
Normal file
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"arrowParens": "always",
|
||||
"semi": false,
|
||||
"tabWidth": 2,
|
||||
"printWidth": 80,
|
||||
"singleQuote": true,
|
||||
"jsxSingleQuote": true,
|
||||
"trailingComma": "es5",
|
||||
"bracketSpacing": true,
|
||||
"endOfLine": "lf",
|
||||
"plugins": [
|
||||
"@trivago/prettier-plugin-sort-imports",
|
||||
"prettier-plugin-tailwindcss"
|
||||
],
|
||||
"tailwindStylesheet": "./src/styles/index.css",
|
||||
"importOrder": [
|
||||
"^path$",
|
||||
"^vite$",
|
||||
"^@vitejs/(.*)$",
|
||||
"^react$",
|
||||
"^react-dom/client$",
|
||||
"^react/(.*)$",
|
||||
"^globals$",
|
||||
"^zod$",
|
||||
"^axios$",
|
||||
"^date-fns$",
|
||||
"^react-hook-form$",
|
||||
"^use-intl$",
|
||||
"^@radix-ui/(.*)$",
|
||||
"^@hookform/resolvers/zod$",
|
||||
"^@tanstack/react-query$",
|
||||
"^@tanstack/react-router$",
|
||||
"^@tanstack/react-table$",
|
||||
"<THIRD_PARTY_MODULES>",
|
||||
"^@/assets/(.*)",
|
||||
"^@/api/(.*)$",
|
||||
"^@/stores/(.*)$",
|
||||
"^@/lib/(.*)$",
|
||||
"^@/utils/(.*)$",
|
||||
"^@/constants/(.*)$",
|
||||
"^@/context/(.*)$",
|
||||
"^@/hooks/(.*)$",
|
||||
"^@/components/layouts/(.*)$",
|
||||
"^@/components/ui/(.*)$",
|
||||
"^@/components/errors/(.*)$",
|
||||
"^@/components/(.*)$",
|
||||
"^@/features/(.*)$",
|
||||
"^[./]"
|
||||
]
|
||||
}
|
||||
331
shadcn-admin/CHANGELOG.md
Normal file
331
shadcn-admin/CHANGELOG.md
Normal file
@@ -0,0 +1,331 @@
|
||||
## v2.2.1 (2025-11-06)
|
||||
|
||||
### Fix
|
||||
|
||||
- **style**: update data attribute class in authenticated layout (#249)
|
||||
- prevent navigation to 500 page during development (#240)
|
||||
- **style**: apply variant 'destructive' to sign-out buttons (#236)
|
||||
- add missing space in profile form (#235)
|
||||
|
||||
### Refactor
|
||||
|
||||
- enhance tables and update table layout (#234)
|
||||
|
||||
## v2.2.0 (2025-10-09)
|
||||
|
||||
### Feat
|
||||
|
||||
- add analytics tab in dashboard page (#220)
|
||||
- add extra AppTitle component for sidebar header (#216)
|
||||
- update 2-column sign in page (#213)
|
||||
|
||||
### Fix
|
||||
|
||||
- update sidebar menu chevron direction in RTL mode (#229)
|
||||
- pagination button spacing (#215)
|
||||
- upgrade lucide-react to solve antivirus warning (#211)
|
||||
|
||||
### Refactor
|
||||
|
||||
- move sidebar related components into app-sidebar
|
||||
- change SidebarInset component from 'main' to 'div'
|
||||
- replace extra main container query with content container query
|
||||
- replace inline svg logo with logo component (#214)
|
||||
|
||||
## v2.1.0 (2025-08-23)
|
||||
|
||||
### Feat
|
||||
|
||||
- enhance data table pagination with page numbers (#207)
|
||||
- enhance auth flow with sign-out dialogs and redirect functionality (#206)
|
||||
|
||||
### Refactor
|
||||
|
||||
- reorganize utility files into `lib/` folder (#209)
|
||||
- extract data-table components and reorganize structure (#208)
|
||||
|
||||
## v2.0.0 (2025-08-16)
|
||||
|
||||
### BREAKING CHANGE
|
||||
|
||||
- CSS file structure has been reorganized
|
||||
|
||||
### Feat
|
||||
|
||||
- add search param sync in apps route (#200)
|
||||
- improve tables and sync table states with search param (#199)
|
||||
- add data table bulk action toolbar (#196)
|
||||
- add config drawer and update overall layout (#186)
|
||||
- RTL support (#179)
|
||||
|
||||
### Fix
|
||||
|
||||
- adjust layout styles in search and top nav in dashboard page
|
||||
- update spacing and layout styles
|
||||
- update faceted icon color
|
||||
- improve user table hover & selected styles (#195)
|
||||
- add max-width for large screens to improve responsiveness (#194)
|
||||
- adjust chat border radius for better responsiveness (#193)
|
||||
- update hard-coded or inconsistent colors (#191)
|
||||
- use variable for inset layout height calculation
|
||||
- faded-bottom overflow issue in inset layout
|
||||
- hide unnecessary configs on mobile (#189)
|
||||
- adjust file input text vertical alignment (#188)
|
||||
|
||||
### Refactor
|
||||
|
||||
- enforce consistency and code quality (#198)
|
||||
- improve code quality and consistency (#197)
|
||||
- update error routes (#192)
|
||||
- remove DirSwitch component and its usage in Tasks (#190)
|
||||
- standardize using cookie as persist state (#187)
|
||||
- separate CSS into modular theme and base styles (#185)
|
||||
- replace tabler icons with lucide icons (#183)
|
||||
|
||||
## v1.4.2 (2025-07-23)
|
||||
|
||||
### Fix
|
||||
|
||||
- remove unnecessary transitions in table (#176)
|
||||
- overflow background in tables (#175)
|
||||
|
||||
## v1.4.1 (2025-06-25)
|
||||
|
||||
### Fix
|
||||
|
||||
- user list overflow in chat (#160)
|
||||
- prevent showing collapsed menu on mobile (#155)
|
||||
- white background select dropdown in dark mode (#149)
|
||||
|
||||
### Refactor
|
||||
|
||||
- update font config guide in fonts.ts (#164)
|
||||
|
||||
## v1.4.0 (2025-05-25)
|
||||
|
||||
### Feat
|
||||
|
||||
- **clerk**: add Clerk for auth and protected route (#146)
|
||||
|
||||
### Fix
|
||||
|
||||
- add an indicator for nested pages in search (#147)
|
||||
- update faded-bottom color with css variable (#139)
|
||||
|
||||
## v1.3.0 (2025-04-16)
|
||||
|
||||
### Fix
|
||||
|
||||
- replace custom otp with input-otp component (#131)
|
||||
- disable layout animation on mobile (#130)
|
||||
- upgrade react-day-picker and update calendar component (#129)
|
||||
|
||||
### Others
|
||||
|
||||
- upgrade Tailwind CSS to v4 (#125)
|
||||
- upgrade dependencies (#128)
|
||||
- configure automatic code-splitting (#127)
|
||||
|
||||
## v1.2.0 (2025-04-12)
|
||||
|
||||
### Feat
|
||||
|
||||
- add loading indicator during page transitions (#119)
|
||||
- add light favicons and theme-based switching (#112)
|
||||
- add new chat dialog in chats page (#90)
|
||||
|
||||
### Fix
|
||||
|
||||
- add fallback font for fontFamily (#110)
|
||||
- broken focus behavior in add user dialog (#113)
|
||||
|
||||
## v1.1.0 (2025-01-30)
|
||||
|
||||
### Feat
|
||||
|
||||
- allow changing font family in setting
|
||||
|
||||
### Fix
|
||||
|
||||
- update sidebar color in dark mode for consistent look (#87)
|
||||
- use overflow-clip in table paginations (#86)
|
||||
- **style**: update global scrollbar style (#82)
|
||||
- toolbar filter placeholder typo in user table (#76)
|
||||
|
||||
## v1.0.3 (2024-12-28)
|
||||
|
||||
### Fix
|
||||
|
||||
- add gap between buttons in import task dialog (#70)
|
||||
- hide button sort if column cannot be hidden & update filterFn (#69)
|
||||
- nav links added in profile dropdown (#68)
|
||||
|
||||
### Refactor
|
||||
|
||||
- optimize states in users/tasks context (#71)
|
||||
|
||||
## v1.0.2 (2024-12-25)
|
||||
|
||||
### Fix
|
||||
|
||||
- update overall layout due to scroll-lock bug (#66)
|
||||
|
||||
### Refactor
|
||||
|
||||
- analyze and remove unused files/exports with knip (#67)
|
||||
|
||||
## v1.0.1 (2024-12-14)
|
||||
|
||||
### Fix
|
||||
|
||||
- merge two button components into one (#60)
|
||||
- loading all tabler-icon chunks in dev mode (#59)
|
||||
- display menu dropdown when sidebar collapsed (#58)
|
||||
- update spacing & alignment in dialogs/drawers
|
||||
- update border & transition of sticky columns in user table
|
||||
- update heading alignment to left in user dialogs
|
||||
- add height and scroll area in user mutation dialogs
|
||||
- update `/dashboard` route to just `/`
|
||||
- **build**: replace require with import in tailwind.config.js
|
||||
|
||||
### Refactor
|
||||
|
||||
- remove unnecessary layout-backup file
|
||||
|
||||
## v1.0.0 (2024-12-09)
|
||||
|
||||
### BREAKING CHANGE
|
||||
|
||||
- Restructured the entire folder
|
||||
hierarchy to adopt a feature-based structure. This
|
||||
change improves code modularity and maintainability
|
||||
but introduces breaking changes.
|
||||
|
||||
### Feat
|
||||
|
||||
- implement task dialogs
|
||||
- implement user invite dialog
|
||||
- implement users CRUD
|
||||
- implement global command/search
|
||||
- implement custom sidebar trigger
|
||||
- implement coming-soon page
|
||||
|
||||
### Fix
|
||||
|
||||
- uncontrolled issue in account setting
|
||||
- card layout issue in app integrations page
|
||||
- remove form reset logic from useEffect in task import
|
||||
- update JSX types due to react 19
|
||||
- prevent card stretch in filtered app layout
|
||||
- layout wrap issue in tasks page on mobile
|
||||
- update user column hover and selected colors
|
||||
- add setTimeout in user dialog closing
|
||||
- layout shift issue in dropdown modal
|
||||
- z-axis overflow issue in header
|
||||
- stretch search bar only in mobile
|
||||
- language dropdown issue in account setting
|
||||
- update overflow contents with scroll area
|
||||
|
||||
### Refactor
|
||||
|
||||
- update layouts and extract common layout
|
||||
- reorganize project to feature-based structure
|
||||
|
||||
## v1.0.0-beta.5 (2024-11-11)
|
||||
|
||||
### Feat
|
||||
|
||||
- add multiple language support (#37)
|
||||
|
||||
### Fix
|
||||
|
||||
- ensure site syncs with system theme changes (#49)
|
||||
- recent sales responsive on ipad view (#40)
|
||||
|
||||
## v1.0.0-beta.4 (2024-09-22)
|
||||
|
||||
### Feat
|
||||
|
||||
- upgrade theme button to theme dropdown (#33)
|
||||
- **a11y**: add "Skip to Main" button to improve keyboard navigation (#27)
|
||||
|
||||
### Fix
|
||||
|
||||
- optimize onComplete/onIncomplete invocation (#32)
|
||||
- solve asChild attribute issue in custom button (#31)
|
||||
- improve custom Button component (#28)
|
||||
|
||||
## v1.0.0-beta.3 (2024-08-25)
|
||||
|
||||
### Feat
|
||||
|
||||
- implement chat page (#21)
|
||||
- add 401 error page (#12)
|
||||
- implement apps page
|
||||
- add otp page
|
||||
|
||||
### Fix
|
||||
|
||||
- prevent focus zoom on mobile devices (#20)
|
||||
- resolve eslint script issue (#18)
|
||||
- **a11y**: update default aria-label of each pin-input
|
||||
- resolve OTP paste issue in multi-digit pin-input
|
||||
- update layouts and solve overflow issues (#11)
|
||||
- sync pin inputs programmatically
|
||||
|
||||
## v1.0.0-beta.2 (2024-03-18)
|
||||
|
||||
### Feat
|
||||
|
||||
- implement custom pin-input component (#2)
|
||||
|
||||
## v1.0.0-beta.1 (2024-02-08)
|
||||
|
||||
### Feat
|
||||
|
||||
- update theme-color meta tag when theme is updated
|
||||
- add coming soon page in broken pages
|
||||
- implement tasks table and page
|
||||
- add remaining settings pages
|
||||
- add example error page for settings
|
||||
- update general error page to be more flexible
|
||||
- implement settings layout and settings profile page
|
||||
- add error pages
|
||||
- add password-input custom component
|
||||
- add sign-up page
|
||||
- add forgot-password page
|
||||
- add box sign in page
|
||||
- add email + password sign in page
|
||||
- make sidebar responsive and accessible
|
||||
- add tailwind prettier plugin
|
||||
- make sidebar collapsed state in local storage
|
||||
- add check current active nav hook
|
||||
- add loader component ui
|
||||
- update dropdown nav by default if child is active
|
||||
- add main-panel in dashboard
|
||||
- **ui**: add dark mode
|
||||
- **ui**: implement side nav ui
|
||||
|
||||
### Fix
|
||||
|
||||
- update incorrect overflow side nav height
|
||||
- exclude shadcn components from linting and remove unused props
|
||||
- solve text overflow issue when nav text is long
|
||||
- replace nav with dropdown in mobile topnav
|
||||
- make sidebar scrollable when overflow
|
||||
- update nav link keys
|
||||
- **ui**: update label style
|
||||
|
||||
### Refactor
|
||||
|
||||
- move password-input component into custom component dir
|
||||
- add custom button component
|
||||
- extract redundant codes into layout component
|
||||
- update react-router to use new api for routing
|
||||
- update main panel layout
|
||||
- update major layouts and styling
|
||||
- update main panel to be responsive
|
||||
- update sidebar collapsed state to false in mobile
|
||||
- update sidebar logo and title
|
||||
- **ui**: remove unnecessary spacing
|
||||
- remove unused files
|
||||
21
shadcn-admin/LICENSE
Normal file
21
shadcn-admin/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 Sat Naing
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
119
shadcn-admin/README.md
Normal file
119
shadcn-admin/README.md
Normal file
@@ -0,0 +1,119 @@
|
||||
# Shadcn Admin Dashboard
|
||||
|
||||
Admin Dashboard UI crafted with Shadcn and Vite. Built with responsiveness and accessibility in mind.
|
||||
|
||||

|
||||
|
||||
[](https://go.clerk.com/GttUAaK)
|
||||
|
||||
I've been creating dashboard UIs at work and for my personal projects. I always wanted to make a reusable collection of dashboard UI for future projects; and here it is now. While I've created a few custom components, some of the code is directly adapted from ShadcnUI examples.
|
||||
|
||||
> This is not a starter project (template) though. I'll probably make one in the future.
|
||||
|
||||
## Features
|
||||
|
||||
- Light/dark mode
|
||||
- Responsive
|
||||
- Accessible
|
||||
- With built-in Sidebar component
|
||||
- Global search command
|
||||
- 10+ pages
|
||||
- Extra custom components
|
||||
- RTL support
|
||||
|
||||
<details>
|
||||
<summary>Customized Components (click to expand)</summary>
|
||||
|
||||
This project uses Shadcn UI components, but some have been slightly modified for better RTL (Right-to-Left) support and other improvements. These customized components differ from the original Shadcn UI versions.
|
||||
|
||||
If you want to update components using the Shadcn CLI (e.g., `npx shadcn@latest add <component>`), it's generally safe for non-customized components. For the listed customized ones, you may need to manually merge changes to preserve the project's modifications and avoid overwriting RTL support or other updates.
|
||||
|
||||
> If you don't require RTL support, you can safely update the 'RTL Updated Components' via the Shadcn CLI, as these changes are primarily for RTL compatibility. The 'Modified Components' may have other customizations to consider.
|
||||
|
||||
### Modified Components
|
||||
|
||||
- scroll-area
|
||||
- sonner
|
||||
- separator
|
||||
|
||||
### RTL Updated Components
|
||||
|
||||
- alert-dialog
|
||||
- calendar
|
||||
- command
|
||||
- dialog
|
||||
- dropdown-menu
|
||||
- select
|
||||
- table
|
||||
- sheet
|
||||
- sidebar
|
||||
- switch
|
||||
|
||||
**Notes:**
|
||||
|
||||
- **Modified Components**: These have general updates, potentially including RTL adjustments.
|
||||
- **RTL Updated Components**: These have specific changes for RTL language support (e.g., layout, positioning).
|
||||
- For implementation details, check the source files in `src/components/ui/`.
|
||||
- All other Shadcn UI components in the project are standard and can be safely updated via the CLI.
|
||||
|
||||
</details>
|
||||
|
||||
## Tech Stack
|
||||
|
||||
**UI:** [ShadcnUI](https://ui.shadcn.com) (TailwindCSS + RadixUI)
|
||||
|
||||
**Build Tool:** [Vite](https://vitejs.dev/)
|
||||
|
||||
**Routing:** [TanStack Router](https://tanstack.com/router/latest)
|
||||
|
||||
**Type Checking:** [TypeScript](https://www.typescriptlang.org/)
|
||||
|
||||
**Linting/Formatting:** [ESLint](https://eslint.org/) & [Prettier](https://prettier.io/)
|
||||
|
||||
**Icons:** [Lucide Icons](https://lucide.dev/icons/), [Tabler Icons](https://tabler.io/icons) (Brand icons only)
|
||||
|
||||
**Auth (partial):** [Clerk](https://go.clerk.com/GttUAaK)
|
||||
|
||||
## Run Locally
|
||||
|
||||
Clone the project
|
||||
|
||||
```bash
|
||||
git clone https://github.com/satnaing/shadcn-admin.git
|
||||
```
|
||||
|
||||
Go to the project directory
|
||||
|
||||
```bash
|
||||
cd shadcn-admin
|
||||
```
|
||||
|
||||
Install dependencies
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
Start the server
|
||||
|
||||
```bash
|
||||
pnpm run dev
|
||||
```
|
||||
|
||||
## Sponsoring this project ❤️
|
||||
|
||||
If you find this project helpful or use this in your own work, consider [sponsoring me](https://github.com/sponsors/satnaing) to support development and maintenance. You can [buy me a coffee](https://buymeacoffee.com/satnaing) as well. Don’t worry, every penny helps. Thank you! 🙏
|
||||
|
||||
For questions or sponsorship inquiries, feel free to reach out at [satnaingdev@gmail.com](mailto:satnaingdev@gmail.com).
|
||||
|
||||
### Current Sponsor
|
||||
|
||||
- [Clerk](https://go.clerk.com/GttUAaK) - authentication and user management for the modern web
|
||||
|
||||
## Author
|
||||
|
||||
Crafted with 🤍 by [@satnaing](https://github.com/satnaing)
|
||||
|
||||
## License
|
||||
|
||||
Licensed under the [MIT License](https://choosealicense.com/licenses/mit/)
|
||||
21
shadcn-admin/components.json
Normal file
21
shadcn-admin/components.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "new-york",
|
||||
"rsc": false,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"css": "src/styles/index.css",
|
||||
"baseColor": "slate",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
},
|
||||
"iconLibrary": "lucide"
|
||||
}
|
||||
7
shadcn-admin/cz.yaml
Normal file
7
shadcn-admin/cz.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
commitizen:
|
||||
name: cz_conventional_commits
|
||||
tag_format: v$version
|
||||
update_changelog_on_bump: true
|
||||
version_provider: npm
|
||||
version_scheme: semver
|
||||
59
shadcn-admin/eslint.config.js
Normal file
59
shadcn-admin/eslint.config.js
Normal file
@@ -0,0 +1,59 @@
|
||||
import globals from 'globals'
|
||||
import js from '@eslint/js'
|
||||
import pluginQuery from '@tanstack/eslint-plugin-query'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
import { defineConfig } from 'eslint/config'
|
||||
import tseslint from 'typescript-eslint'
|
||||
|
||||
export default defineConfig(
|
||||
{ ignores: ['dist', 'src/components/ui'] },
|
||||
{
|
||||
extends: [
|
||||
js.configs.recommended,
|
||||
...tseslint.configs.recommended,
|
||||
...pluginQuery.configs['flat/recommended'],
|
||||
],
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
},
|
||||
plugins: {
|
||||
'react-hooks': reactHooks,
|
||||
'react-refresh': reactRefresh,
|
||||
},
|
||||
rules: {
|
||||
...reactHooks.configs.recommended.rules,
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
'no-console': 'error',
|
||||
'no-unused-vars': 'off',
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'error',
|
||||
{
|
||||
args: 'all',
|
||||
argsIgnorePattern: '^_',
|
||||
caughtErrors: 'all',
|
||||
caughtErrorsIgnorePattern: '^_',
|
||||
destructuredArrayIgnorePattern: '^_',
|
||||
varsIgnorePattern: '^_',
|
||||
ignoreRestSiblings: true,
|
||||
},
|
||||
],
|
||||
// Enforce type-only imports for TypeScript types
|
||||
'@typescript-eslint/consistent-type-imports': [
|
||||
'error',
|
||||
{
|
||||
prefer: 'type-imports',
|
||||
fixStyle: 'inline-type-imports',
|
||||
disallowTypeAnnotations: false,
|
||||
},
|
||||
],
|
||||
// Prevent duplicate imports from the same module
|
||||
'no-duplicate-imports': 'error',
|
||||
},
|
||||
}
|
||||
)
|
||||
80
shadcn-admin/index.html
Normal file
80
shadcn-admin/index.html
Normal file
@@ -0,0 +1,80 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/svg+xml"
|
||||
href="/images/favicon.svg"
|
||||
media="(prefers-color-scheme: light)"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/svg+xml"
|
||||
href="/images/favicon_light.svg"
|
||||
media="(prefers-color-scheme: dark)"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
href="/images/favicon.png"
|
||||
media="(prefers-color-scheme: light)"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
href="/images/favicon_light.png"
|
||||
media="(prefers-color-scheme: dark)"
|
||||
/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
|
||||
<!-- Primary Meta Tags -->
|
||||
<title>Shadcn Admin</title>
|
||||
<meta name="title" content="Shadcn Admin" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Admin Dashboard UI built with Shadcn and Vite."
|
||||
/>
|
||||
|
||||
<!-- Open Graph / Facebook -->
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:url" content="https://shadcn-admin.netlify.app" />
|
||||
<meta property="og:title" content="Shadcn Admin" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="Admin Dashboard UI built with Shadcn and Vite."
|
||||
/>
|
||||
<meta
|
||||
property="og:image"
|
||||
content="https://shadcn-admin.netlify.app/images/shadcn-admin.png"
|
||||
/>
|
||||
|
||||
<!-- Twitter -->
|
||||
<meta property="twitter:card" content="summary_large_image" />
|
||||
<meta property="twitter:url" content="https://shadcn-admin.netlify.app" />
|
||||
<meta property="twitter:title" content="Shadcn Admin" />
|
||||
<meta
|
||||
property="twitter:description"
|
||||
content="Admin Dashboard UI built with Shadcn and Vite."
|
||||
/>
|
||||
<meta
|
||||
property="twitter:image"
|
||||
content="https://shadcn-admin.netlify.app/images/shadcn-admin.png"
|
||||
/>
|
||||
|
||||
<!-- font family -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&family=Manrope:wght@200..800&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
|
||||
<meta name="theme-color" content="#fff" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
8
shadcn-admin/knip.config.ts
Normal file
8
shadcn-admin/knip.config.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import type { KnipConfig } from 'knip';
|
||||
|
||||
const config: KnipConfig = {
|
||||
ignore: ['src/components/ui/**', 'src/routeTree.gen.ts'],
|
||||
ignoreDependencies: ["tailwindcss", "tw-animate-css"]
|
||||
};
|
||||
|
||||
export default config;
|
||||
4
shadcn-admin/netlify.toml
Normal file
4
shadcn-admin/netlify.toml
Normal file
@@ -0,0 +1,4 @@
|
||||
[[redirects]]
|
||||
from = "/*"
|
||||
to = "/index.html"
|
||||
status = 200
|
||||
84
shadcn-admin/package.json
Normal file
84
shadcn-admin/package.json
Normal file
@@ -0,0 +1,84 @@
|
||||
{
|
||||
"name": "shadcn-admin",
|
||||
"private": false,
|
||||
"version": "2.2.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview",
|
||||
"format:check": "prettier --check .",
|
||||
"format": "prettier --write .",
|
||||
"knip": "knip"
|
||||
},
|
||||
"dependencies": {
|
||||
"@clerk/clerk-react": "^5.58.1",
|
||||
"@hookform/resolvers": "^5.2.2",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.15",
|
||||
"@radix-ui/react-avatar": "^1.1.11",
|
||||
"@radix-ui/react-checkbox": "^1.3.3",
|
||||
"@radix-ui/react-collapsible": "^1.1.12",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-direction": "^1.1.1",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||
"@radix-ui/react-icons": "^1.3.2",
|
||||
"@radix-ui/react-label": "^2.1.8",
|
||||
"@radix-ui/react-popover": "^1.1.15",
|
||||
"@radix-ui/react-radio-group": "^1.3.8",
|
||||
"@radix-ui/react-scroll-area": "^1.2.10",
|
||||
"@radix-ui/react-select": "^2.2.6",
|
||||
"@radix-ui/react-separator": "^1.1.8",
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
"@radix-ui/react-switch": "^1.2.6",
|
||||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@supabase/supabase-js": "^2.93.3",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@tanstack/react-query": "^5.90.12",
|
||||
"@tanstack/react-router": "^1.141.2",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"axios": "^1.13.2",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "1.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"input-otp": "^1.4.2",
|
||||
"lucide-react": "^0.561.0",
|
||||
"react": "^19.2.3",
|
||||
"react-day-picker": "9.12.0",
|
||||
"react-dom": "^19.2.3",
|
||||
"react-hook-form": "^7.68.0",
|
||||
"react-top-loading-bar": "^3.0.2",
|
||||
"recharts": "^3.6.0",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"zod": "^4.2.0",
|
||||
"zustand": "^5.0.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.2",
|
||||
"@faker-js/faker": "^10.1.0",
|
||||
"@tanstack/eslint-plugin-query": "^5.91.2",
|
||||
"@tanstack/react-query-devtools": "^5.91.1",
|
||||
"@tanstack/react-router-devtools": "^1.141.2",
|
||||
"@tanstack/router-plugin": "^1.141.2",
|
||||
"@trivago/prettier-plugin-sort-imports": "^6.0.0",
|
||||
"@types/node": "^25.0.2",
|
||||
"@types/react": "^19.2.7",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react-swc": "^4.2.2",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"eslint-plugin-react-refresh": "^0.4.25",
|
||||
"globals": "^16.5.0",
|
||||
"knip": "^5.73.4",
|
||||
"prettier": "^3.7.4",
|
||||
"prettier-plugin-tailwindcss": "^0.7.2",
|
||||
"typescript": "~5.9.3",
|
||||
"typescript-eslint": "^8.49.0",
|
||||
"vite": "^7.3.0"
|
||||
}
|
||||
}
|
||||
6151
shadcn-admin/pnpm-lock.yaml
generated
Normal file
6151
shadcn-admin/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
BIN
shadcn-admin/public/images/favicon.png
Normal file
BIN
shadcn-admin/public/images/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 494 B |
4
shadcn-admin/public/images/favicon.svg
Normal file
4
shadcn-admin/public/images/favicon.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.com/svgjs" width="24" height="24"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokewidth="2" strokelinecap="round" strokelinejoin="round">
|
||||
<path d="M15 6v12a3 3 0 1 0 3-3H6a3 3 0 1 0 3 3V6a3 3 0 1 0-3 3h12a3 3 0 1 0-3-3"></path>
|
||||
</svg><style>@media (prefers-color-scheme: light) { :root { filter: contrast(1) brightness(0.8); } }
|
||||
</style></svg>
|
||||
|
After Width: | Height: | Size: 520 B |
BIN
shadcn-admin/public/images/favicon_light.png
Normal file
BIN
shadcn-admin/public/images/favicon_light.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 247 B |
1
shadcn-admin/public/images/favicon_light.svg
Normal file
1
shadcn-admin/public/images/favicon_light.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="24" height="24" viewBox="948,463.5,24,24"><defs><clipPath id="clip-1"><rect x="948" y="463.5" width="24" height="24" id="clip-1" stroke="none"/></clipPath></defs><g fill="none" fill-rule="nonzero" stroke="none" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="10" stroke-dasharray="" stroke-dashoffset="0" font-family="none" font-weight="none" font-size="none" text-anchor="none" style="mix-blend-mode: normal"><g><g id="Group 1"><g clip-path="url(#clip-1)" id="Group 1"><path d="M963,469.5v12c0,1.65685 1.34315,3 3,3c1.65685,0 3,-1.34315 3,-3c0,-1.65685 -1.34315,-3 -3,-3h-12c-1.65685,0 -3,1.34315 -3,3c0,1.65685 1.34315,3 3,3c1.65685,0 3,-1.34315 3,-3v-12c0,-1.65685 -1.34315,-3 -3,-3c-1.65685,0 -3,1.34315 -3,3c0,1.65685 1.34315,3 3,3h12c1.65685,0 3,-1.34315 3,-3c0,-1.65685 -1.34315,-3 -3,-3c-1.65685,0 -3,1.34315 -3,3" id="Path 1" stroke="#f2f2f2"/></g></g></g></g></svg>
|
||||
|
After Width: | Height: | Size: 1013 B |
BIN
shadcn-admin/public/images/shadcn-admin.png
Normal file
BIN
shadcn-admin/public/images/shadcn-admin.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 273 KiB |
28
shadcn-admin/src/assets/brand-icons/icon-discord.tsx
Normal file
28
shadcn-admin/src/assets/brand-icons/icon-discord.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import { type SVGProps } from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export function IconDiscord({ className, ...props }: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
role='img'
|
||||
viewBox='0 0 24 24'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
width='24'
|
||||
height='24'
|
||||
className={cn('[&>path]:stroke-current', className)}
|
||||
fill='none'
|
||||
stroke='currentColor'
|
||||
strokeWidth='2'
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
{...props}
|
||||
>
|
||||
<title>Discord</title>
|
||||
<path strokeWidth='0' d='M0 0h24v24H0z' fill='none' />
|
||||
<path d='M8 12a1 1 0 1 0 2 0a1 1 0 0 0 -2 0' />
|
||||
<path d='M14 12a1 1 0 1 0 2 0a1 1 0 0 0 -2 0' />
|
||||
<path d='M15.5 17c0 1 1.5 3 2 3c1.5 0 2.833 -1.667 3.5 -3c.667 -1.667 .5 -5.833 -1.5 -11.5c-1.457 -1.015 -3 -1.34 -4.5 -1.5l-.972 1.923a11.913 11.913 0 0 0 -4.053 0l-.975 -1.923c-1.5 .16 -3.043 .485 -4.5 1.5c-2 5.667 -2.167 9.833 -1.5 11.5c.667 1.333 2 3 3.5 3c.5 0 2 -2 2 -3' />
|
||||
<path d='M7 16.5c3.5 1 6.5 1 10 0' />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
33
shadcn-admin/src/assets/brand-icons/icon-docker.tsx
Normal file
33
shadcn-admin/src/assets/brand-icons/icon-docker.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import { type SVGProps } from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export function IconDocker({ className, ...props }: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
role='img'
|
||||
viewBox='0 0 24 24'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
width='24'
|
||||
height='24'
|
||||
className={cn('[&>path]:stroke-current', className)}
|
||||
fill='none'
|
||||
stroke='currentColor'
|
||||
strokeWidth='2'
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
{...props}
|
||||
>
|
||||
<title>Docker</title>
|
||||
<path strokeWidth='0' d='M0 0h24v24H0z' fill='none' />
|
||||
<path d='M22 12.54c-1.804 -.345 -2.701 -1.08 -3.523 -2.94c-.487 .696 -1.102 1.568 -.92 2.4c.028 .238 -.32 1 -.557 1h-14c0 5.208 3.164 7 6.196 7c4.124 .022 7.828 -1.376 9.854 -5c1.146 -.101 2.296 -1.505 2.95 -2.46z' />
|
||||
<path d='M5 10h3v3h-3z' />
|
||||
<path d='M8 10h3v3h-3z' />
|
||||
<path d='M11 10h3v3h-3z' />
|
||||
<path d='M8 7h3v3h-3z' />
|
||||
<path d='M11 7h3v3h-3z' />
|
||||
<path d='M11 4h3v3h-3z' />
|
||||
<path d='M4.571 18c1.5 0 2.047 -.074 2.958 -.78' />
|
||||
<path d='M10 16l0 .01' />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
25
shadcn-admin/src/assets/brand-icons/icon-facebook.tsx
Normal file
25
shadcn-admin/src/assets/brand-icons/icon-facebook.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import { type SVGProps } from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export function IconFacebook({ className, ...props }: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
role='img'
|
||||
viewBox='0 0 24 24'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
width='24'
|
||||
height='24'
|
||||
className={cn('[&>path]:stroke-current', className)}
|
||||
fill='none'
|
||||
stroke='currentColor'
|
||||
strokeWidth='2'
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
{...props}
|
||||
>
|
||||
<title>Facebook</title>
|
||||
<path strokeWidth='0' d='M0 0h24v24H0z' fill='none' />
|
||||
<path d='M7 10v4h3v7h4v-7h3l1 -4h-4v-2a1 1 0 0 1 1 -1h3v-4h-3a5 5 0 0 0 -5 5v2h-3' />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
27
shadcn-admin/src/assets/brand-icons/icon-figma.tsx
Normal file
27
shadcn-admin/src/assets/brand-icons/icon-figma.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import { type SVGProps } from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export function IconFigma({ className, ...props }: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
role='img'
|
||||
viewBox='0 0 24 24'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
width='24'
|
||||
height='24'
|
||||
className={cn('[&>path]:stroke-current', className)}
|
||||
fill='none'
|
||||
stroke='currentColor'
|
||||
strokeWidth='2'
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
{...props}
|
||||
>
|
||||
<title>Figma</title>
|
||||
<path strokeWidth='0' d='M0 0h24v24H0z' fill='none' />
|
||||
<path d='M15 12m-3 0a3 3 0 1 0 6 0a3 3 0 1 0 -6 0' />
|
||||
<path d='M6 3m0 3a3 3 0 0 1 3 -3h6a3 3 0 0 1 3 3v0a3 3 0 0 1 -3 3h-6a3 3 0 0 1 -3 -3z' />
|
||||
<path d='M9 9a3 3 0 0 0 0 6h3m-3 0a3 3 0 1 0 3 3v-15' />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
25
shadcn-admin/src/assets/brand-icons/icon-github.tsx
Normal file
25
shadcn-admin/src/assets/brand-icons/icon-github.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import { type SVGProps } from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export function IconGithub({ className, ...props }: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
role='img'
|
||||
viewBox='0 0 24 24'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
width='24'
|
||||
height='24'
|
||||
className={cn('[&>path]:stroke-current', className)}
|
||||
fill='none'
|
||||
stroke='currentColor'
|
||||
strokeWidth='2'
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
{...props}
|
||||
>
|
||||
<title>GitHub</title>
|
||||
<path strokeWidth='0' d='M0 0h24v24H0z' fill='none' />
|
||||
<path d='M9 19c-4.3 1.4 -4.3 -2.5 -6 -3m12 5v-3.5c0 -1 .1 -1.4 -.5 -2c2.8 -.3 5.5 -1.4 5.5 -6a4.6 4.6 0 0 0 -1.3 -3.2a4.2 4.2 0 0 0 -.1 -3.2s-1.1 -.3 -3.5 1.3a12.3 12.3 0 0 0 -6.2 0c-2.4 -1.6 -3.5 -1.3 -3.5 -1.3a4.2 4.2 0 0 0 -.1 3.2a4.6 4.6 0 0 0 -1.3 3.2c0 4.6 2.7 5.7 5.5 6c-.6 .6 -.6 1.2 -.5 2v3.5' />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
25
shadcn-admin/src/assets/brand-icons/icon-gitlab.tsx
Normal file
25
shadcn-admin/src/assets/brand-icons/icon-gitlab.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import { type SVGProps } from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export function IconGitlab({ className, ...props }: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
role='img'
|
||||
viewBox='0 0 24 24'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
width='24'
|
||||
height='24'
|
||||
className={cn('[&>path]:stroke-current', className)}
|
||||
fill='none'
|
||||
stroke='currentColor'
|
||||
strokeWidth='2'
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
{...props}
|
||||
>
|
||||
<title>GitLab</title>
|
||||
<path strokeWidth='0' d='M0 0h24v24H0z' fill='none' />
|
||||
<path d='M21 14l-9 7l-9 -7l3 -11l3 7h6l3 -7z' />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
28
shadcn-admin/src/assets/brand-icons/icon-gmail.tsx
Normal file
28
shadcn-admin/src/assets/brand-icons/icon-gmail.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import { type SVGProps } from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export function IconGmail({ className, ...props }: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
role='img'
|
||||
viewBox='0 0 24 24'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
width='24'
|
||||
height='24'
|
||||
className={cn('[&>path]:stroke-current', className)}
|
||||
fill='none'
|
||||
stroke='currentColor'
|
||||
strokeWidth='2'
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
{...props}
|
||||
>
|
||||
<title>Gmail</title>
|
||||
<path strokeWidth='0' d='M0 0h24v24H0z' fill='none' />
|
||||
<path d='M16 20h3a1 1 0 0 0 1 -1v-14a1 1 0 0 0 -1 -1h-3v16z' />
|
||||
<path d='M5 20h3v-16h-3a1 1 0 0 0 -1 1v14a1 1 0 0 0 1 1z' />
|
||||
<path d='M16 4l-4 4l-4 -4' />
|
||||
<path d='M4 6.5l8 7.5l8 -7.5' />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
30
shadcn-admin/src/assets/brand-icons/icon-medium.tsx
Normal file
30
shadcn-admin/src/assets/brand-icons/icon-medium.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import { type SVGProps } from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export function IconMedium({ className, ...props }: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
role='img'
|
||||
viewBox='0 0 24 24'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
width='24'
|
||||
height='24'
|
||||
className={cn('[&>path]:stroke-current', className)}
|
||||
fill='none'
|
||||
stroke='currentColor'
|
||||
strokeWidth='2'
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
{...props}
|
||||
>
|
||||
<title>Medium</title>
|
||||
<path strokeWidth='0' d='M0 0h24v24H0z' fill='none' />
|
||||
<path d='M4 4m0 2a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2z' />
|
||||
<path d='M8 9h1l3 3l3 -3h1' />
|
||||
<path d='M8 15l2 0' />
|
||||
<path d='M14 15l2 0' />
|
||||
<path d='M9 9l0 6' />
|
||||
<path d='M15 9l0 6' />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
28
shadcn-admin/src/assets/brand-icons/icon-notion.tsx
Normal file
28
shadcn-admin/src/assets/brand-icons/icon-notion.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import { type SVGProps } from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export function IconNotion({ className, ...props }: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
role='img'
|
||||
viewBox='0 0 24 24'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
width='24'
|
||||
height='24'
|
||||
className={cn('[&>path]:stroke-current', className)}
|
||||
fill='none'
|
||||
stroke='currentColor'
|
||||
strokeWidth='2'
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
{...props}
|
||||
>
|
||||
<title>Notion</title>
|
||||
<path strokeWidth='0' d='M0 0h24v24H0z' fill='none' />
|
||||
<path d='M11 17.5v-6.5h.5l4 6h.5v-6.5' />
|
||||
<path d='M19.077 20.071l-11.53 .887a1 1 0 0 1 -.876 -.397l-2.471 -3.294a1 1 0 0 1 -.2 -.6v-10.741a1 1 0 0 1 .923 -.997l11.389 -.876a2 2 0 0 1 1.262 .33l1.535 1.023a2 2 0 0 1 .891 1.664v12.004a1 1 0 0 1 -.923 .997z' />
|
||||
<path d='M4.5 5.5l2.5 2.5' />
|
||||
<path d='M20 7l-13 1v12.5' />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
26
shadcn-admin/src/assets/brand-icons/icon-skype.tsx
Normal file
26
shadcn-admin/src/assets/brand-icons/icon-skype.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import { type SVGProps } from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export function IconSkype({ className, ...props }: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
role='img'
|
||||
viewBox='0 0 24 24'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
width='24'
|
||||
height='24'
|
||||
className={cn('[&>path]:stroke-current', className)}
|
||||
fill='none'
|
||||
stroke='currentColor'
|
||||
strokeWidth='2'
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
{...props}
|
||||
>
|
||||
<title>Skype</title>
|
||||
<path strokeWidth='0' d='M0 0h24v24H0z' fill='none' />
|
||||
<path d='M12 3a9 9 0 0 1 8.603 11.65a4.5 4.5 0 0 1 -5.953 5.953a9 9 0 0 1 -11.253 -11.253a4.5 4.5 0 0 1 5.953 -5.954a8.987 8.987 0 0 1 2.65 -.396z' />
|
||||
<path d='M8 14.5c.5 2 2.358 2.5 4 2.5c2.905 0 4 -1.187 4 -2.5c0 -1.503 -1.927 -2.5 -4 -2.5s-4 -1 -4 -2.5c0 -1.313 1.095 -2.5 4 -2.5c1.642 0 3.5 .5 4 2.5' />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
28
shadcn-admin/src/assets/brand-icons/icon-slack.tsx
Normal file
28
shadcn-admin/src/assets/brand-icons/icon-slack.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import { type SVGProps } from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export function IconSlack({ className, ...props }: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
role='img'
|
||||
viewBox='0 0 24 24'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
width='24'
|
||||
height='24'
|
||||
className={cn('[&>path]:stroke-current', className)}
|
||||
fill='none'
|
||||
stroke='currentColor'
|
||||
strokeWidth='2'
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
{...props}
|
||||
>
|
||||
<title>Slack</title>
|
||||
<path strokeWidth='0' d='M0 0h24v24H0z' fill='none' />
|
||||
<path d='M12 12v-6a2 2 0 0 1 4 0v6m0 -2a2 2 0 1 1 2 2h-6' />
|
||||
<path d='M12 12h6a2 2 0 0 1 0 4h-6m2 0a2 2 0 1 1 -2 2v-6' />
|
||||
<path d='M12 12v6a2 2 0 0 1 -4 0v-6m0 2a2 2 0 1 1 -2 -2h6' />
|
||||
<path d='M12 12h-6a2 2 0 0 1 0 -4h6m-2 0a2 2 0 1 1 2 -2v6' />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
25
shadcn-admin/src/assets/brand-icons/icon-stripe.tsx
Normal file
25
shadcn-admin/src/assets/brand-icons/icon-stripe.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import { type SVGProps } from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export function IconStripe({ className, ...props }: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
role='img'
|
||||
viewBox='0 0 24 24'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
width='24'
|
||||
height='24'
|
||||
className={cn('[&>path]:stroke-current', className)}
|
||||
fill='none'
|
||||
stroke='currentColor'
|
||||
strokeWidth='2'
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
{...props}
|
||||
>
|
||||
<title>Stripe</title>
|
||||
<path strokeWidth='0' d='M0 0h24v24H0z' fill='none' />
|
||||
<path d='M11.453 8.056c0 -.623 .518 -.979 1.442 -.979c1.69 0 3.41 .343 4.605 .923l.5 -4c-.948 -.449 -2.82 -1 -5.5 -1c-1.895 0 -3.373 .087 -4.5 1c-1.172 .956 -2 2.33 -2 4c0 3.03 1.958 4.906 5 6c1.961 .69 3 .743 3 1.5c0 .735 -.851 1.5 -2 1.5c-1.423 0 -3.963 -.609 -5.5 -1.5l-.5 4c1.321 .734 3.474 1.5 6 1.5c2 0 3.957 -.468 5.084 -1.36c1.263 -.979 1.916 -2.268 1.916 -4.14c0 -3.096 -1.915 -4.547 -5 -5.637c-1.646 -.605 -2.544 -1.07 -2.544 -1.807z' />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
25
shadcn-admin/src/assets/brand-icons/icon-telegram.tsx
Normal file
25
shadcn-admin/src/assets/brand-icons/icon-telegram.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import { type SVGProps } from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export function IconTelegram({ className, ...props }: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
role='img'
|
||||
viewBox='0 0 24 24'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
width='24'
|
||||
height='24'
|
||||
className={cn('[&>path]:stroke-current', className)}
|
||||
fill='none'
|
||||
stroke='currentColor'
|
||||
strokeWidth='2'
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
{...props}
|
||||
>
|
||||
<title>Telegram</title>
|
||||
<path strokeWidth='0' d='M0 0h24v24H0z' fill='none' />
|
||||
<path d='M15 10l-4 4l6 6l4 -16l-18 7l4 2l2 6l3 -4' />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
27
shadcn-admin/src/assets/brand-icons/icon-trello.tsx
Normal file
27
shadcn-admin/src/assets/brand-icons/icon-trello.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import { type SVGProps } from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export function IconTrello({ className, ...props }: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
role='img'
|
||||
viewBox='0 0 24 24'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
width='24'
|
||||
height='24'
|
||||
className={cn('[&>path]:stroke-current', className)}
|
||||
fill='none'
|
||||
stroke='currentColor'
|
||||
strokeWidth='2'
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
{...props}
|
||||
>
|
||||
<title>Trello</title>
|
||||
<path strokeWidth='0' d='M0 0h24v24H0z' fill='none' />
|
||||
<path d='M4 4m0 2a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2z' />
|
||||
<path d='M7 7h3v10h-3z' />
|
||||
<path d='M14 7h3v6h-3z' />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
26
shadcn-admin/src/assets/brand-icons/icon-whatsapp.tsx
Normal file
26
shadcn-admin/src/assets/brand-icons/icon-whatsapp.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import { type SVGProps } from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export function IconWhatsapp({ className, ...props }: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
role='img'
|
||||
viewBox='0 0 24 24'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
width='24'
|
||||
height='24'
|
||||
className={cn('[&>path]:stroke-current', className)}
|
||||
fill='none'
|
||||
stroke='currentColor'
|
||||
strokeWidth='2'
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
{...props}
|
||||
>
|
||||
<title>WhatsApp</title>
|
||||
<path strokeWidth='0' d='M0 0h24v24H0z' fill='none' />
|
||||
<path d='M3 21l1.65 -3.8a9 9 0 1 1 3.4 2.9l-5.05 .9' />
|
||||
<path d='M9 10a.5 .5 0 0 0 1 0v-1a.5 .5 0 0 0 -1 0v1a5 5 0 0 0 5 5h1a.5 .5 0 0 0 0 -1h-1a.5 .5 0 0 0 0 1' />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
26
shadcn-admin/src/assets/brand-icons/icon-zoom.tsx
Normal file
26
shadcn-admin/src/assets/brand-icons/icon-zoom.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import { type SVGProps } from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export function IconZoom({ className, ...props }: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
role='img'
|
||||
viewBox='0 0 24 24'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
width='24'
|
||||
height='24'
|
||||
className={cn('[&>path]:stroke-current', className)}
|
||||
fill='none'
|
||||
stroke='currentColor'
|
||||
strokeWidth='2'
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
{...props}
|
||||
>
|
||||
<title>Zoom</title>
|
||||
<path strokeWidth='0' d='M0 0h24v24H0z' fill='none' />
|
||||
<path d='M17.011 9.385v5.128l3.989 3.487v-12z' />
|
||||
<path d='M3.887 6h10.08c1.468 0 3.033 1.203 3.033 2.803v8.196a.991 .991 0 0 1 -.975 1h-10.373c-1.667 0 -2.652 -1.5 -2.652 -3l.01 -8a.882 .882 0 0 1 .208 -.71a.841 .841 0 0 1 .67 -.287z' />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
16
shadcn-admin/src/assets/brand-icons/index.ts
Normal file
16
shadcn-admin/src/assets/brand-icons/index.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export { IconDiscord } from './icon-discord'
|
||||
export { IconDocker } from './icon-docker'
|
||||
export { IconFacebook } from './icon-facebook'
|
||||
export { IconFigma } from './icon-figma'
|
||||
export { IconGithub } from './icon-github'
|
||||
export { IconGitlab } from './icon-gitlab'
|
||||
export { IconGmail } from './icon-gmail'
|
||||
export { IconMedium } from './icon-medium'
|
||||
export { IconNotion } from './icon-notion'
|
||||
export { IconSkype } from './icon-skype'
|
||||
export { IconSlack } from './icon-slack'
|
||||
export { IconStripe } from './icon-stripe'
|
||||
export { IconTelegram } from './icon-telegram'
|
||||
export { IconTrello } from './icon-trello'
|
||||
export { IconWhatsapp } from './icon-whatsapp'
|
||||
export { IconZoom } from './icon-zoom'
|
||||
41
shadcn-admin/src/assets/clerk-full-logo.tsx
Normal file
41
shadcn-admin/src/assets/clerk-full-logo.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import { type SVGProps } from 'react'
|
||||
|
||||
export function ClerkFullLogo(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
width={77}
|
||||
height={24}
|
||||
viewBox='0 0 77 24'
|
||||
fill='none'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d='M35.148 16.738a4.198 4.198 0 01-3.06 1.283 3.53 3.53 0 01-2.604-1.034c-.619-.645-.975-1.566-.975-2.665 0-2.199 1.432-3.703 3.58-3.703a3.914 3.914 0 013.034 1.377l1.859-1.644c-1.211-1.47-3.176-2.229-5.042-2.229-3.652 0-6.24 2.517-6.24 6.22 0 1.831.643 3.374 1.728 4.463s2.631 1.728 4.415 1.728c2.317 0 4.166-.94 5.203-2.122l-1.898-1.674zM38.727 3.428h2.766V20.34h-2.766V3.428zM54.818 15.283c.046-.368.07-.74.076-1.11 0-3.507-2.296-6.047-5.847-6.047a5.738 5.738 0 00-4.215 1.725c-1.038 1.089-1.66 2.631-1.66 4.47 0 3.749 2.642 6.216 6.146 6.216 2.35 0 4.043-.951 5.058-2.242l-1.812-1.605-.09-.076a3.749 3.749 0 01-3.008 1.406c-1.778 0-3.061-1.037-3.427-2.737h8.779zm-8.733-2.22a3.365 3.365 0 01.737-1.449 3.082 3.082 0 012.368-.996c1.58 0 2.57.988 2.911 2.445h-6.016zM63.445 8.09v3.084a13.36 13.36 0 00-.838-.05c-2.094 0-3.282 1.505-3.282 3.479v5.736h-2.763V8.261h2.763v1.83h.025c.938-1.283 2.284-1.997 3.75-1.997l.345-.004zM69.887 15.281l-1.998 2.222v2.837h-2.764V3.428h2.764v10.374L72.822 8.3h3.283l-4.341 4.86 4.417 7.18h-3.11l-3.133-5.059h-.051z'
|
||||
fill='#1F0256'
|
||||
/>
|
||||
<path
|
||||
d='M19.116 3.16l-2.88 2.881a.571.571 0 01-.701.084 6.854 6.854 0 00-10.39 5.647 6.867 6.867 0 00.979 3.764.571.571 0 01-.084.699l-2.88 2.88a.57.57 0 01-.865-.063A11.994 11.994 0 0119.051 2.295a.57.57 0 01.065.866z'
|
||||
fill='url(#paint0_linear_26568_214324)'
|
||||
/>
|
||||
<path
|
||||
d='M19.113 20.829l-2.88-2.88a.571.571 0 00-.7-.085 6.854 6.854 0 01-7.081 0 .571.571 0 00-.7.084l-2.881 2.88a.57.57 0 00.062.877 11.994 11.994 0 0014.114 0 .571.571 0 00.066-.876zM11.997 15.422a3.427 3.427 0 100-6.854 3.427 3.427 0 000 6.854z'
|
||||
fill='#1F0256'
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id='paint0_linear_26568_214324'
|
||||
x1={16.4087}
|
||||
y1={-1.75881}
|
||||
x2={-7.88473}
|
||||
y2={22.5365}
|
||||
gradientUnits='userSpaceOnUse'
|
||||
>
|
||||
<stop stopColor='#17CCFC' />
|
||||
<stop offset={0.5} stopColor='#5D31FF' />
|
||||
<stop offset={1} stopColor='#F35AFF' />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
23
shadcn-admin/src/assets/clerk-logo.tsx
Normal file
23
shadcn-admin/src/assets/clerk-logo.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { type SVGProps } from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export function ClerkLogo({ className, ...props }: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
role='img'
|
||||
viewBox='0 0 24 24'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
id='clerk'
|
||||
height='24'
|
||||
width='24'
|
||||
className={cn('[&>path]:fill-foreground', className)}
|
||||
{...props}
|
||||
>
|
||||
<title>Clerk</title>
|
||||
<path
|
||||
d='m21.47 20.829 -2.881 -2.881a0.572 0.572 0 0 0 -0.7 -0.084 6.854 6.854 0 0 1 -7.081 0 0.576 0.576 0 0 0 -0.7 0.084l-2.881 2.881a0.576 0.576 0 0 0 -0.103 0.69 0.57 0.57 0 0 0 0.166 0.186 12 12 0 0 0 14.113 0 0.58 0.58 0 0 0 0.239 -0.423 0.576 0.576 0 0 0 -0.172 -0.453Zm0.002 -17.668 -2.88 2.88a0.569 0.569 0 0 1 -0.701 0.084A6.857 6.857 0 0 0 8.724 8.08a6.862 6.862 0 0 0 -1.222 3.692 6.86 6.86 0 0 0 0.978 3.764 0.573 0.573 0 0 1 -0.083 0.699l-2.881 2.88a0.567 0.567 0 0 1 -0.864 -0.063A11.993 11.993 0 0 1 6.771 2.7a11.99 11.99 0 0 1 14.637 -0.405 0.566 0.566 0 0 1 0.232 0.418 0.57 0.57 0 0 1 -0.168 0.448Zm-7.118 12.261a3.427 3.427 0 1 0 0 -6.854 3.427 3.427 0 0 0 0 6.854Z'
|
||||
strokeWidth='1'
|
||||
></path>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
110
shadcn-admin/src/assets/custom/icon-dir.tsx
Normal file
110
shadcn-admin/src/assets/custom/icon-dir.tsx
Normal file
@@ -0,0 +1,110 @@
|
||||
import { type SVGProps } from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { type Direction } from '@/context/direction-provider'
|
||||
|
||||
type IconDirProps = SVGProps<SVGSVGElement> & {
|
||||
dir: Direction
|
||||
}
|
||||
|
||||
export function IconDir({ dir, className, ...props }: IconDirProps) {
|
||||
return (
|
||||
<svg
|
||||
data-name={`icon-dir-${dir}`}
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='0 0 79.86 51.14'
|
||||
className={cn(dir === 'rtl' && 'rotate-y-180', className)}
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d='M23.42.51h51.92c2.21 0 4 1.79 4 4v42.18c0 2.21-1.79 4-4 4H23.42s-.04-.02-.04-.04V.55s.02-.04.04-.04z'
|
||||
opacity={0.15}
|
||||
/>
|
||||
<path
|
||||
fill='none'
|
||||
opacity={0.72}
|
||||
strokeLinecap='round'
|
||||
strokeMiterlimit={10}
|
||||
strokeWidth='2px'
|
||||
d='M5.56 14.88L17.78 14.88'
|
||||
/>
|
||||
<path
|
||||
fill='none'
|
||||
opacity={0.48}
|
||||
strokeLinecap='round'
|
||||
strokeMiterlimit={10}
|
||||
strokeWidth='2px'
|
||||
d='M5.56 22.09L16.08 22.09'
|
||||
/>
|
||||
<path
|
||||
fill='none'
|
||||
opacity={0.55}
|
||||
strokeLinecap='round'
|
||||
strokeMiterlimit={10}
|
||||
strokeWidth='2px'
|
||||
d='M5.56 18.38L14.93 18.38'
|
||||
/>
|
||||
<g strokeLinecap='round' strokeMiterlimit={10}>
|
||||
<circle cx={7.51} cy={7.4} r={2.54} opacity={0.8} />
|
||||
<path
|
||||
fill='none'
|
||||
opacity={0.8}
|
||||
strokeWidth='2px'
|
||||
d='M12.06 6.14L17.78 6.14'
|
||||
/>
|
||||
<path fill='none' opacity={0.6} d='M11.85 8.79L16.91 8.79' />
|
||||
</g>
|
||||
<path
|
||||
fill='none'
|
||||
opacity={0.62}
|
||||
strokeLinecap='round'
|
||||
strokeMiterlimit={10}
|
||||
strokeWidth='3px'
|
||||
d='M29.41 7.4L34.67 7.4'
|
||||
/>
|
||||
<rect
|
||||
x={28.76}
|
||||
y={11.21}
|
||||
width={26.03}
|
||||
height={2.73}
|
||||
rx={0.64}
|
||||
ry={0.64}
|
||||
opacity={0.44}
|
||||
strokeLinecap='round'
|
||||
strokeMiterlimit={10}
|
||||
/>
|
||||
<rect
|
||||
x={28.76}
|
||||
y={17.01}
|
||||
width={44.25}
|
||||
height={13.48}
|
||||
rx={0.64}
|
||||
ry={0.64}
|
||||
opacity={0.3}
|
||||
strokeLinecap='round'
|
||||
strokeMiterlimit={10}
|
||||
/>
|
||||
<rect
|
||||
x={28.76}
|
||||
y={33.57}
|
||||
width={44.25}
|
||||
height={4.67}
|
||||
rx={0.64}
|
||||
ry={0.64}
|
||||
opacity={0.21}
|
||||
strokeLinecap='round'
|
||||
strokeMiterlimit={10}
|
||||
/>
|
||||
<rect
|
||||
x={28.76}
|
||||
y={41.32}
|
||||
width={36.21}
|
||||
height={4.67}
|
||||
rx={0.64}
|
||||
ry={0.64}
|
||||
opacity={0.3}
|
||||
strokeLinecap='round'
|
||||
strokeMiterlimit={10}
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
131
shadcn-admin/src/assets/custom/icon-layout-compact.tsx
Normal file
131
shadcn-admin/src/assets/custom/icon-layout-compact.tsx
Normal file
@@ -0,0 +1,131 @@
|
||||
import { type SVGProps } from 'react'
|
||||
|
||||
export function IconLayoutCompact(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
data-name='icon-layout-compact'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='0 0 79.86 51.14'
|
||||
{...props}
|
||||
>
|
||||
<rect
|
||||
x={5.84}
|
||||
y={5.2}
|
||||
width={4}
|
||||
height={40}
|
||||
rx={2}
|
||||
ry={2}
|
||||
strokeLinecap='round'
|
||||
strokeMiterlimit={10}
|
||||
/>
|
||||
<g stroke='#fff' strokeLinecap='round' strokeMiterlimit={10}>
|
||||
<path
|
||||
fill='none'
|
||||
opacity={0.66}
|
||||
strokeWidth='2px'
|
||||
d='M7.26 11.56L8.37 11.56'
|
||||
/>
|
||||
<path
|
||||
fill='none'
|
||||
opacity={0.51}
|
||||
strokeWidth='2px'
|
||||
d='M7.26 14.49L8.37 14.49'
|
||||
/>
|
||||
<path
|
||||
fill='none'
|
||||
opacity={0.52}
|
||||
strokeWidth='2px'
|
||||
d='M7.26 17.39L8.37 17.39'
|
||||
/>
|
||||
<circle cx={7.81} cy={7.25} r={1.16} fill='#fff' opacity={0.8} />
|
||||
</g>
|
||||
<path
|
||||
fill='none'
|
||||
opacity={0.75}
|
||||
strokeLinecap='round'
|
||||
strokeMiterlimit={10}
|
||||
strokeWidth='3px'
|
||||
d='M15.81 14.49L22.89 14.49'
|
||||
/>
|
||||
<rect
|
||||
x={14.93}
|
||||
y={18.39}
|
||||
width={22.19}
|
||||
height={2.73}
|
||||
rx={0.64}
|
||||
ry={0.64}
|
||||
opacity={0.5}
|
||||
strokeLinecap='round'
|
||||
strokeMiterlimit={10}
|
||||
/>
|
||||
<rect
|
||||
x={14.93}
|
||||
y={5.89}
|
||||
width={59.16}
|
||||
height={2.73}
|
||||
rx={0.64}
|
||||
ry={0.64}
|
||||
opacity={0.9}
|
||||
strokeLinecap='round'
|
||||
strokeMiterlimit={10}
|
||||
/>
|
||||
<rect
|
||||
x={14.93}
|
||||
y={24.22}
|
||||
width={32.68}
|
||||
height={19.95}
|
||||
rx={2.11}
|
||||
ry={2.11}
|
||||
opacity={0.4}
|
||||
strokeLinecap='round'
|
||||
strokeMiterlimit={10}
|
||||
/>
|
||||
<g strokeLinecap='round' strokeMiterlimit={10}>
|
||||
<rect
|
||||
x={59.05}
|
||||
y={38.15}
|
||||
width={2.01}
|
||||
height={3.42}
|
||||
rx={0.33}
|
||||
ry={0.33}
|
||||
opacity={0.32}
|
||||
/>
|
||||
<rect
|
||||
x={54.78}
|
||||
y={34.99}
|
||||
width={2.01}
|
||||
height={6.58}
|
||||
rx={0.33}
|
||||
ry={0.33}
|
||||
opacity={0.44}
|
||||
/>
|
||||
<rect
|
||||
x={63.17}
|
||||
y={32.86}
|
||||
width={2.01}
|
||||
height={8.7}
|
||||
rx={0.33}
|
||||
ry={0.33}
|
||||
opacity={0.53}
|
||||
/>
|
||||
<rect
|
||||
x={67.54}
|
||||
y={29.17}
|
||||
width={2.01}
|
||||
height={12.4}
|
||||
rx={0.33}
|
||||
ry={0.33}
|
||||
opacity={0.66}
|
||||
/>
|
||||
</g>
|
||||
<g opacity={0.5}>
|
||||
<circle cx={62.16} cy={18.63} r={7.5} />
|
||||
<path d='M62.16 11.63c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.14-7-7 3.14-7 7-7m0-1c-4.42 0-8 3.58-8 8s3.58 8 8 8 8-3.58 8-8-3.58-8-8-8z' />
|
||||
</g>
|
||||
<g opacity={0.74}>
|
||||
<path d='M63.04 18.13l3.38-5.67c.93.64 1.7 1.48 2.26 2.47.56.98.89 2.08.96 3.21h-6.6z' />
|
||||
<path d='M66.57 13.19a6.977 6.977 0 012.52 4.44h-5.17l2.65-4.44m-.31-1.43l-4.1 6.87h8c0-1.39-.36-2.75-1.04-3.95a8.007 8.007 0 00-2.86-2.92z' />
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
124
shadcn-admin/src/assets/custom/icon-layout-default.tsx
Normal file
124
shadcn-admin/src/assets/custom/icon-layout-default.tsx
Normal file
@@ -0,0 +1,124 @@
|
||||
import { type SVGProps } from 'react'
|
||||
|
||||
export function IconLayoutDefault(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
data-name='con-layout-default'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='0 0 79.86 51.14'
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d='M39.22 15.99h-8.16c-.79 0-1.43-.67-1.43-1.5s.64-1.5 1.43-1.5h8.16c.79 0 1.43.67 1.43 1.5s-.64 1.5-1.43 1.5z'
|
||||
opacity={0.75}
|
||||
/>
|
||||
<rect
|
||||
x={29.63}
|
||||
y={18.39}
|
||||
width={16.72}
|
||||
height={2.73}
|
||||
rx={1.36}
|
||||
ry={1.36}
|
||||
opacity={0.5}
|
||||
/>
|
||||
<path
|
||||
d='M75.1 6.68v1.45c0 .63-.49 1.14-1.09 1.14H30.72c-.6 0-1.09-.51-1.09-1.14V6.68c0-.62.49-1.14 1.09-1.14h43.29c.6 0 1.09.52 1.09 1.14z'
|
||||
opacity={0.9}
|
||||
/>
|
||||
<rect
|
||||
x={29.63}
|
||||
y={24.22}
|
||||
width={21.8}
|
||||
height={19.95}
|
||||
rx={2.11}
|
||||
ry={2.11}
|
||||
opacity={0.4}
|
||||
/>
|
||||
<g strokeLinecap='round' strokeMiterlimit={10}>
|
||||
<rect
|
||||
x={61.06}
|
||||
y={38.15}
|
||||
width={2.01}
|
||||
height={3.42}
|
||||
rx={0.33}
|
||||
ry={0.33}
|
||||
opacity={0.32}
|
||||
/>
|
||||
<rect
|
||||
x={56.78}
|
||||
y={34.99}
|
||||
width={2.01}
|
||||
height={6.58}
|
||||
rx={0.33}
|
||||
ry={0.33}
|
||||
opacity={0.44}
|
||||
/>
|
||||
<rect
|
||||
x={65.17}
|
||||
y={32.86}
|
||||
width={2.01}
|
||||
height={8.7}
|
||||
rx={0.33}
|
||||
ry={0.33}
|
||||
opacity={0.53}
|
||||
/>
|
||||
<rect
|
||||
x={69.55}
|
||||
y={29.17}
|
||||
width={2.01}
|
||||
height={12.4}
|
||||
rx={0.33}
|
||||
ry={0.33}
|
||||
opacity={0.66}
|
||||
/>
|
||||
</g>
|
||||
<g opacity={0.5}>
|
||||
<circle cx={63.17} cy={18.63} r={7.5} />
|
||||
<path d='M63.17 11.63c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.14-7-7 3.14-7 7-7m0-1c-4.42 0-8 3.58-8 8s3.58 8 8 8 8-3.58 8-8-3.58-8-8-8z' />
|
||||
</g>
|
||||
<g opacity={0.74}>
|
||||
<path d='M64.05 18.13l3.38-5.67c.93.64 1.7 1.48 2.26 2.47.56.98.89 2.08.96 3.21h-6.6z' />
|
||||
<path d='M67.57 13.19a6.977 6.977 0 012.52 4.44h-5.17l2.65-4.44m-.31-1.43l-4.1 6.87h8c0-1.39-.36-2.75-1.04-3.95a8.007 8.007 0 00-2.86-2.92z' />
|
||||
</g>
|
||||
<g strokeLinecap='round' strokeMiterlimit={10}>
|
||||
<rect
|
||||
x={5.84}
|
||||
y={5.02}
|
||||
width={19.14}
|
||||
height={40}
|
||||
rx={2}
|
||||
ry={2}
|
||||
opacity={0.8}
|
||||
/>
|
||||
<g stroke='#fff'>
|
||||
<path
|
||||
fill='none'
|
||||
opacity={0.72}
|
||||
strokeWidth='2px'
|
||||
d='M9.02 17.39L21.25 17.39'
|
||||
/>
|
||||
<path
|
||||
fill='none'
|
||||
opacity={0.48}
|
||||
strokeWidth='2px'
|
||||
d='M9.02 24.6L19.54 24.6'
|
||||
/>
|
||||
<path
|
||||
fill='none'
|
||||
opacity={0.55}
|
||||
strokeWidth='2px'
|
||||
d='M9.02 20.88L18.4 20.88'
|
||||
/>
|
||||
<circle cx={10.98} cy={9.91} r={2.54} fill='#fff' opacity={0.8} />
|
||||
<path
|
||||
fill='none'
|
||||
opacity={0.8}
|
||||
strokeWidth='2px'
|
||||
d='M15.53 8.65L21.25 8.65'
|
||||
/>
|
||||
<path fill='none' opacity={0.6} d='M15.32 11.3L20.38 11.3' />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
100
shadcn-admin/src/assets/custom/icon-layout-full.tsx
Normal file
100
shadcn-admin/src/assets/custom/icon-layout-full.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
import { type SVGProps } from 'react'
|
||||
|
||||
export function IconLayoutFull(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
data-name='icon-layout-full'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='0 0 79.86 51.14'
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fill='none'
|
||||
opacity={0.75}
|
||||
strokeLinecap='round'
|
||||
strokeMiterlimit={10}
|
||||
strokeWidth='3px'
|
||||
d='M6.85 14.49L15.02 14.49'
|
||||
/>
|
||||
<rect
|
||||
x={5.84}
|
||||
y={18.39}
|
||||
width={25.6}
|
||||
height={2.73}
|
||||
rx={0.64}
|
||||
ry={0.64}
|
||||
opacity={0.5}
|
||||
strokeLinecap='round'
|
||||
strokeMiterlimit={10}
|
||||
/>
|
||||
<rect
|
||||
x={5.84}
|
||||
y={5.89}
|
||||
width={68.26}
|
||||
height={2.73}
|
||||
rx={0.64}
|
||||
ry={0.64}
|
||||
opacity={0.9}
|
||||
strokeLinecap='round'
|
||||
strokeMiterlimit={10}
|
||||
/>
|
||||
<rect
|
||||
x={5.84}
|
||||
y={24.22}
|
||||
width={37.71}
|
||||
height={19.95}
|
||||
rx={2.11}
|
||||
ry={2.11}
|
||||
opacity={0.4}
|
||||
strokeLinecap='round'
|
||||
strokeMiterlimit={10}
|
||||
/>
|
||||
<g strokeLinecap='round' strokeMiterlimit={10}>
|
||||
<rect
|
||||
x={59.05}
|
||||
y={38.15}
|
||||
width={2.01}
|
||||
height={3.42}
|
||||
rx={0.33}
|
||||
ry={0.33}
|
||||
opacity={0.32}
|
||||
/>
|
||||
<rect
|
||||
x={54.78}
|
||||
y={34.99}
|
||||
width={2.01}
|
||||
height={6.58}
|
||||
rx={0.33}
|
||||
ry={0.33}
|
||||
opacity={0.44}
|
||||
/>
|
||||
<rect
|
||||
x={63.17}
|
||||
y={32.86}
|
||||
width={2.01}
|
||||
height={8.7}
|
||||
rx={0.33}
|
||||
ry={0.33}
|
||||
opacity={0.53}
|
||||
/>
|
||||
<rect
|
||||
x={67.54}
|
||||
y={29.17}
|
||||
width={2.01}
|
||||
height={12.4}
|
||||
rx={0.33}
|
||||
ry={0.33}
|
||||
opacity={0.66}
|
||||
/>
|
||||
</g>
|
||||
<g opacity={0.5}>
|
||||
<circle cx={62.16} cy={18.63} r={7.5} />
|
||||
<path d='M62.16 11.63c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.14-7-7 3.14-7 7-7m0-1c-4.42 0-8 3.58-8 8s3.58 8 8 8 8-3.58 8-8-3.58-8-8-8z' />
|
||||
</g>
|
||||
<g opacity={0.74}>
|
||||
<path d='M63.04 18.13l3.38-5.67c.93.64 1.7 1.48 2.26 2.47.56.98.89 2.08.96 3.21h-6.6z' />
|
||||
<path d='M66.57 13.19a6.977 6.977 0 012.52 4.44h-5.17l2.65-4.44m-.31-1.43l-4.1 6.87h8c0-1.39-.36-2.75-1.04-3.95a8.007 8.007 0 00-2.86-2.92z' />
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
82
shadcn-admin/src/assets/custom/icon-sidebar-floating.tsx
Normal file
82
shadcn-admin/src/assets/custom/icon-sidebar-floating.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
import { type SVGProps } from 'react'
|
||||
|
||||
export function IconSidebarFloating(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
data-name='icon-sidebar-floating'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='0 0 79.86 51.14'
|
||||
{...props}
|
||||
>
|
||||
<rect
|
||||
x={5.89}
|
||||
y={5.15}
|
||||
width={19.74}
|
||||
height={40}
|
||||
rx={2}
|
||||
ry={2}
|
||||
opacity={0.8}
|
||||
strokeLinecap='round'
|
||||
strokeMiterlimit={10}
|
||||
/>
|
||||
<g stroke='#fff' strokeLinecap='round' strokeMiterlimit={10}>
|
||||
<path
|
||||
fill='none'
|
||||
opacity={0.72}
|
||||
strokeWidth='2px'
|
||||
d='M9.81 18.36L22.04 18.36'
|
||||
/>
|
||||
<path
|
||||
fill='none'
|
||||
opacity={0.48}
|
||||
strokeWidth='2px'
|
||||
d='M9.81 25.57L20.33 25.57'
|
||||
/>
|
||||
<path
|
||||
fill='none'
|
||||
opacity={0.55}
|
||||
strokeWidth='2px'
|
||||
d='M9.81 21.85L19.18 21.85'
|
||||
/>
|
||||
<circle cx={11.76} cy={10.88} r={2.54} fill='#fff' opacity={0.8} />
|
||||
<path
|
||||
fill='none'
|
||||
opacity={0.8}
|
||||
strokeWidth='2px'
|
||||
d='M16.31 9.62L22.04 9.62'
|
||||
/>
|
||||
<path fill='none' opacity={0.6} d='M16.1 12.27L21.16 12.27' />
|
||||
</g>
|
||||
<path
|
||||
fill='none'
|
||||
opacity={0.62}
|
||||
strokeLinecap='round'
|
||||
strokeMiterlimit={10}
|
||||
strokeWidth='3px'
|
||||
d='M30.59 9.62L35.85 9.62'
|
||||
/>
|
||||
<rect
|
||||
x={29.94}
|
||||
y={13.42}
|
||||
width={26.03}
|
||||
height={2.73}
|
||||
rx={0.64}
|
||||
ry={0.64}
|
||||
opacity={0.44}
|
||||
strokeLinecap='round'
|
||||
strokeMiterlimit={10}
|
||||
/>
|
||||
<rect
|
||||
x={29.94}
|
||||
y={19.28}
|
||||
width={43.11}
|
||||
height={25.87}
|
||||
rx={2}
|
||||
ry={2}
|
||||
opacity={0.3}
|
||||
strokeLinecap='round'
|
||||
strokeMiterlimit={10}
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
58
shadcn-admin/src/assets/custom/icon-sidebar-inset.tsx
Normal file
58
shadcn-admin/src/assets/custom/icon-sidebar-inset.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import { type SVGProps } from 'react'
|
||||
|
||||
export function IconSidebarInset(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
data-name='icon-sidebar-inset'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='0 0 79.86 51.14'
|
||||
{...props}
|
||||
>
|
||||
<rect
|
||||
x={23.39}
|
||||
y={5.57}
|
||||
width={50.22}
|
||||
height={40}
|
||||
rx={2}
|
||||
ry={2}
|
||||
opacity={0.2}
|
||||
strokeLinecap='round'
|
||||
strokeMiterlimit={10}
|
||||
/>
|
||||
<path
|
||||
fill='none'
|
||||
opacity={0.72}
|
||||
strokeLinecap='round'
|
||||
strokeMiterlimit={10}
|
||||
strokeWidth='2px'
|
||||
d='M5.08 17.05L17.31 17.05'
|
||||
/>
|
||||
<path
|
||||
fill='none'
|
||||
opacity={0.48}
|
||||
strokeLinecap='round'
|
||||
strokeMiterlimit={10}
|
||||
strokeWidth='2px'
|
||||
d='M5.08 24.25L15.6 24.25'
|
||||
/>
|
||||
<path
|
||||
fill='none'
|
||||
opacity={0.55}
|
||||
strokeLinecap='round'
|
||||
strokeMiterlimit={10}
|
||||
strokeWidth='2px'
|
||||
d='M5.08 20.54L14.46 20.54'
|
||||
/>
|
||||
<g strokeLinecap='round' strokeMiterlimit={10}>
|
||||
<circle cx={7.04} cy={9.57} r={2.54} opacity={0.8} />
|
||||
<path
|
||||
fill='none'
|
||||
opacity={0.8}
|
||||
strokeWidth='2px'
|
||||
d='M11.59 8.3L17.31 8.3'
|
||||
/>
|
||||
<path fill='none' opacity={0.6} d='M11.38 10.95L16.44 10.95' />
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
53
shadcn-admin/src/assets/custom/icon-sidebar-sidebar.tsx
Normal file
53
shadcn-admin/src/assets/custom/icon-sidebar-sidebar.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import { type SVGProps } from 'react'
|
||||
|
||||
export function IconSidebarSidebar(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
data-name='icon-sidebar-sidebar'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='0 0 79.86 51.14'
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d='M23.42.51h51.99c2.21 0 4 1.79 4 4v42.18c0 2.21-1.79 4-4 4H23.42s-.04-.02-.04-.04V.55s.02-.04.04-.04z'
|
||||
opacity={0.2}
|
||||
strokeLinecap='round'
|
||||
strokeMiterlimit={10}
|
||||
/>
|
||||
<path
|
||||
fill='none'
|
||||
opacity={0.72}
|
||||
strokeLinecap='round'
|
||||
strokeMiterlimit={10}
|
||||
strokeWidth='2px'
|
||||
d='M5.56 14.88L17.78 14.88'
|
||||
/>
|
||||
<path
|
||||
fill='none'
|
||||
opacity={0.48}
|
||||
strokeLinecap='round'
|
||||
strokeMiterlimit={10}
|
||||
strokeWidth='2px'
|
||||
d='M5.56 22.09L16.08 22.09'
|
||||
/>
|
||||
<path
|
||||
fill='none'
|
||||
opacity={0.55}
|
||||
strokeLinecap='round'
|
||||
strokeMiterlimit={10}
|
||||
strokeWidth='2px'
|
||||
d='M5.56 18.38L14.93 18.38'
|
||||
/>
|
||||
<g strokeLinecap='round' strokeMiterlimit={10}>
|
||||
<circle cx={7.51} cy={7.4} r={2.54} opacity={0.8} />
|
||||
<path
|
||||
fill='none'
|
||||
opacity={0.8}
|
||||
strokeWidth='2px'
|
||||
d='M12.06 6.14L17.78 6.14'
|
||||
/>
|
||||
<path fill='none' opacity={0.6} d='M11.85 8.79L16.91 8.79' />
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
79
shadcn-admin/src/assets/custom/icon-theme-dark.tsx
Normal file
79
shadcn-admin/src/assets/custom/icon-theme-dark.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
import { type SVGProps } from 'react'
|
||||
|
||||
export function IconThemeDark(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
data-name='icon-theme-dark'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='0 0 79.86 51.14'
|
||||
{...props}
|
||||
>
|
||||
<g fill='#1d2b3f'>
|
||||
<rect x={0.53} y={0.5} width={78.83} height={50.14} rx={3.5} ry={3.5} />
|
||||
<path d='M75.86 1c1.65 0 3 1.35 3 3v43.14c0 1.65-1.35 3-3 3H4.03c-1.65 0-3-1.35-3-3V4c0-1.65 1.35-3 3-3h71.83m0-1H4.03c-2.21 0-4 1.79-4 4v43.14c0 2.21 1.79 4 4 4h71.83c2.21 0 4-1.79 4-4V4c0-2.21-1.79-4-4-4z' />
|
||||
</g>
|
||||
<path
|
||||
d='M22.88 0h52.97c2.21 0 4 1.79 4 4v43.14c0 2.21-1.79 4-4 4H22.88V0z'
|
||||
fill='#0d1628'
|
||||
/>
|
||||
<circle cx={6.7} cy={7.04} r={3.54} fill='#426187' />
|
||||
<path
|
||||
d='M18.12 6.39h-5.87c-.6 0-1.09-.45-1.09-1s.49-1 1.09-1h5.87c.6 0 1.09.45 1.09 1s-.49 1-1.09 1zM16.55 9.77h-4.24c-.55 0-1-.45-1-1s.45-1 1-1h4.24c.55 0 1 .45 1 1s-.45 1-1 1zM18.32 17.37H4.59c-.69 0-1.25-.47-1.25-1.05s.56-1.05 1.25-1.05h13.73c.69 0 1.25.47 1.25 1.05s-.56 1.05-1.25 1.05zM15.34 21.26h-11c-.55 0-1-.41-1-.91s.45-.91 1-.91h11c.55 0 1 .41 1 .91s-.45.91-1 .91zM16.46 25.57H4.43c-.6 0-1.09-.44-1.09-.98s.49-.98 1.09-.98h12.03c.6 0 1.09.44 1.09.98s-.49.98-1.09.98z'
|
||||
fill='#426187'
|
||||
/>
|
||||
<g fill='#2a62bc'>
|
||||
<rect
|
||||
x={33.36}
|
||||
y={19.73}
|
||||
width={2.75}
|
||||
height={3.42}
|
||||
rx={0.33}
|
||||
ry={0.33}
|
||||
opacity={0.32}
|
||||
/>
|
||||
<rect
|
||||
x={29.64}
|
||||
y={16.57}
|
||||
width={2.75}
|
||||
height={6.58}
|
||||
rx={0.33}
|
||||
ry={0.33}
|
||||
opacity={0.44}
|
||||
/>
|
||||
<rect
|
||||
x={37.16}
|
||||
y={14.44}
|
||||
width={2.75}
|
||||
height={8.7}
|
||||
rx={0.33}
|
||||
ry={0.33}
|
||||
opacity={0.53}
|
||||
/>
|
||||
<rect
|
||||
x={41.19}
|
||||
y={10.75}
|
||||
width={2.75}
|
||||
height={12.4}
|
||||
rx={0.33}
|
||||
ry={0.33}
|
||||
opacity={0.53}
|
||||
/>
|
||||
</g>
|
||||
<circle cx={62.74} cy={16.32} r={8} fill='#2f5491' opacity={0.5} />
|
||||
<path
|
||||
d='M62.74 16.32l4.1-6.87c1.19.71 2.18 1.72 2.86 2.92s1.04 2.57 1.04 3.95h-8z'
|
||||
fill='#2f5491'
|
||||
opacity={0.74}
|
||||
/>
|
||||
<rect
|
||||
x={29.64}
|
||||
y={27.75}
|
||||
width={41.62}
|
||||
height={18.62}
|
||||
rx={1.69}
|
||||
ry={1.69}
|
||||
fill='#17273f'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
78
shadcn-admin/src/assets/custom/icon-theme-light.tsx
Normal file
78
shadcn-admin/src/assets/custom/icon-theme-light.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
import { type SVGProps } from 'react'
|
||||
|
||||
export function IconThemeLight(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
data-name='icon-theme-light'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='0 0 79.86 51.14'
|
||||
{...props}
|
||||
>
|
||||
<g fill='#d9d9d9'>
|
||||
<rect x={0.53} y={0.5} width={78.83} height={50.14} rx={3.5} ry={3.5} />
|
||||
<path d='M75.86 1c1.65 0 3 1.35 3 3v43.14c0 1.65-1.35 3-3 3H4.03c-1.65 0-3-1.35-3-3V4c0-1.65 1.35-3 3-3h71.83m0-1H4.03c-2.21 0-4 1.79-4 4v43.14c0 2.21 1.79 4 4 4h71.83c2.21 0 4-1.79 4-4V4c0-2.21-1.79-4-4-4z' />
|
||||
</g>
|
||||
<path
|
||||
d='M22.88 0h52.97c2.21 0 4 1.79 4 4v43.14c0 2.21-1.79 4-4 4H22.88V0z'
|
||||
fill='#ecedef'
|
||||
/>
|
||||
<circle cx={6.7} cy={7.04} r={3.54} fill='#fff' />
|
||||
<path
|
||||
d='M18.12 6.39h-5.87c-.6 0-1.09-.45-1.09-1s.49-1 1.09-1h5.87c.6 0 1.09.45 1.09 1s-.49 1-1.09 1zM16.55 9.77h-4.24c-.55 0-1-.45-1-1s.45-1 1-1h4.24c.55 0 1 .45 1 1s-.45 1-1 1zM18.32 17.37H4.59c-.69 0-1.25-.47-1.25-1.05s.56-1.05 1.25-1.05h13.73c.69 0 1.25.47 1.25 1.05s-.56 1.05-1.25 1.05zM15.34 21.26h-11c-.55 0-1-.41-1-.91s.45-.91 1-.91h11c.55 0 1 .41 1 .91s-.45.91-1 .91zM16.46 25.57H4.43c-.6 0-1.09-.44-1.09-.98s.49-.98 1.09-.98h12.03c.6 0 1.09.44 1.09.98s-.49.98-1.09.98z'
|
||||
fill='#fff'
|
||||
/>
|
||||
<g fill='#c0c4c4'>
|
||||
<rect
|
||||
x={33.36}
|
||||
y={19.73}
|
||||
width={2.75}
|
||||
height={3.42}
|
||||
rx={0.33}
|
||||
ry={0.33}
|
||||
opacity={0.32}
|
||||
/>
|
||||
<rect
|
||||
x={29.64}
|
||||
y={16.57}
|
||||
width={2.75}
|
||||
height={6.58}
|
||||
rx={0.33}
|
||||
ry={0.33}
|
||||
opacity={0.44}
|
||||
/>
|
||||
<rect
|
||||
x={37.16}
|
||||
y={14.44}
|
||||
width={2.75}
|
||||
height={8.7}
|
||||
rx={0.33}
|
||||
ry={0.33}
|
||||
opacity={0.53}
|
||||
/>
|
||||
<rect
|
||||
x={41.19}
|
||||
y={10.75}
|
||||
width={2.75}
|
||||
height={12.4}
|
||||
rx={0.33}
|
||||
ry={0.33}
|
||||
opacity={0.53}
|
||||
/>
|
||||
</g>
|
||||
<circle cx={62.74} cy={16.32} r={8} fill='#fff' />
|
||||
<g fill='#d9d9d9'>
|
||||
<path d='M63.62 15.82L67 10.15c.93.64 1.7 1.48 2.26 2.47.56.98.89 2.08.96 3.21h-6.6z' />
|
||||
<path d='M67.14 10.88a6.977 6.977 0 012.52 4.44h-5.17l2.65-4.44m-.31-1.43l-4.1 6.87h8c0-1.39-.36-2.75-1.04-3.95s-1.67-2.21-2.86-2.92z' />
|
||||
</g>
|
||||
<rect
|
||||
x={29.64}
|
||||
y={27.75}
|
||||
width={41.62}
|
||||
height={18.62}
|
||||
rx={1.69}
|
||||
ry={1.69}
|
||||
fill='#fff'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
116
shadcn-admin/src/assets/custom/icon-theme-system.tsx
Normal file
116
shadcn-admin/src/assets/custom/icon-theme-system.tsx
Normal file
@@ -0,0 +1,116 @@
|
||||
import { type SVGProps } from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export function IconThemeSystem({
|
||||
className,
|
||||
...props
|
||||
}: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
data-name='icon-theme-system'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='0 0 79.86 51.14'
|
||||
className={cn(
|
||||
'overflow-hidden rounded-[6px]',
|
||||
'fill-primary stroke-primary group-data-[state=unchecked]:fill-muted-foreground group-data-[state=unchecked]:stroke-muted-foreground',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<path opacity={0.2} d='M0 0.03H22.88V51.17H0z' />
|
||||
<circle
|
||||
cx={6.7}
|
||||
cy={7.04}
|
||||
r={3.54}
|
||||
fill='#fff'
|
||||
opacity={0.8}
|
||||
stroke='#fff'
|
||||
strokeLinecap='round'
|
||||
strokeMiterlimit={10}
|
||||
/>
|
||||
<path
|
||||
d='M18.12 6.39h-5.87c-.6 0-1.09-.45-1.09-1s.49-1 1.09-1h5.87c.6 0 1.09.45 1.09 1s-.49 1-1.09 1zM16.55 9.77h-4.24c-.55 0-1-.45-1-1s.45-1 1-1h4.24c.55 0 1 .45 1 1s-.45 1-1 1z'
|
||||
fill='#fff'
|
||||
stroke='none'
|
||||
opacity={0.75}
|
||||
/>
|
||||
<path
|
||||
d='M18.32 17.37H4.59c-.69 0-1.25-.47-1.25-1.05s.56-1.05 1.25-1.05h13.73c.69 0 1.25.47 1.25 1.05s-.56 1.05-1.25 1.05z'
|
||||
fill='#fff'
|
||||
stroke='none'
|
||||
opacity={0.72}
|
||||
/>
|
||||
<path
|
||||
d='M15.34 21.26h-11c-.55 0-1-.41-1-.91s.45-.91 1-.91h11c.55 0 1 .41 1 .91s-.45.91-1 .91z'
|
||||
fill='#fff'
|
||||
stroke='none'
|
||||
opacity={0.55}
|
||||
/>
|
||||
<path
|
||||
d='M16.46 25.57H4.43c-.6 0-1.09-.44-1.09-.98s.49-.98 1.09-.98h12.03c.6 0 1.09.44 1.09.98s-.49.98-1.09.98z'
|
||||
fill='#fff'
|
||||
stroke='none'
|
||||
opacity={0.67}
|
||||
/>
|
||||
<rect
|
||||
x={33.36}
|
||||
y={19.73}
|
||||
width={2.75}
|
||||
height={3.42}
|
||||
rx={0.33}
|
||||
ry={0.33}
|
||||
opacity={0.31}
|
||||
stroke='none'
|
||||
/>
|
||||
<rect
|
||||
x={29.64}
|
||||
y={16.57}
|
||||
width={2.75}
|
||||
height={6.58}
|
||||
rx={0.33}
|
||||
ry={0.33}
|
||||
opacity={0.4}
|
||||
stroke='none'
|
||||
/>
|
||||
<rect
|
||||
x={37.16}
|
||||
y={14.44}
|
||||
width={2.75}
|
||||
height={8.7}
|
||||
rx={0.33}
|
||||
ry={0.33}
|
||||
opacity={0.26}
|
||||
stroke='none'
|
||||
/>
|
||||
<rect
|
||||
x={41.19}
|
||||
y={10.75}
|
||||
width={2.75}
|
||||
height={12.4}
|
||||
rx={0.33}
|
||||
ry={0.33}
|
||||
opacity={0.37}
|
||||
stroke='none'
|
||||
/>
|
||||
<g>
|
||||
<circle cx={62.74} cy={16.32} r={8} opacity={0.25} />
|
||||
<path
|
||||
d='M62.74 16.32l4.1-6.87c1.19.71 2.18 1.72 2.86 2.92s1.04 2.57 1.04 3.95h-8z'
|
||||
opacity={0.45}
|
||||
/>
|
||||
</g>
|
||||
<rect
|
||||
x={29.64}
|
||||
y={27.75}
|
||||
width={41.62}
|
||||
height={18.62}
|
||||
rx={1.69}
|
||||
ry={1.69}
|
||||
opacity={0.3}
|
||||
stroke='none'
|
||||
strokeLinecap='round'
|
||||
strokeMiterlimit={10}
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
24
shadcn-admin/src/assets/logo.tsx
Normal file
24
shadcn-admin/src/assets/logo.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import { type SVGProps } from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export function Logo({ className, ...props }: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
id='shadcn-admin-logo'
|
||||
viewBox='0 0 24 24'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
height='24'
|
||||
width='24'
|
||||
fill='none'
|
||||
stroke='currentColor'
|
||||
strokeWidth='2'
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
className={cn('size-6', className)}
|
||||
{...props}
|
||||
>
|
||||
<title>Shadcn-Admin</title>
|
||||
<path d='M15 6v12a3 3 0 1 0 3-3H6a3 3 0 1 0 3 3V6a3 3 0 1 0-3 3h12a3 3 0 1 0-3-3' />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
16
shadcn-admin/src/components/coming-soon.tsx
Normal file
16
shadcn-admin/src/components/coming-soon.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Telescope } from 'lucide-react'
|
||||
|
||||
export function ComingSoon() {
|
||||
return (
|
||||
<div className='h-svh'>
|
||||
<div className='m-auto flex h-full w-full flex-col items-center justify-center gap-2'>
|
||||
<Telescope size={72} />
|
||||
<h1 className='text-4xl leading-tight font-bold'>Coming Soon!</h1>
|
||||
<p className='text-center text-muted-foreground'>
|
||||
This page has not been created yet. <br />
|
||||
Stay tuned though!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
91
shadcn-admin/src/components/command-menu.tsx
Normal file
91
shadcn-admin/src/components/command-menu.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
import React from 'react'
|
||||
import { useNavigate } from '@tanstack/react-router'
|
||||
import { ArrowRight, ChevronRight, Laptop, Moon, Sun } from 'lucide-react'
|
||||
import { useSearch } from '@/context/search-provider'
|
||||
import { useTheme } from '@/context/theme-provider'
|
||||
import {
|
||||
CommandDialog,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
CommandSeparator,
|
||||
} from '@/components/ui/command'
|
||||
import { sidebarData } from './layout/data/sidebar-data'
|
||||
import { ScrollArea } from './ui/scroll-area'
|
||||
|
||||
export function CommandMenu() {
|
||||
const navigate = useNavigate()
|
||||
const { setTheme } = useTheme()
|
||||
const { open, setOpen } = useSearch()
|
||||
|
||||
const runCommand = React.useCallback(
|
||||
(command: () => unknown) => {
|
||||
setOpen(false)
|
||||
command()
|
||||
},
|
||||
[setOpen]
|
||||
)
|
||||
|
||||
return (
|
||||
<CommandDialog modal open={open} onOpenChange={setOpen}>
|
||||
<CommandInput placeholder='Type a command or search...' />
|
||||
<CommandList>
|
||||
<ScrollArea type='hover' className='h-72 pe-1'>
|
||||
<CommandEmpty>No results found.</CommandEmpty>
|
||||
{sidebarData.navGroups.map((group) => (
|
||||
<CommandGroup key={group.title} heading={group.title}>
|
||||
{group.items.map((navItem, i) => {
|
||||
if (navItem.url)
|
||||
return (
|
||||
<CommandItem
|
||||
key={`${navItem.url}-${i}`}
|
||||
value={navItem.title}
|
||||
onSelect={() => {
|
||||
runCommand(() => navigate({ to: navItem.url }))
|
||||
}}
|
||||
>
|
||||
<div className='flex size-4 items-center justify-center'>
|
||||
<ArrowRight className='size-2 text-muted-foreground/80' />
|
||||
</div>
|
||||
{navItem.title}
|
||||
</CommandItem>
|
||||
)
|
||||
|
||||
return navItem.items?.map((subItem, i) => (
|
||||
<CommandItem
|
||||
key={`${navItem.title}-${subItem.url}-${i}`}
|
||||
value={`${navItem.title}-${subItem.url}`}
|
||||
onSelect={() => {
|
||||
runCommand(() => navigate({ to: subItem.url }))
|
||||
}}
|
||||
>
|
||||
<div className='flex size-4 items-center justify-center'>
|
||||
<ArrowRight className='size-2 text-muted-foreground/80' />
|
||||
</div>
|
||||
{navItem.title} <ChevronRight /> {subItem.title}
|
||||
</CommandItem>
|
||||
))
|
||||
})}
|
||||
</CommandGroup>
|
||||
))}
|
||||
<CommandSeparator />
|
||||
<CommandGroup heading='Theme'>
|
||||
<CommandItem onSelect={() => runCommand(() => setTheme('light'))}>
|
||||
<Sun /> <span>Light</span>
|
||||
</CommandItem>
|
||||
<CommandItem onSelect={() => runCommand(() => setTheme('dark'))}>
|
||||
<Moon className='scale-90' />
|
||||
<span>Dark</span>
|
||||
</CommandItem>
|
||||
<CommandItem onSelect={() => runCommand(() => setTheme('system'))}>
|
||||
<Laptop />
|
||||
<span>System</span>
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
</ScrollArea>
|
||||
</CommandList>
|
||||
</CommandDialog>
|
||||
)
|
||||
}
|
||||
354
shadcn-admin/src/components/config-drawer.tsx
Normal file
354
shadcn-admin/src/components/config-drawer.tsx
Normal file
@@ -0,0 +1,354 @@
|
||||
import { type SVGProps } from 'react'
|
||||
import { Root as Radio, Item } from '@radix-ui/react-radio-group'
|
||||
import { CircleCheck, RotateCcw, Settings } from 'lucide-react'
|
||||
import { IconDir } from '@/assets/custom/icon-dir'
|
||||
import { IconLayoutCompact } from '@/assets/custom/icon-layout-compact'
|
||||
import { IconLayoutDefault } from '@/assets/custom/icon-layout-default'
|
||||
import { IconLayoutFull } from '@/assets/custom/icon-layout-full'
|
||||
import { IconSidebarFloating } from '@/assets/custom/icon-sidebar-floating'
|
||||
import { IconSidebarInset } from '@/assets/custom/icon-sidebar-inset'
|
||||
import { IconSidebarSidebar } from '@/assets/custom/icon-sidebar-sidebar'
|
||||
import { IconThemeDark } from '@/assets/custom/icon-theme-dark'
|
||||
import { IconThemeLight } from '@/assets/custom/icon-theme-light'
|
||||
import { IconThemeSystem } from '@/assets/custom/icon-theme-system'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useDirection } from '@/context/direction-provider'
|
||||
import { type Collapsible, useLayout } from '@/context/layout-provider'
|
||||
import { useTheme } from '@/context/theme-provider'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetDescription,
|
||||
SheetFooter,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
SheetTrigger,
|
||||
} from '@/components/ui/sheet'
|
||||
import { useSidebar } from './ui/sidebar'
|
||||
|
||||
export function ConfigDrawer() {
|
||||
const { setOpen } = useSidebar()
|
||||
const { resetDir } = useDirection()
|
||||
const { resetTheme } = useTheme()
|
||||
const { resetLayout } = useLayout()
|
||||
|
||||
const handleReset = () => {
|
||||
setOpen(true)
|
||||
resetDir()
|
||||
resetTheme()
|
||||
resetLayout()
|
||||
}
|
||||
|
||||
return (
|
||||
<Sheet>
|
||||
<SheetTrigger asChild>
|
||||
<Button
|
||||
size='icon'
|
||||
variant='ghost'
|
||||
aria-label='Open theme settings'
|
||||
aria-describedby='config-drawer-description'
|
||||
className='rounded-full'
|
||||
>
|
||||
<Settings aria-hidden='true' />
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent className='flex flex-col'>
|
||||
<SheetHeader className='pb-0 text-start'>
|
||||
<SheetTitle>Theme Settings</SheetTitle>
|
||||
<SheetDescription id='config-drawer-description'>
|
||||
Adjust the appearance and layout to suit your preferences.
|
||||
</SheetDescription>
|
||||
</SheetHeader>
|
||||
<div className='space-y-6 overflow-y-auto px-4'>
|
||||
<ThemeConfig />
|
||||
<SidebarConfig />
|
||||
<LayoutConfig />
|
||||
<DirConfig />
|
||||
</div>
|
||||
<SheetFooter className='gap-2'>
|
||||
<Button
|
||||
variant='destructive'
|
||||
onClick={handleReset}
|
||||
aria-label='Reset all settings to default values'
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
</SheetFooter>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
)
|
||||
}
|
||||
|
||||
function SectionTitle({
|
||||
title,
|
||||
showReset = false,
|
||||
onReset,
|
||||
className,
|
||||
}: {
|
||||
title: string
|
||||
showReset?: boolean
|
||||
onReset?: () => void
|
||||
className?: string
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'mb-2 flex items-center gap-2 text-sm font-semibold text-muted-foreground',
|
||||
className
|
||||
)}
|
||||
>
|
||||
{title}
|
||||
{showReset && onReset && (
|
||||
<Button
|
||||
size='icon'
|
||||
variant='secondary'
|
||||
className='size-4 rounded-full'
|
||||
onClick={onReset}
|
||||
>
|
||||
<RotateCcw className='size-3' />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function RadioGroupItem({
|
||||
item,
|
||||
isTheme = false,
|
||||
}: {
|
||||
item: {
|
||||
value: string
|
||||
label: string
|
||||
icon: (props: SVGProps<SVGSVGElement>) => React.ReactElement
|
||||
}
|
||||
isTheme?: boolean
|
||||
}) {
|
||||
return (
|
||||
<Item
|
||||
value={item.value}
|
||||
className={cn('group outline-none', 'transition duration-200 ease-in')}
|
||||
aria-label={`Select ${item.label.toLowerCase()}`}
|
||||
aria-describedby={`${item.value}-description`}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'relative rounded-[6px] ring-[1px] ring-border',
|
||||
'group-data-[state=checked]:shadow-2xl group-data-[state=checked]:ring-primary',
|
||||
'group-focus-visible:ring-2'
|
||||
)}
|
||||
role='img'
|
||||
aria-hidden='false'
|
||||
aria-label={`${item.label} option preview`}
|
||||
>
|
||||
<CircleCheck
|
||||
className={cn(
|
||||
'size-6 fill-primary stroke-white',
|
||||
'group-data-[state=unchecked]:hidden',
|
||||
'absolute top-0 right-0 translate-x-1/2 -translate-y-1/2'
|
||||
)}
|
||||
aria-hidden='true'
|
||||
/>
|
||||
<item.icon
|
||||
className={cn(
|
||||
!isTheme &&
|
||||
'fill-primary stroke-primary group-data-[state=unchecked]:fill-muted-foreground group-data-[state=unchecked]:stroke-muted-foreground'
|
||||
)}
|
||||
aria-hidden='true'
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className='mt-1 text-xs'
|
||||
id={`${item.value}-description`}
|
||||
aria-live='polite'
|
||||
>
|
||||
{item.label}
|
||||
</div>
|
||||
</Item>
|
||||
)
|
||||
}
|
||||
|
||||
function ThemeConfig() {
|
||||
const { defaultTheme, theme, setTheme } = useTheme()
|
||||
return (
|
||||
<div>
|
||||
<SectionTitle
|
||||
title='Theme'
|
||||
showReset={theme !== defaultTheme}
|
||||
onReset={() => setTheme(defaultTheme)}
|
||||
/>
|
||||
<Radio
|
||||
value={theme}
|
||||
onValueChange={setTheme}
|
||||
className='grid w-full max-w-md grid-cols-3 gap-4'
|
||||
aria-label='Select theme preference'
|
||||
aria-describedby='theme-description'
|
||||
>
|
||||
{[
|
||||
{
|
||||
value: 'system',
|
||||
label: 'System',
|
||||
icon: IconThemeSystem,
|
||||
},
|
||||
{
|
||||
value: 'light',
|
||||
label: 'Light',
|
||||
icon: IconThemeLight,
|
||||
},
|
||||
{
|
||||
value: 'dark',
|
||||
label: 'Dark',
|
||||
icon: IconThemeDark,
|
||||
},
|
||||
].map((item) => (
|
||||
<RadioGroupItem key={item.value} item={item} isTheme />
|
||||
))}
|
||||
</Radio>
|
||||
<div id='theme-description' className='sr-only'>
|
||||
Choose between system preference, light mode, or dark mode
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarConfig() {
|
||||
const { defaultVariant, variant, setVariant } = useLayout()
|
||||
return (
|
||||
<div className='max-md:hidden'>
|
||||
<SectionTitle
|
||||
title='Sidebar'
|
||||
showReset={defaultVariant !== variant}
|
||||
onReset={() => setVariant(defaultVariant)}
|
||||
/>
|
||||
<Radio
|
||||
value={variant}
|
||||
onValueChange={setVariant}
|
||||
className='grid w-full max-w-md grid-cols-3 gap-4'
|
||||
aria-label='Select sidebar style'
|
||||
aria-describedby='sidebar-description'
|
||||
>
|
||||
{[
|
||||
{
|
||||
value: 'inset',
|
||||
label: 'Inset',
|
||||
icon: IconSidebarInset,
|
||||
},
|
||||
{
|
||||
value: 'floating',
|
||||
label: 'Floating',
|
||||
icon: IconSidebarFloating,
|
||||
},
|
||||
{
|
||||
value: 'sidebar',
|
||||
label: 'Sidebar',
|
||||
icon: IconSidebarSidebar,
|
||||
},
|
||||
].map((item) => (
|
||||
<RadioGroupItem key={item.value} item={item} />
|
||||
))}
|
||||
</Radio>
|
||||
<div id='sidebar-description' className='sr-only'>
|
||||
Choose between inset, floating, or standard sidebar layout
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function LayoutConfig() {
|
||||
const { open, setOpen } = useSidebar()
|
||||
const { defaultCollapsible, collapsible, setCollapsible } = useLayout()
|
||||
|
||||
const radioState = open ? 'default' : collapsible
|
||||
|
||||
return (
|
||||
<div className='max-md:hidden'>
|
||||
<SectionTitle
|
||||
title='Layout'
|
||||
showReset={radioState !== 'default'}
|
||||
onReset={() => {
|
||||
setOpen(true)
|
||||
setCollapsible(defaultCollapsible)
|
||||
}}
|
||||
/>
|
||||
<Radio
|
||||
value={radioState}
|
||||
onValueChange={(v) => {
|
||||
if (v === 'default') {
|
||||
setOpen(true)
|
||||
return
|
||||
}
|
||||
setOpen(false)
|
||||
setCollapsible(v as Collapsible)
|
||||
}}
|
||||
className='grid w-full max-w-md grid-cols-3 gap-4'
|
||||
aria-label='Select layout style'
|
||||
aria-describedby='layout-description'
|
||||
>
|
||||
{[
|
||||
{
|
||||
value: 'default',
|
||||
label: 'Default',
|
||||
icon: IconLayoutDefault,
|
||||
},
|
||||
{
|
||||
value: 'icon',
|
||||
label: 'Compact',
|
||||
icon: IconLayoutCompact,
|
||||
},
|
||||
{
|
||||
value: 'offcanvas',
|
||||
label: 'Full layout',
|
||||
icon: IconLayoutFull,
|
||||
},
|
||||
].map((item) => (
|
||||
<RadioGroupItem key={item.value} item={item} />
|
||||
))}
|
||||
</Radio>
|
||||
<div id='layout-description' className='sr-only'>
|
||||
Choose between default expanded, compact icon-only, or full layout mode
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function DirConfig() {
|
||||
const { defaultDir, dir, setDir } = useDirection()
|
||||
return (
|
||||
<div>
|
||||
<SectionTitle
|
||||
title='Direction'
|
||||
showReset={defaultDir !== dir}
|
||||
onReset={() => setDir(defaultDir)}
|
||||
/>
|
||||
<Radio
|
||||
value={dir}
|
||||
onValueChange={setDir}
|
||||
className='grid w-full max-w-md grid-cols-3 gap-4'
|
||||
aria-label='Select site direction'
|
||||
aria-describedby='direction-description'
|
||||
>
|
||||
{[
|
||||
{
|
||||
value: 'ltr',
|
||||
label: 'Left to Right',
|
||||
icon: (props: SVGProps<SVGSVGElement>) => (
|
||||
<IconDir dir='ltr' {...props} />
|
||||
),
|
||||
},
|
||||
{
|
||||
value: 'rtl',
|
||||
label: 'Right to Left',
|
||||
icon: (props: SVGProps<SVGSVGElement>) => (
|
||||
<IconDir dir='rtl' {...props} />
|
||||
),
|
||||
},
|
||||
].map((item) => (
|
||||
<RadioGroupItem key={item.value} item={item} />
|
||||
))}
|
||||
</Radio>
|
||||
<div id='direction-description' className='sr-only'>
|
||||
Choose between left-to-right or right-to-left site direction
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
67
shadcn-admin/src/components/confirm-dialog.tsx
Normal file
67
shadcn-admin/src/components/confirm-dialog.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import { cn } from '@/lib/utils'
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
|
||||
type ConfirmDialogProps = {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
title: React.ReactNode
|
||||
disabled?: boolean
|
||||
desc: React.JSX.Element | string
|
||||
cancelBtnText?: string
|
||||
confirmText?: React.ReactNode
|
||||
destructive?: boolean
|
||||
handleConfirm: () => void
|
||||
isLoading?: boolean
|
||||
className?: string
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
export function ConfirmDialog(props: ConfirmDialogProps) {
|
||||
const {
|
||||
title,
|
||||
desc,
|
||||
children,
|
||||
className,
|
||||
confirmText,
|
||||
cancelBtnText,
|
||||
destructive,
|
||||
isLoading,
|
||||
disabled = false,
|
||||
handleConfirm,
|
||||
...actions
|
||||
} = props
|
||||
return (
|
||||
<AlertDialog {...actions}>
|
||||
<AlertDialogContent className={cn(className && className)}>
|
||||
<AlertDialogHeader className='text-start'>
|
||||
<AlertDialogTitle>{title}</AlertDialogTitle>
|
||||
<AlertDialogDescription asChild>
|
||||
<div>{desc}</div>
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
{children}
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={isLoading}>
|
||||
{cancelBtnText ?? 'Cancel'}
|
||||
</AlertDialogCancel>
|
||||
<Button
|
||||
variant={destructive ? 'destructive' : 'default'}
|
||||
onClick={handleConfirm}
|
||||
disabled={disabled || isLoading}
|
||||
>
|
||||
{confirmText ?? 'Continue'}
|
||||
</Button>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
)
|
||||
}
|
||||
213
shadcn-admin/src/components/data-table/bulk-actions.tsx
Normal file
213
shadcn-admin/src/components/data-table/bulk-actions.tsx
Normal file
@@ -0,0 +1,213 @@
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import { type Table } from '@tanstack/react-table'
|
||||
import { X } from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip'
|
||||
|
||||
type DataTableBulkActionsProps<TData> = {
|
||||
table: Table<TData>
|
||||
entityName: string
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
/**
|
||||
* A modular toolbar for displaying bulk actions when table rows are selected.
|
||||
*
|
||||
* @template TData The type of data in the table.
|
||||
* @param {object} props The component props.
|
||||
* @param {Table<TData>} props.table The react-table instance.
|
||||
* @param {string} props.entityName The name of the entity being acted upon (e.g., "task", "user").
|
||||
* @param {React.ReactNode} props.children The action buttons to be rendered inside the toolbar.
|
||||
* @returns {React.ReactNode | null} The rendered component or null if no rows are selected.
|
||||
*/
|
||||
export function DataTableBulkActions<TData>({
|
||||
table,
|
||||
entityName,
|
||||
children,
|
||||
}: DataTableBulkActionsProps<TData>): React.ReactNode | null {
|
||||
const selectedRows = table.getFilteredSelectedRowModel().rows
|
||||
const selectedCount = selectedRows.length
|
||||
const toolbarRef = useRef<HTMLDivElement>(null)
|
||||
const [announcement, setAnnouncement] = useState('')
|
||||
|
||||
// Announce selection changes to screen readers
|
||||
useEffect(() => {
|
||||
if (selectedCount > 0) {
|
||||
const message = `${selectedCount} ${entityName}${selectedCount > 1 ? 's' : ''} selected. Bulk actions toolbar is available.`
|
||||
|
||||
// Use queueMicrotask to defer state update and avoid cascading renders
|
||||
queueMicrotask(() => {
|
||||
setAnnouncement(message)
|
||||
})
|
||||
|
||||
// Clear announcement after a delay
|
||||
const timer = setTimeout(() => setAnnouncement(''), 3000)
|
||||
return () => clearTimeout(timer)
|
||||
}
|
||||
}, [selectedCount, entityName])
|
||||
|
||||
const handleClearSelection = () => {
|
||||
table.resetRowSelection()
|
||||
}
|
||||
|
||||
const handleKeyDown = (event: React.KeyboardEvent) => {
|
||||
const buttons = toolbarRef.current?.querySelectorAll('button')
|
||||
if (!buttons) return
|
||||
|
||||
const currentIndex = Array.from(buttons).findIndex(
|
||||
(button) => button === document.activeElement
|
||||
)
|
||||
|
||||
switch (event.key) {
|
||||
case 'ArrowRight': {
|
||||
event.preventDefault()
|
||||
const nextIndex = (currentIndex + 1) % buttons.length
|
||||
buttons[nextIndex]?.focus()
|
||||
break
|
||||
}
|
||||
case 'ArrowLeft': {
|
||||
event.preventDefault()
|
||||
const prevIndex =
|
||||
currentIndex === 0 ? buttons.length - 1 : currentIndex - 1
|
||||
buttons[prevIndex]?.focus()
|
||||
break
|
||||
}
|
||||
case 'Home':
|
||||
event.preventDefault()
|
||||
buttons[0]?.focus()
|
||||
break
|
||||
case 'End':
|
||||
event.preventDefault()
|
||||
buttons[buttons.length - 1]?.focus()
|
||||
break
|
||||
case 'Escape': {
|
||||
// Check if the Escape key came from a dropdown trigger or content
|
||||
// We can't check dropdown state because Radix UI closes it before our handler runs
|
||||
const target = event.target as HTMLElement
|
||||
const activeElement = document.activeElement as HTMLElement
|
||||
|
||||
// Check if the event target or currently focused element is a dropdown trigger
|
||||
const isFromDropdownTrigger =
|
||||
target?.getAttribute('data-slot') === 'dropdown-menu-trigger' ||
|
||||
activeElement?.getAttribute('data-slot') ===
|
||||
'dropdown-menu-trigger' ||
|
||||
target?.closest('[data-slot="dropdown-menu-trigger"]') ||
|
||||
activeElement?.closest('[data-slot="dropdown-menu-trigger"]')
|
||||
|
||||
// Check if the focused element is inside dropdown content (which is portaled)
|
||||
const isFromDropdownContent =
|
||||
activeElement?.closest('[data-slot="dropdown-menu-content"]') ||
|
||||
target?.closest('[data-slot="dropdown-menu-content"]')
|
||||
|
||||
if (isFromDropdownTrigger || isFromDropdownContent) {
|
||||
// Escape was meant for the dropdown - don't clear selection
|
||||
return
|
||||
}
|
||||
|
||||
// Escape was meant for the toolbar - clear selection
|
||||
event.preventDefault()
|
||||
handleClearSelection()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedCount === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Live region for screen reader announcements */}
|
||||
<div
|
||||
aria-live='polite'
|
||||
aria-atomic='true'
|
||||
className='sr-only'
|
||||
role='status'
|
||||
>
|
||||
{announcement}
|
||||
</div>
|
||||
|
||||
<div
|
||||
ref={toolbarRef}
|
||||
role='toolbar'
|
||||
aria-label={`Bulk actions for ${selectedCount} selected ${entityName}${selectedCount > 1 ? 's' : ''}`}
|
||||
aria-describedby='bulk-actions-description'
|
||||
tabIndex={-1}
|
||||
onKeyDown={handleKeyDown}
|
||||
className={cn(
|
||||
'fixed bottom-6 left-1/2 z-50 -translate-x-1/2 rounded-xl',
|
||||
'transition-all delay-100 duration-300 ease-out hover:scale-105',
|
||||
'focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:outline-none'
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'p-2 shadow-xl',
|
||||
'rounded-xl border',
|
||||
'bg-background/95 backdrop-blur-lg supports-backdrop-filter:bg-background/60',
|
||||
'flex items-center gap-x-2'
|
||||
)}
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant='outline'
|
||||
size='icon'
|
||||
onClick={handleClearSelection}
|
||||
className='size-6 rounded-full'
|
||||
aria-label='Clear selection'
|
||||
title='Clear selection (Escape)'
|
||||
>
|
||||
<X />
|
||||
<span className='sr-only'>Clear selection</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Clear selection (Escape)</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Separator
|
||||
className='h-5'
|
||||
orientation='vertical'
|
||||
aria-hidden='true'
|
||||
/>
|
||||
|
||||
<div
|
||||
className='flex items-center gap-x-1 text-sm'
|
||||
id='bulk-actions-description'
|
||||
>
|
||||
<Badge
|
||||
variant='default'
|
||||
className='min-w-8 rounded-lg'
|
||||
aria-label={`${selectedCount} selected`}
|
||||
>
|
||||
{selectedCount}
|
||||
</Badge>{' '}
|
||||
<span className='hidden sm:inline'>
|
||||
{entityName}
|
||||
{selectedCount > 1 ? 's' : ''}
|
||||
</span>{' '}
|
||||
selected
|
||||
</div>
|
||||
|
||||
<Separator
|
||||
className='h-5'
|
||||
orientation='vertical'
|
||||
aria-hidden='true'
|
||||
/>
|
||||
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
74
shadcn-admin/src/components/data-table/column-header.tsx
Normal file
74
shadcn-admin/src/components/data-table/column-header.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import {
|
||||
ArrowDownIcon,
|
||||
ArrowUpIcon,
|
||||
CaretSortIcon,
|
||||
EyeNoneIcon,
|
||||
} from '@radix-ui/react-icons'
|
||||
import { type Column } from '@tanstack/react-table'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
|
||||
type DataTableColumnHeaderProps<TData, TValue> =
|
||||
React.HTMLAttributes<HTMLDivElement> & {
|
||||
column: Column<TData, TValue>
|
||||
title: string
|
||||
}
|
||||
|
||||
export function DataTableColumnHeader<TData, TValue>({
|
||||
column,
|
||||
title,
|
||||
className,
|
||||
}: DataTableColumnHeaderProps<TData, TValue>) {
|
||||
if (!column.getCanSort()) {
|
||||
return <div className={cn(className)}>{title}</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('flex items-center space-x-2', className)}>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='sm'
|
||||
className='h-8 data-[state=open]:bg-accent'
|
||||
>
|
||||
<span>{title}</span>
|
||||
{column.getIsSorted() === 'desc' ? (
|
||||
<ArrowDownIcon className='ms-2 h-4 w-4' />
|
||||
) : column.getIsSorted() === 'asc' ? (
|
||||
<ArrowUpIcon className='ms-2 h-4 w-4' />
|
||||
) : (
|
||||
<CaretSortIcon className='ms-2 h-4 w-4' />
|
||||
)}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align='start'>
|
||||
<DropdownMenuItem onClick={() => column.toggleSorting(false)}>
|
||||
<ArrowUpIcon className='size-3.5 text-muted-foreground/70' />
|
||||
Asc
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => column.toggleSorting(true)}>
|
||||
<ArrowDownIcon className='size-3.5 text-muted-foreground/70' />
|
||||
Desc
|
||||
</DropdownMenuItem>
|
||||
{column.getCanHide() && (
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={() => column.toggleVisibility(false)}>
|
||||
<EyeNoneIcon className='size-3.5 text-muted-foreground/70' />
|
||||
Hide
|
||||
</DropdownMenuItem>
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
146
shadcn-admin/src/components/data-table/faceted-filter.tsx
Normal file
146
shadcn-admin/src/components/data-table/faceted-filter.tsx
Normal file
@@ -0,0 +1,146 @@
|
||||
import * as React from 'react'
|
||||
import { CheckIcon, PlusCircledIcon } from '@radix-ui/react-icons'
|
||||
import { type Column } from '@tanstack/react-table'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
CommandSeparator,
|
||||
} from '@/components/ui/command'
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@/components/ui/popover'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
|
||||
type DataTableFacetedFilterProps<TData, TValue> = {
|
||||
column?: Column<TData, TValue>
|
||||
title?: string
|
||||
options: {
|
||||
label: string
|
||||
value: string
|
||||
icon?: React.ComponentType<{ className?: string }>
|
||||
}[]
|
||||
}
|
||||
|
||||
export function DataTableFacetedFilter<TData, TValue>({
|
||||
column,
|
||||
title,
|
||||
options,
|
||||
}: DataTableFacetedFilterProps<TData, TValue>) {
|
||||
const facets = column?.getFacetedUniqueValues()
|
||||
const selectedValues = new Set(column?.getFilterValue() as string[])
|
||||
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant='outline' size='sm' className='h-8 border-dashed'>
|
||||
<PlusCircledIcon className='size-4' />
|
||||
{title}
|
||||
{selectedValues?.size > 0 && (
|
||||
<>
|
||||
<Separator orientation='vertical' className='mx-2 h-4' />
|
||||
<Badge
|
||||
variant='secondary'
|
||||
className='rounded-sm px-1 font-normal lg:hidden'
|
||||
>
|
||||
{selectedValues.size}
|
||||
</Badge>
|
||||
<div className='hidden space-x-1 lg:flex'>
|
||||
{selectedValues.size > 2 ? (
|
||||
<Badge
|
||||
variant='secondary'
|
||||
className='rounded-sm px-1 font-normal'
|
||||
>
|
||||
{selectedValues.size} selected
|
||||
</Badge>
|
||||
) : (
|
||||
options
|
||||
.filter((option) => selectedValues.has(option.value))
|
||||
.map((option) => (
|
||||
<Badge
|
||||
variant='secondary'
|
||||
key={option.value}
|
||||
className='rounded-sm px-1 font-normal'
|
||||
>
|
||||
{option.label}
|
||||
</Badge>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className='w-[200px] p-0' align='start'>
|
||||
<Command>
|
||||
<CommandInput placeholder={title} />
|
||||
<CommandList>
|
||||
<CommandEmpty>No results found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{options.map((option) => {
|
||||
const isSelected = selectedValues.has(option.value)
|
||||
return (
|
||||
<CommandItem
|
||||
key={option.value}
|
||||
onSelect={() => {
|
||||
if (isSelected) {
|
||||
selectedValues.delete(option.value)
|
||||
} else {
|
||||
selectedValues.add(option.value)
|
||||
}
|
||||
const filterValues = Array.from(selectedValues)
|
||||
column?.setFilterValue(
|
||||
filterValues.length ? filterValues : undefined
|
||||
)
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'flex size-4 items-center justify-center rounded-sm border border-primary',
|
||||
isSelected
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'opacity-50 [&_svg]:invisible'
|
||||
)}
|
||||
>
|
||||
<CheckIcon className={cn('h-4 w-4 text-background')} />
|
||||
</div>
|
||||
{option.icon && (
|
||||
<option.icon className='size-4 text-muted-foreground' />
|
||||
)}
|
||||
<span>{option.label}</span>
|
||||
{facets?.get(option.value) && (
|
||||
<span className='ms-auto flex h-4 w-4 items-center justify-center font-mono text-xs'>
|
||||
{facets.get(option.value)}
|
||||
</span>
|
||||
)}
|
||||
</CommandItem>
|
||||
)
|
||||
})}
|
||||
</CommandGroup>
|
||||
{selectedValues.size > 0 && (
|
||||
<>
|
||||
<CommandSeparator />
|
||||
<CommandGroup>
|
||||
<CommandItem
|
||||
onSelect={() => column?.setFilterValue(undefined)}
|
||||
className='justify-center text-center'
|
||||
>
|
||||
Clear filters
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
</>
|
||||
)}
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
6
shadcn-admin/src/components/data-table/index.ts
Normal file
6
shadcn-admin/src/components/data-table/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export { DataTablePagination } from './pagination'
|
||||
export { DataTableColumnHeader } from './column-header'
|
||||
export { DataTableFacetedFilter } from './faceted-filter'
|
||||
export { DataTableViewOptions } from './view-options'
|
||||
export { DataTableToolbar } from './toolbar'
|
||||
export { DataTableBulkActions } from './bulk-actions'
|
||||
130
shadcn-admin/src/components/data-table/pagination.tsx
Normal file
130
shadcn-admin/src/components/data-table/pagination.tsx
Normal file
@@ -0,0 +1,130 @@
|
||||
import {
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
DoubleArrowLeftIcon,
|
||||
DoubleArrowRightIcon,
|
||||
} from '@radix-ui/react-icons'
|
||||
import { type Table } from '@tanstack/react-table'
|
||||
import { cn, getPageNumbers } from '@/lib/utils'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
|
||||
type DataTablePaginationProps<TData> = {
|
||||
table: Table<TData>
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function DataTablePagination<TData>({
|
||||
table,
|
||||
className,
|
||||
}: DataTablePaginationProps<TData>) {
|
||||
const currentPage = table.getState().pagination.pageIndex + 1
|
||||
const totalPages = table.getPageCount()
|
||||
const pageNumbers = getPageNumbers(currentPage, totalPages)
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center justify-between overflow-clip px-2',
|
||||
'@max-2xl/content:flex-col-reverse @max-2xl/content:gap-4',
|
||||
className
|
||||
)}
|
||||
style={{ overflowClipMargin: 1 }}
|
||||
>
|
||||
<div className='flex w-full items-center justify-between'>
|
||||
<div className='flex w-[100px] items-center justify-center text-sm font-medium @2xl/content:hidden'>
|
||||
Page {currentPage} of {totalPages}
|
||||
</div>
|
||||
<div className='flex items-center gap-2 @max-2xl/content:flex-row-reverse'>
|
||||
<Select
|
||||
value={`${table.getState().pagination.pageSize}`}
|
||||
onValueChange={(value) => {
|
||||
table.setPageSize(Number(value))
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className='h-8 w-[70px]'>
|
||||
<SelectValue placeholder={table.getState().pagination.pageSize} />
|
||||
</SelectTrigger>
|
||||
<SelectContent side='top'>
|
||||
{[10, 20, 30, 40, 50].map((pageSize) => (
|
||||
<SelectItem key={pageSize} value={`${pageSize}`}>
|
||||
{pageSize}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className='hidden text-sm font-medium sm:block'>Rows per page</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex items-center sm:space-x-6 lg:space-x-8'>
|
||||
<div className='flex w-[100px] items-center justify-center text-sm font-medium @max-3xl/content:hidden'>
|
||||
Page {currentPage} of {totalPages}
|
||||
</div>
|
||||
<div className='flex items-center space-x-2'>
|
||||
<Button
|
||||
variant='outline'
|
||||
className='size-8 p-0 @max-md/content:hidden'
|
||||
onClick={() => table.setPageIndex(0)}
|
||||
disabled={!table.getCanPreviousPage()}
|
||||
>
|
||||
<span className='sr-only'>Go to first page</span>
|
||||
<DoubleArrowLeftIcon className='h-4 w-4' />
|
||||
</Button>
|
||||
<Button
|
||||
variant='outline'
|
||||
className='size-8 p-0'
|
||||
onClick={() => table.previousPage()}
|
||||
disabled={!table.getCanPreviousPage()}
|
||||
>
|
||||
<span className='sr-only'>Go to previous page</span>
|
||||
<ChevronLeftIcon className='h-4 w-4' />
|
||||
</Button>
|
||||
|
||||
{/* Page number buttons */}
|
||||
{pageNumbers.map((pageNumber, index) => (
|
||||
<div key={`${pageNumber}-${index}`} className='flex items-center'>
|
||||
{pageNumber === '...' ? (
|
||||
<span className='px-1 text-sm text-muted-foreground'>...</span>
|
||||
) : (
|
||||
<Button
|
||||
variant={currentPage === pageNumber ? 'default' : 'outline'}
|
||||
className='h-8 min-w-8 px-2'
|
||||
onClick={() => table.setPageIndex((pageNumber as number) - 1)}
|
||||
>
|
||||
<span className='sr-only'>Go to page {pageNumber}</span>
|
||||
{pageNumber}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
<Button
|
||||
variant='outline'
|
||||
className='size-8 p-0'
|
||||
onClick={() => table.nextPage()}
|
||||
disabled={!table.getCanNextPage()}
|
||||
>
|
||||
<span className='sr-only'>Go to next page</span>
|
||||
<ChevronRightIcon className='h-4 w-4' />
|
||||
</Button>
|
||||
<Button
|
||||
variant='outline'
|
||||
className='size-8 p-0 @max-md/content:hidden'
|
||||
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
|
||||
disabled={!table.getCanNextPage()}
|
||||
>
|
||||
<span className='sr-only'>Go to last page</span>
|
||||
<DoubleArrowRightIcon className='h-4 w-4' />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
85
shadcn-admin/src/components/data-table/toolbar.tsx
Normal file
85
shadcn-admin/src/components/data-table/toolbar.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
import { Cross2Icon } from '@radix-ui/react-icons'
|
||||
import { type Table } from '@tanstack/react-table'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { DataTableFacetedFilter } from './faceted-filter'
|
||||
import { DataTableViewOptions } from './view-options'
|
||||
|
||||
type DataTableToolbarProps<TData> = {
|
||||
table: Table<TData>
|
||||
searchPlaceholder?: string
|
||||
searchKey?: string
|
||||
filters?: {
|
||||
columnId: string
|
||||
title: string
|
||||
options: {
|
||||
label: string
|
||||
value: string
|
||||
icon?: React.ComponentType<{ className?: string }>
|
||||
}[]
|
||||
}[]
|
||||
}
|
||||
|
||||
export function DataTableToolbar<TData>({
|
||||
table,
|
||||
searchPlaceholder = 'Filter...',
|
||||
searchKey,
|
||||
filters = [],
|
||||
}: DataTableToolbarProps<TData>) {
|
||||
const isFiltered =
|
||||
table.getState().columnFilters.length > 0 || table.getState().globalFilter
|
||||
|
||||
return (
|
||||
<div className='flex items-center justify-between'>
|
||||
<div className='flex flex-1 flex-col-reverse items-start gap-y-2 sm:flex-row sm:items-center sm:space-x-2'>
|
||||
{searchKey ? (
|
||||
<Input
|
||||
placeholder={searchPlaceholder}
|
||||
value={
|
||||
(table.getColumn(searchKey)?.getFilterValue() as string) ?? ''
|
||||
}
|
||||
onChange={(event) =>
|
||||
table.getColumn(searchKey)?.setFilterValue(event.target.value)
|
||||
}
|
||||
className='h-8 w-[150px] lg:w-[250px]'
|
||||
/>
|
||||
) : (
|
||||
<Input
|
||||
placeholder={searchPlaceholder}
|
||||
value={table.getState().globalFilter ?? ''}
|
||||
onChange={(event) => table.setGlobalFilter(event.target.value)}
|
||||
className='h-8 w-[150px] lg:w-[250px]'
|
||||
/>
|
||||
)}
|
||||
<div className='flex gap-x-2'>
|
||||
{filters.map((filter) => {
|
||||
const column = table.getColumn(filter.columnId)
|
||||
if (!column) return null
|
||||
return (
|
||||
<DataTableFacetedFilter
|
||||
key={filter.columnId}
|
||||
column={column}
|
||||
title={filter.title}
|
||||
options={filter.options}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
{isFiltered && (
|
||||
<Button
|
||||
variant='ghost'
|
||||
onClick={() => {
|
||||
table.resetColumnFilters()
|
||||
table.setGlobalFilter('')
|
||||
}}
|
||||
className='h-8 px-2 lg:px-3'
|
||||
>
|
||||
Reset
|
||||
<Cross2Icon className='ms-2 h-4 w-4' />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<DataTableViewOptions table={table} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
56
shadcn-admin/src/components/data-table/view-options.tsx
Normal file
56
shadcn-admin/src/components/data-table/view-options.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import { DropdownMenuTrigger } from '@radix-ui/react-dropdown-menu'
|
||||
import { MixerHorizontalIcon } from '@radix-ui/react-icons'
|
||||
import { type Table } from '@tanstack/react-table'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
|
||||
type DataTableViewOptionsProps<TData> = {
|
||||
table: Table<TData>
|
||||
}
|
||||
|
||||
export function DataTableViewOptions<TData>({
|
||||
table,
|
||||
}: DataTableViewOptionsProps<TData>) {
|
||||
return (
|
||||
<DropdownMenu modal={false}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant='outline'
|
||||
size='sm'
|
||||
className='ms-auto hidden h-8 lg:flex'
|
||||
>
|
||||
<MixerHorizontalIcon className='size-4' />
|
||||
View
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align='end' className='w-[150px]'>
|
||||
<DropdownMenuLabel>Toggle columns</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
{table
|
||||
.getAllColumns()
|
||||
.filter(
|
||||
(column) =>
|
||||
typeof column.accessorFn !== 'undefined' && column.getCanHide()
|
||||
)
|
||||
.map((column) => {
|
||||
return (
|
||||
<DropdownMenuCheckboxItem
|
||||
key={column.id}
|
||||
className='capitalize'
|
||||
checked={column.getIsVisible()}
|
||||
onCheckedChange={(value) => column.toggleVisibility(!!value)}
|
||||
>
|
||||
{column.id}
|
||||
</DropdownMenuCheckboxItem>
|
||||
)
|
||||
})}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
||||
51
shadcn-admin/src/components/date-picker.tsx
Normal file
51
shadcn-admin/src/components/date-picker.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import { format } from 'date-fns'
|
||||
import { Calendar as CalendarIcon } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Calendar } from '@/components/ui/calendar'
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@/components/ui/popover'
|
||||
|
||||
type DatePickerProps = {
|
||||
selected: Date | undefined
|
||||
onSelect: (date: Date | undefined) => void
|
||||
placeholder?: string
|
||||
}
|
||||
|
||||
export function DatePicker({
|
||||
selected,
|
||||
onSelect,
|
||||
placeholder = 'Pick a date',
|
||||
}: DatePickerProps) {
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant='outline'
|
||||
data-empty={!selected}
|
||||
className='w-[240px] justify-start text-start font-normal data-[empty=true]:text-muted-foreground'
|
||||
>
|
||||
{selected ? (
|
||||
format(selected, 'MMM d, yyyy')
|
||||
) : (
|
||||
<span>{placeholder}</span>
|
||||
)}
|
||||
<CalendarIcon className='ms-auto h-4 w-4 opacity-50' />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className='w-auto p-0'>
|
||||
<Calendar
|
||||
mode='single'
|
||||
captionLayout='dropdown'
|
||||
selected={selected}
|
||||
onSelect={onSelect}
|
||||
disabled={(date: Date) =>
|
||||
date > new Date() || date < new Date('1900-01-01')
|
||||
}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
37
shadcn-admin/src/components/layout/app-sidebar.tsx
Normal file
37
shadcn-admin/src/components/layout/app-sidebar.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import { useLayout } from '@/context/layout-provider'
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarFooter,
|
||||
SidebarHeader,
|
||||
SidebarRail,
|
||||
} from '@/components/ui/sidebar'
|
||||
// import { AppTitle } from './app-title'
|
||||
import { sidebarData } from './data/sidebar-data'
|
||||
import { NavGroup } from './nav-group'
|
||||
import { NavUser } from './nav-user'
|
||||
import { TeamSwitcher } from './team-switcher'
|
||||
|
||||
export function AppSidebar() {
|
||||
const { collapsible, variant } = useLayout()
|
||||
return (
|
||||
<Sidebar collapsible={collapsible} variant={variant}>
|
||||
<SidebarHeader>
|
||||
<TeamSwitcher teams={sidebarData.teams} />
|
||||
|
||||
{/* Replace <TeamSwitch /> with the following <AppTitle />
|
||||
/* if you want to use the normal app title instead of TeamSwitch dropdown */}
|
||||
{/* <AppTitle /> */}
|
||||
</SidebarHeader>
|
||||
<SidebarContent>
|
||||
{sidebarData.navGroups.map((props) => (
|
||||
<NavGroup key={props.title} {...props} />
|
||||
))}
|
||||
</SidebarContent>
|
||||
<SidebarFooter>
|
||||
<NavUser user={sidebarData.user} />
|
||||
</SidebarFooter>
|
||||
<SidebarRail />
|
||||
</Sidebar>
|
||||
)
|
||||
}
|
||||
64
shadcn-admin/src/components/layout/app-title.tsx
Normal file
64
shadcn-admin/src/components/layout/app-title.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import { Link } from '@tanstack/react-router'
|
||||
import { Menu, X } from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import {
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
useSidebar,
|
||||
} from '@/components/ui/sidebar'
|
||||
import { Button } from '../ui/button'
|
||||
|
||||
export function AppTitle() {
|
||||
const { setOpenMobile } = useSidebar()
|
||||
return (
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton
|
||||
size='lg'
|
||||
className='gap-0 py-0 hover:bg-transparent active:bg-transparent'
|
||||
asChild
|
||||
>
|
||||
<div>
|
||||
<Link
|
||||
to='/'
|
||||
onClick={() => setOpenMobile(false)}
|
||||
className='grid flex-1 text-start text-sm leading-tight'
|
||||
>
|
||||
<span className='truncate font-bold'>Shadcn-Admin</span>
|
||||
<span className='truncate text-xs'>Vite + ShadcnUI</span>
|
||||
</Link>
|
||||
<ToggleSidebar />
|
||||
</div>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
)
|
||||
}
|
||||
|
||||
function ToggleSidebar({
|
||||
className,
|
||||
onClick,
|
||||
...props
|
||||
}: React.ComponentProps<typeof Button>) {
|
||||
const { toggleSidebar } = useSidebar()
|
||||
|
||||
return (
|
||||
<Button
|
||||
data-sidebar='trigger'
|
||||
data-slot='sidebar-trigger'
|
||||
variant='ghost'
|
||||
size='icon'
|
||||
className={cn('aspect-square size-8 max-md:scale-125', className)}
|
||||
onClick={(event) => {
|
||||
onClick?.(event)
|
||||
toggleSidebar()
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
<X className='md:hidden' />
|
||||
<Menu className='max-md:hidden' />
|
||||
<span className='sr-only'>Toggle Sidebar</span>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
42
shadcn-admin/src/components/layout/authenticated-layout.tsx
Normal file
42
shadcn-admin/src/components/layout/authenticated-layout.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import { Outlet } from '@tanstack/react-router'
|
||||
import { getCookie } from '@/lib/cookies'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { LayoutProvider } from '@/context/layout-provider'
|
||||
import { SearchProvider } from '@/context/search-provider'
|
||||
import { SidebarInset, SidebarProvider } from '@/components/ui/sidebar'
|
||||
import { AppSidebar } from '@/components/layout/app-sidebar'
|
||||
import { SkipToMain } from '@/components/skip-to-main'
|
||||
|
||||
type AuthenticatedLayoutProps = {
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
export function AuthenticatedLayout({ children }: AuthenticatedLayoutProps) {
|
||||
const defaultOpen = getCookie('sidebar_state') !== 'false'
|
||||
return (
|
||||
<SearchProvider>
|
||||
<LayoutProvider>
|
||||
<SidebarProvider defaultOpen={defaultOpen}>
|
||||
<SkipToMain />
|
||||
<AppSidebar />
|
||||
<SidebarInset
|
||||
className={cn(
|
||||
// Set content container, so we can use container queries
|
||||
'@container/content',
|
||||
|
||||
// If layout is fixed, set the height
|
||||
// to 100svh to prevent overflow
|
||||
'has-data-[layout=fixed]:h-svh',
|
||||
|
||||
// If layout is fixed and sidebar is inset,
|
||||
// set the height to 100svh - spacing (total margins) to prevent overflow
|
||||
'peer-data-[variant=inset]:has-data-[layout=fixed]:h-[calc(100svh-(var(--spacing)*4))]'
|
||||
)}
|
||||
>
|
||||
{children ?? <Outlet />}
|
||||
</SidebarInset>
|
||||
</SidebarProvider>
|
||||
</LayoutProvider>
|
||||
</SearchProvider>
|
||||
)
|
||||
}
|
||||
210
shadcn-admin/src/components/layout/data/sidebar-data.ts
Normal file
210
shadcn-admin/src/components/layout/data/sidebar-data.ts
Normal file
@@ -0,0 +1,210 @@
|
||||
import {
|
||||
Construction,
|
||||
LayoutDashboard,
|
||||
Monitor,
|
||||
Bug,
|
||||
ListTodo,
|
||||
FileX,
|
||||
HelpCircle,
|
||||
Lock,
|
||||
Bell,
|
||||
Package,
|
||||
Palette,
|
||||
ServerOff,
|
||||
Settings,
|
||||
Wrench,
|
||||
UserCog,
|
||||
UserX,
|
||||
Users,
|
||||
MessagesSquare,
|
||||
ShieldCheck,
|
||||
AudioWaveform,
|
||||
Command,
|
||||
GalleryVerticalEnd,
|
||||
} from 'lucide-react'
|
||||
import { ClerkLogo } from '@/assets/clerk-logo'
|
||||
import { type SidebarData } from '../types'
|
||||
|
||||
export const sidebarData: SidebarData = {
|
||||
user: {
|
||||
name: 'satnaing',
|
||||
email: 'satnaingdev@gmail.com',
|
||||
avatar: '/avatars/shadcn.jpg',
|
||||
},
|
||||
teams: [
|
||||
{
|
||||
name: 'Shadcn Admin',
|
||||
logo: Command,
|
||||
plan: 'Vite + ShadcnUI',
|
||||
},
|
||||
{
|
||||
name: 'Acme Inc',
|
||||
logo: GalleryVerticalEnd,
|
||||
plan: 'Enterprise',
|
||||
},
|
||||
{
|
||||
name: 'Acme Corp.',
|
||||
logo: AudioWaveform,
|
||||
plan: 'Startup',
|
||||
},
|
||||
],
|
||||
navGroups: [
|
||||
{
|
||||
title: 'General',
|
||||
items: [
|
||||
{
|
||||
title: 'Dashboard',
|
||||
url: '/',
|
||||
icon: LayoutDashboard,
|
||||
},
|
||||
{
|
||||
title: 'Tasks',
|
||||
url: '/tasks',
|
||||
icon: ListTodo,
|
||||
},
|
||||
{
|
||||
title: 'Characters',
|
||||
url: '/characters',
|
||||
icon: Users,
|
||||
},
|
||||
{
|
||||
title: 'Apps',
|
||||
url: '/apps',
|
||||
icon: Package,
|
||||
},
|
||||
{
|
||||
title: 'Chats',
|
||||
url: '/chats',
|
||||
badge: '3',
|
||||
icon: MessagesSquare,
|
||||
},
|
||||
{
|
||||
title: 'Users',
|
||||
url: '/users',
|
||||
icon: Users,
|
||||
},
|
||||
{
|
||||
title: 'Secured by Clerk',
|
||||
icon: ClerkLogo,
|
||||
items: [
|
||||
{
|
||||
title: 'Sign In',
|
||||
url: '/clerk/sign-in',
|
||||
},
|
||||
{
|
||||
title: 'Sign Up',
|
||||
url: '/clerk/sign-up',
|
||||
},
|
||||
{
|
||||
title: 'User Management',
|
||||
url: '/clerk/user-management',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Pages',
|
||||
items: [
|
||||
{
|
||||
title: 'Auth',
|
||||
icon: ShieldCheck,
|
||||
items: [
|
||||
{
|
||||
title: 'Sign In',
|
||||
url: '/sign-in',
|
||||
},
|
||||
{
|
||||
title: 'Sign In (2 Col)',
|
||||
url: '/sign-in-2',
|
||||
},
|
||||
{
|
||||
title: 'Sign Up',
|
||||
url: '/sign-up',
|
||||
},
|
||||
{
|
||||
title: 'Forgot Password',
|
||||
url: '/forgot-password',
|
||||
},
|
||||
{
|
||||
title: 'OTP',
|
||||
url: '/otp',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Errors',
|
||||
icon: Bug,
|
||||
items: [
|
||||
{
|
||||
title: 'Unauthorized',
|
||||
url: '/errors/unauthorized',
|
||||
icon: Lock,
|
||||
},
|
||||
{
|
||||
title: 'Forbidden',
|
||||
url: '/errors/forbidden',
|
||||
icon: UserX,
|
||||
},
|
||||
{
|
||||
title: 'Not Found',
|
||||
url: '/errors/not-found',
|
||||
icon: FileX,
|
||||
},
|
||||
{
|
||||
title: 'Internal Server Error',
|
||||
url: '/errors/internal-server-error',
|
||||
icon: ServerOff,
|
||||
},
|
||||
{
|
||||
title: 'Maintenance Error',
|
||||
url: '/errors/maintenance-error',
|
||||
icon: Construction,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Other',
|
||||
items: [
|
||||
{
|
||||
title: 'Settings',
|
||||
icon: Settings,
|
||||
items: [
|
||||
{
|
||||
title: 'Profile',
|
||||
url: '/settings',
|
||||
icon: UserCog,
|
||||
},
|
||||
{
|
||||
title: 'Account',
|
||||
url: '/settings/account',
|
||||
icon: Wrench,
|
||||
},
|
||||
{
|
||||
title: 'Appearance',
|
||||
url: '/settings/appearance',
|
||||
icon: Palette,
|
||||
},
|
||||
{
|
||||
title: 'Notifications',
|
||||
url: '/settings/notifications',
|
||||
icon: Bell,
|
||||
},
|
||||
{
|
||||
title: 'Display',
|
||||
url: '/settings/display',
|
||||
icon: Monitor,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Help Center',
|
||||
url: '/help-center',
|
||||
icon: HelpCircle,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
50
shadcn-admin/src/components/layout/header.tsx
Normal file
50
shadcn-admin/src/components/layout/header.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { SidebarTrigger } from '@/components/ui/sidebar'
|
||||
|
||||
type HeaderProps = React.HTMLAttributes<HTMLElement> & {
|
||||
fixed?: boolean
|
||||
ref?: React.Ref<HTMLElement>
|
||||
}
|
||||
|
||||
export function Header({ className, fixed, children, ...props }: HeaderProps) {
|
||||
const [offset, setOffset] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
const onScroll = () => {
|
||||
setOffset(document.body.scrollTop || document.documentElement.scrollTop)
|
||||
}
|
||||
|
||||
// Add scroll listener to the body
|
||||
document.addEventListener('scroll', onScroll, { passive: true })
|
||||
|
||||
// Clean up the event listener on unmount
|
||||
return () => document.removeEventListener('scroll', onScroll)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<header
|
||||
className={cn(
|
||||
'z-50 h-16',
|
||||
fixed && 'header-fixed peer/header sticky top-0 w-[inherit]',
|
||||
offset > 10 && fixed ? 'shadow' : 'shadow-none',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'relative flex h-full items-center gap-3 p-4 sm:gap-4',
|
||||
offset > 10 &&
|
||||
fixed &&
|
||||
'after:absolute after:inset-0 after:-z-10 after:bg-background/20 after:backdrop-blur-lg'
|
||||
)}
|
||||
>
|
||||
<SidebarTrigger variant='outline' className='max-md:scale-125' />
|
||||
<Separator orientation='vertical' className='h-6' />
|
||||
{children}
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
27
shadcn-admin/src/components/layout/main.tsx
Normal file
27
shadcn-admin/src/components/layout/main.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
type MainProps = React.HTMLAttributes<HTMLElement> & {
|
||||
fixed?: boolean
|
||||
fluid?: boolean
|
||||
ref?: React.Ref<HTMLElement>
|
||||
}
|
||||
|
||||
export function Main({ fixed, className, fluid, ...props }: MainProps) {
|
||||
return (
|
||||
<main
|
||||
data-layout={fixed ? 'fixed' : 'auto'}
|
||||
className={cn(
|
||||
'px-4 py-6',
|
||||
|
||||
// If layout is fixed, make the main container flex and grow
|
||||
fixed && 'flex grow flex-col overflow-hidden',
|
||||
|
||||
// If layout is not fluid, set the max-width
|
||||
!fluid &&
|
||||
'@7xl/content:mx-auto @7xl/content:w-full @7xl/content:max-w-7xl',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
185
shadcn-admin/src/components/layout/nav-group.tsx
Normal file
185
shadcn-admin/src/components/layout/nav-group.tsx
Normal file
@@ -0,0 +1,185 @@
|
||||
import { type ReactNode } from 'react'
|
||||
import { Link, useLocation } from '@tanstack/react-router'
|
||||
import { ChevronRight } from 'lucide-react'
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from '@/components/ui/collapsible'
|
||||
import {
|
||||
SidebarGroup,
|
||||
SidebarGroupLabel,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
SidebarMenuSub,
|
||||
SidebarMenuSubButton,
|
||||
SidebarMenuSubItem,
|
||||
useSidebar,
|
||||
} from '@/components/ui/sidebar'
|
||||
import { Badge } from '../ui/badge'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '../ui/dropdown-menu'
|
||||
import {
|
||||
type NavCollapsible,
|
||||
type NavItem,
|
||||
type NavLink,
|
||||
type NavGroup as NavGroupProps,
|
||||
} from './types'
|
||||
|
||||
export function NavGroup({ title, items }: NavGroupProps) {
|
||||
const { state, isMobile } = useSidebar()
|
||||
const href = useLocation({ select: (location) => location.href })
|
||||
return (
|
||||
<SidebarGroup>
|
||||
<SidebarGroupLabel>{title}</SidebarGroupLabel>
|
||||
<SidebarMenu>
|
||||
{items.map((item) => {
|
||||
const key = `${item.title}-${item.url}`
|
||||
|
||||
if (!item.items)
|
||||
return <SidebarMenuLink key={key} item={item} href={href} />
|
||||
|
||||
if (state === 'collapsed' && !isMobile)
|
||||
return (
|
||||
<SidebarMenuCollapsedDropdown key={key} item={item} href={href} />
|
||||
)
|
||||
|
||||
return <SidebarMenuCollapsible key={key} item={item} href={href} />
|
||||
})}
|
||||
</SidebarMenu>
|
||||
</SidebarGroup>
|
||||
)
|
||||
}
|
||||
|
||||
function NavBadge({ children }: { children: ReactNode }) {
|
||||
return <Badge className='rounded-full px-1 py-0 text-xs'>{children}</Badge>
|
||||
}
|
||||
|
||||
function SidebarMenuLink({ item, href }: { item: NavLink; href: string }) {
|
||||
const { setOpenMobile } = useSidebar()
|
||||
return (
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton
|
||||
asChild
|
||||
isActive={checkIsActive(href, item)}
|
||||
tooltip={item.title}
|
||||
>
|
||||
<Link to={item.url} onClick={() => setOpenMobile(false)}>
|
||||
{item.icon && <item.icon />}
|
||||
<span>{item.title}</span>
|
||||
{item.badge && <NavBadge>{item.badge}</NavBadge>}
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarMenuCollapsible({
|
||||
item,
|
||||
href,
|
||||
}: {
|
||||
item: NavCollapsible
|
||||
href: string
|
||||
}) {
|
||||
const { setOpenMobile } = useSidebar()
|
||||
return (
|
||||
<Collapsible
|
||||
asChild
|
||||
defaultOpen={checkIsActive(href, item, true)}
|
||||
className='group/collapsible'
|
||||
>
|
||||
<SidebarMenuItem>
|
||||
<CollapsibleTrigger asChild>
|
||||
<SidebarMenuButton tooltip={item.title}>
|
||||
{item.icon && <item.icon />}
|
||||
<span>{item.title}</span>
|
||||
{item.badge && <NavBadge>{item.badge}</NavBadge>}
|
||||
<ChevronRight className='ms-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90 rtl:rotate-180' />
|
||||
</SidebarMenuButton>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className='CollapsibleContent'>
|
||||
<SidebarMenuSub>
|
||||
{item.items.map((subItem) => (
|
||||
<SidebarMenuSubItem key={subItem.title}>
|
||||
<SidebarMenuSubButton
|
||||
asChild
|
||||
isActive={checkIsActive(href, subItem)}
|
||||
>
|
||||
<Link to={subItem.url} onClick={() => setOpenMobile(false)}>
|
||||
{subItem.icon && <subItem.icon />}
|
||||
<span>{subItem.title}</span>
|
||||
{subItem.badge && <NavBadge>{subItem.badge}</NavBadge>}
|
||||
</Link>
|
||||
</SidebarMenuSubButton>
|
||||
</SidebarMenuSubItem>
|
||||
))}
|
||||
</SidebarMenuSub>
|
||||
</CollapsibleContent>
|
||||
</SidebarMenuItem>
|
||||
</Collapsible>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarMenuCollapsedDropdown({
|
||||
item,
|
||||
href,
|
||||
}: {
|
||||
item: NavCollapsible
|
||||
href: string
|
||||
}) {
|
||||
return (
|
||||
<SidebarMenuItem>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<SidebarMenuButton
|
||||
tooltip={item.title}
|
||||
isActive={checkIsActive(href, item)}
|
||||
>
|
||||
{item.icon && <item.icon />}
|
||||
<span>{item.title}</span>
|
||||
{item.badge && <NavBadge>{item.badge}</NavBadge>}
|
||||
<ChevronRight className='ms-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90' />
|
||||
</SidebarMenuButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent side='right' align='start' sideOffset={4}>
|
||||
<DropdownMenuLabel>
|
||||
{item.title} {item.badge ? `(${item.badge})` : ''}
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
{item.items.map((sub) => (
|
||||
<DropdownMenuItem key={`${sub.title}-${sub.url}`} asChild>
|
||||
<Link
|
||||
to={sub.url}
|
||||
className={`${checkIsActive(href, sub) ? 'bg-secondary' : ''}`}
|
||||
>
|
||||
{sub.icon && <sub.icon />}
|
||||
<span className='max-w-52 text-wrap'>{sub.title}</span>
|
||||
{sub.badge && (
|
||||
<span className='ms-auto text-xs'>{sub.badge}</span>
|
||||
)}
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</SidebarMenuItem>
|
||||
)
|
||||
}
|
||||
|
||||
function checkIsActive(href: string, item: NavItem, mainNav = false) {
|
||||
return (
|
||||
href === item.url || // /endpint?search=param
|
||||
href.split('?')[0] === item.url || // endpoint
|
||||
!!item?.items?.filter((i) => i.url === href).length || // if child nav is active
|
||||
(mainNav &&
|
||||
href.split('/')[1] !== '' &&
|
||||
href.split('/')[1] === item?.url?.split('/')[1])
|
||||
)
|
||||
}
|
||||
124
shadcn-admin/src/components/layout/nav-user.tsx
Normal file
124
shadcn-admin/src/components/layout/nav-user.tsx
Normal file
@@ -0,0 +1,124 @@
|
||||
import { Link } from '@tanstack/react-router'
|
||||
import {
|
||||
BadgeCheck,
|
||||
Bell,
|
||||
ChevronsUpDown,
|
||||
CreditCard,
|
||||
LogOut,
|
||||
Sparkles,
|
||||
} from 'lucide-react'
|
||||
import useDialogState from '@/hooks/use-dialog-state'
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import {
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
useSidebar,
|
||||
} from '@/components/ui/sidebar'
|
||||
import { SignOutDialog } from '@/components/sign-out-dialog'
|
||||
|
||||
type NavUserProps = {
|
||||
user: {
|
||||
name: string
|
||||
email: string
|
||||
avatar: string
|
||||
}
|
||||
}
|
||||
|
||||
export function NavUser({ user }: NavUserProps) {
|
||||
const { isMobile } = useSidebar()
|
||||
const [open, setOpen] = useDialogState()
|
||||
|
||||
return (
|
||||
<>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<SidebarMenuButton
|
||||
size='lg'
|
||||
className='data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground'
|
||||
>
|
||||
<Avatar className='h-8 w-8 rounded-lg'>
|
||||
<AvatarImage src={user.avatar} alt={user.name} />
|
||||
<AvatarFallback className='rounded-lg'>SN</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className='grid flex-1 text-start text-sm leading-tight'>
|
||||
<span className='truncate font-semibold'>{user.name}</span>
|
||||
<span className='truncate text-xs'>{user.email}</span>
|
||||
</div>
|
||||
<ChevronsUpDown className='ms-auto size-4' />
|
||||
</SidebarMenuButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
className='w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg'
|
||||
side={isMobile ? 'bottom' : 'right'}
|
||||
align='end'
|
||||
sideOffset={4}
|
||||
>
|
||||
<DropdownMenuLabel className='p-0 font-normal'>
|
||||
<div className='flex items-center gap-2 px-1 py-1.5 text-start text-sm'>
|
||||
<Avatar className='h-8 w-8 rounded-lg'>
|
||||
<AvatarImage src={user.avatar} alt={user.name} />
|
||||
<AvatarFallback className='rounded-lg'>SN</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className='grid flex-1 text-start text-sm leading-tight'>
|
||||
<span className='truncate font-semibold'>{user.name}</span>
|
||||
<span className='truncate text-xs'>{user.email}</span>
|
||||
</div>
|
||||
</div>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>
|
||||
<Sparkles />
|
||||
Upgrade to Pro
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link to='/settings/account'>
|
||||
<BadgeCheck />
|
||||
Account
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link to='/settings'>
|
||||
<CreditCard />
|
||||
Billing
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link to='/settings/notifications'>
|
||||
<Bell />
|
||||
Notifications
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
variant='destructive'
|
||||
onClick={() => setOpen(true)}
|
||||
>
|
||||
<LogOut />
|
||||
Sign out
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
|
||||
<SignOutDialog open={!!open} onOpenChange={setOpen} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
86
shadcn-admin/src/components/layout/team-switcher.tsx
Normal file
86
shadcn-admin/src/components/layout/team-switcher.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import * as React from 'react'
|
||||
import { ChevronsUpDown, Plus } from 'lucide-react'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import {
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
useSidebar,
|
||||
} from '@/components/ui/sidebar'
|
||||
|
||||
type TeamSwitcherProps = {
|
||||
teams: {
|
||||
name: string
|
||||
logo: React.ElementType
|
||||
plan: string
|
||||
}[]
|
||||
}
|
||||
|
||||
export function TeamSwitcher({ teams }: TeamSwitcherProps) {
|
||||
const { isMobile } = useSidebar()
|
||||
const [activeTeam, setActiveTeam] = React.useState(teams[0])
|
||||
|
||||
return (
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<SidebarMenuButton
|
||||
size='lg'
|
||||
className='data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground'
|
||||
>
|
||||
<div className='flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground'>
|
||||
<activeTeam.logo className='size-4' />
|
||||
</div>
|
||||
<div className='grid flex-1 text-start text-sm leading-tight'>
|
||||
<span className='truncate font-semibold'>
|
||||
{activeTeam.name}
|
||||
</span>
|
||||
<span className='truncate text-xs'>{activeTeam.plan}</span>
|
||||
</div>
|
||||
<ChevronsUpDown className='ms-auto' />
|
||||
</SidebarMenuButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
className='w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg'
|
||||
align='start'
|
||||
side={isMobile ? 'bottom' : 'right'}
|
||||
sideOffset={4}
|
||||
>
|
||||
<DropdownMenuLabel className='text-xs text-muted-foreground'>
|
||||
Teams
|
||||
</DropdownMenuLabel>
|
||||
{teams.map((team, index) => (
|
||||
<DropdownMenuItem
|
||||
key={team.name}
|
||||
onClick={() => setActiveTeam(team)}
|
||||
className='gap-2 p-2'
|
||||
>
|
||||
<div className='flex size-6 items-center justify-center rounded-sm border'>
|
||||
<team.logo className='size-4 shrink-0' />
|
||||
</div>
|
||||
{team.name}
|
||||
<DropdownMenuShortcut>⌘{index + 1}</DropdownMenuShortcut>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem className='gap-2 p-2'>
|
||||
<div className='flex size-6 items-center justify-center rounded-md border bg-background'>
|
||||
<Plus className='size-4' />
|
||||
</div>
|
||||
<div className='font-medium text-muted-foreground'>Add team</div>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
)
|
||||
}
|
||||
67
shadcn-admin/src/components/layout/top-nav.tsx
Normal file
67
shadcn-admin/src/components/layout/top-nav.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import { Link } from '@tanstack/react-router'
|
||||
import { Menu } from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
|
||||
type TopNavProps = React.HTMLAttributes<HTMLElement> & {
|
||||
links: {
|
||||
title: string
|
||||
href: string
|
||||
isActive: boolean
|
||||
disabled?: boolean
|
||||
}[]
|
||||
}
|
||||
|
||||
export function TopNav({ className, links, ...props }: TopNavProps) {
|
||||
return (
|
||||
<>
|
||||
<div className='lg:hidden'>
|
||||
<DropdownMenu modal={false}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button size='icon' variant='outline' className='md:size-7'>
|
||||
<Menu />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent side='bottom' align='start'>
|
||||
{links.map(({ title, href, isActive, disabled }) => (
|
||||
<DropdownMenuItem key={`${title}-${href}`} asChild>
|
||||
<Link
|
||||
to={href}
|
||||
className={!isActive ? 'text-muted-foreground' : ''}
|
||||
disabled={disabled}
|
||||
>
|
||||
{title}
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
|
||||
<nav
|
||||
className={cn(
|
||||
'hidden items-center space-x-4 lg:flex lg:space-x-4 xl:space-x-6',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{links.map(({ title, href, isActive, disabled }) => (
|
||||
<Link
|
||||
key={`${title}-${href}`}
|
||||
to={href}
|
||||
disabled={disabled}
|
||||
className={`text-sm font-medium transition-colors hover:text-primary ${isActive ? '' : 'text-muted-foreground'}`}
|
||||
>
|
||||
{title}
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
</>
|
||||
)
|
||||
}
|
||||
44
shadcn-admin/src/components/layout/types.ts
Normal file
44
shadcn-admin/src/components/layout/types.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { type LinkProps } from '@tanstack/react-router'
|
||||
|
||||
type User = {
|
||||
name: string
|
||||
email: string
|
||||
avatar: string
|
||||
}
|
||||
|
||||
type Team = {
|
||||
name: string
|
||||
logo: React.ElementType
|
||||
plan: string
|
||||
}
|
||||
|
||||
type BaseNavItem = {
|
||||
title: string
|
||||
badge?: string
|
||||
icon?: React.ElementType
|
||||
}
|
||||
|
||||
type NavLink = BaseNavItem & {
|
||||
url: LinkProps['to'] | (string & {})
|
||||
items?: never
|
||||
}
|
||||
|
||||
type NavCollapsible = BaseNavItem & {
|
||||
items: (BaseNavItem & { url: LinkProps['to'] | (string & {}) })[]
|
||||
url?: never
|
||||
}
|
||||
|
||||
type NavItem = NavCollapsible | NavLink
|
||||
|
||||
type NavGroup = {
|
||||
title: string
|
||||
items: NavItem[]
|
||||
}
|
||||
|
||||
type SidebarData = {
|
||||
user: User
|
||||
teams: Team[]
|
||||
navGroups: NavGroup[]
|
||||
}
|
||||
|
||||
export type { SidebarData, NavGroup, NavItem, NavCollapsible, NavLink }
|
||||
44
shadcn-admin/src/components/learn-more.tsx
Normal file
44
shadcn-admin/src/components/learn-more.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import { type Root, type Content, type Trigger } from '@radix-ui/react-popover'
|
||||
import { CircleQuestionMark } from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@/components/ui/popover'
|
||||
|
||||
type LearnMoreProps = React.ComponentProps<typeof Root> & {
|
||||
contentProps?: React.ComponentProps<typeof Content>
|
||||
triggerProps?: React.ComponentProps<typeof Trigger>
|
||||
}
|
||||
|
||||
export function LearnMore({
|
||||
children,
|
||||
contentProps,
|
||||
triggerProps,
|
||||
...props
|
||||
}: LearnMoreProps) {
|
||||
return (
|
||||
<Popover {...props}>
|
||||
<PopoverTrigger
|
||||
asChild
|
||||
{...triggerProps}
|
||||
className={cn('size-5 rounded-full', triggerProps?.className)}
|
||||
>
|
||||
<Button variant='outline' size='icon'>
|
||||
<span className='sr-only'>Learn more</span>
|
||||
<CircleQuestionMark className='size-4 [&>circle]:hidden' />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
side='top'
|
||||
align='start'
|
||||
{...contentProps}
|
||||
className={cn('text-sm text-muted-foreground', contentProps?.className)}
|
||||
>
|
||||
{children}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
84
shadcn-admin/src/components/long-text.tsx
Normal file
84
shadcn-admin/src/components/long-text.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
import { useRef, useState } from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@/components/ui/popover'
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip'
|
||||
|
||||
type LongTextProps = {
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
contentClassName?: string
|
||||
}
|
||||
|
||||
export function LongText({
|
||||
children,
|
||||
className = '',
|
||||
contentClassName = '',
|
||||
}: LongTextProps) {
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
const [isOverflown, setIsOverflown] = useState(false)
|
||||
|
||||
// Use ref callback to check overflow when element is mounted
|
||||
const refCallback = (node: HTMLDivElement | null) => {
|
||||
ref.current = node
|
||||
if (node && checkOverflow(node)) {
|
||||
queueMicrotask(() => setIsOverflown(true))
|
||||
}
|
||||
}
|
||||
|
||||
if (!isOverflown)
|
||||
return (
|
||||
<div ref={refCallback} className={cn('truncate', className)}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='hidden sm:block'>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div ref={refCallback} className={cn('truncate', className)}>
|
||||
{children}
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p className={contentClassName}>{children}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
<div className='sm:hidden'>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<div ref={refCallback} className={cn('truncate', className)}>
|
||||
{children}
|
||||
</div>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className={cn('w-fit', contentClassName)}>
|
||||
<p>{children}</p>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const checkOverflow = (textContainer: HTMLDivElement | null) => {
|
||||
if (textContainer) {
|
||||
return (
|
||||
textContainer.offsetHeight < textContainer.scrollHeight ||
|
||||
textContainer.offsetWidth < textContainer.scrollWidth
|
||||
)
|
||||
}
|
||||
return false
|
||||
}
|
||||
25
shadcn-admin/src/components/navigation-progress.tsx
Normal file
25
shadcn-admin/src/components/navigation-progress.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { useRouterState } from '@tanstack/react-router'
|
||||
import LoadingBar, { type LoadingBarRef } from 'react-top-loading-bar'
|
||||
|
||||
export function NavigationProgress() {
|
||||
const ref = useRef<LoadingBarRef>(null)
|
||||
const state = useRouterState()
|
||||
|
||||
useEffect(() => {
|
||||
if (state.status === 'pending') {
|
||||
ref.current?.continuousStart()
|
||||
} else {
|
||||
ref.current?.complete()
|
||||
}
|
||||
}, [state.status])
|
||||
|
||||
return (
|
||||
<LoadingBar
|
||||
color='var(--muted-foreground)'
|
||||
ref={ref}
|
||||
shadow={true}
|
||||
height={2}
|
||||
/>
|
||||
)
|
||||
}
|
||||
42
shadcn-admin/src/components/password-input.tsx
Normal file
42
shadcn-admin/src/components/password-input.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import * as React from 'react'
|
||||
import { Eye, EyeOff } from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Button } from './ui/button'
|
||||
|
||||
type PasswordInputProps = Omit<
|
||||
React.InputHTMLAttributes<HTMLInputElement>,
|
||||
'type'
|
||||
> & {
|
||||
ref?: React.Ref<HTMLInputElement>
|
||||
}
|
||||
|
||||
export function PasswordInput({
|
||||
className,
|
||||
disabled,
|
||||
ref,
|
||||
...props
|
||||
}: PasswordInputProps) {
|
||||
const [showPassword, setShowPassword] = React.useState(false)
|
||||
|
||||
return (
|
||||
<div className={cn('relative rounded-md', className)}>
|
||||
<input
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
className='flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-xs transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:ring-1 focus-visible:ring-ring focus-visible:outline-hidden disabled:cursor-not-allowed disabled:opacity-50'
|
||||
ref={ref}
|
||||
disabled={disabled}
|
||||
{...props}
|
||||
/>
|
||||
<Button
|
||||
type='button'
|
||||
size='icon'
|
||||
variant='ghost'
|
||||
disabled={disabled}
|
||||
className='absolute end-1 top-1/2 h-6 w-6 -translate-y-1/2 rounded-md text-muted-foreground'
|
||||
onClick={() => setShowPassword((prev) => !prev)}
|
||||
>
|
||||
{showPassword ? <Eye size={18} /> : <EyeOff size={18} />}
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
75
shadcn-admin/src/components/profile-dropdown.tsx
Normal file
75
shadcn-admin/src/components/profile-dropdown.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import { Link } from '@tanstack/react-router'
|
||||
import useDialogState from '@/hooks/use-dialog-state'
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import { SignOutDialog } from '@/components/sign-out-dialog'
|
||||
|
||||
export function ProfileDropdown() {
|
||||
const [open, setOpen] = useDialogState()
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownMenu modal={false}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant='ghost' className='relative h-8 w-8 rounded-full'>
|
||||
<Avatar className='h-8 w-8'>
|
||||
<AvatarImage src='/avatars/01.png' alt='@shadcn' />
|
||||
<AvatarFallback>SN</AvatarFallback>
|
||||
</Avatar>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className='w-56' align='end' forceMount>
|
||||
<DropdownMenuLabel className='font-normal'>
|
||||
<div className='flex flex-col gap-1.5'>
|
||||
<p className='text-sm leading-none font-medium'>satnaing</p>
|
||||
<p className='text-xs leading-none text-muted-foreground'>
|
||||
satnaingdev@gmail.com
|
||||
</p>
|
||||
</div>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link to='/settings'>
|
||||
Profile
|
||||
<DropdownMenuShortcut>⇧⌘P</DropdownMenuShortcut>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link to='/settings'>
|
||||
Billing
|
||||
<DropdownMenuShortcut>⌘B</DropdownMenuShortcut>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link to='/settings'>
|
||||
Settings
|
||||
<DropdownMenuShortcut>⌘S</DropdownMenuShortcut>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>New Team</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem variant='destructive' onClick={() => setOpen(true)}>
|
||||
Sign out
|
||||
<DropdownMenuShortcut className='text-current'>
|
||||
⇧⌘Q
|
||||
</DropdownMenuShortcut>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
<SignOutDialog open={!!open} onOpenChange={setOpen} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
37
shadcn-admin/src/components/search.tsx
Normal file
37
shadcn-admin/src/components/search.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import { SearchIcon } from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useSearch } from '@/context/search-provider'
|
||||
import { Button } from './ui/button'
|
||||
|
||||
type SearchProps = {
|
||||
className?: string
|
||||
type?: React.HTMLInputTypeAttribute
|
||||
placeholder?: string
|
||||
}
|
||||
|
||||
export function Search({
|
||||
className = '',
|
||||
placeholder = 'Search',
|
||||
}: SearchProps) {
|
||||
const { setOpen } = useSearch()
|
||||
return (
|
||||
<Button
|
||||
variant='outline'
|
||||
className={cn(
|
||||
'group relative h-8 w-full flex-1 justify-start rounded-md bg-muted/25 text-sm font-normal text-muted-foreground shadow-none hover:bg-accent sm:w-40 sm:pe-12 md:flex-none lg:w-52 xl:w-64',
|
||||
className
|
||||
)}
|
||||
onClick={() => setOpen(true)}
|
||||
>
|
||||
<SearchIcon
|
||||
aria-hidden='true'
|
||||
className='absolute start-1.5 top-1/2 -translate-y-1/2'
|
||||
size={16}
|
||||
/>
|
||||
<span className='ms-4'>{placeholder}</span>
|
||||
<kbd className='pointer-events-none absolute end-[0.3rem] top-[0.3rem] hidden h-5 items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium opacity-100 select-none group-hover:bg-accent sm:flex'>
|
||||
<span className='text-xs'>⌘</span>K
|
||||
</kbd>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
62
shadcn-admin/src/components/select-dropdown.tsx
Normal file
62
shadcn-admin/src/components/select-dropdown.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import { Loader } from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { FormControl } from '@/components/ui/form'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
|
||||
type SelectDropdownProps = {
|
||||
onValueChange?: (value: string) => void
|
||||
defaultValue: string | undefined
|
||||
placeholder?: string
|
||||
isPending?: boolean
|
||||
items: { label: string; value: string }[] | undefined
|
||||
disabled?: boolean
|
||||
className?: string
|
||||
isControlled?: boolean
|
||||
}
|
||||
|
||||
export function SelectDropdown({
|
||||
defaultValue,
|
||||
onValueChange,
|
||||
isPending,
|
||||
items,
|
||||
placeholder,
|
||||
disabled,
|
||||
className = '',
|
||||
isControlled = false,
|
||||
}: SelectDropdownProps) {
|
||||
const defaultState = isControlled
|
||||
? { value: defaultValue, onValueChange }
|
||||
: { defaultValue, onValueChange }
|
||||
return (
|
||||
<Select {...defaultState}>
|
||||
<FormControl>
|
||||
<SelectTrigger disabled={disabled} className={cn(className)}>
|
||||
<SelectValue placeholder={placeholder ?? 'Select'} />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{isPending ? (
|
||||
<SelectItem disabled value='loading' className='h-14'>
|
||||
<div className='flex items-center justify-center gap-2'>
|
||||
<Loader className='h-5 w-5 animate-spin' />
|
||||
{' '}
|
||||
Loading...
|
||||
</div>
|
||||
</SelectItem>
|
||||
) : (
|
||||
items?.map(({ label, value }) => (
|
||||
<SelectItem key={value} value={value}>
|
||||
{label}
|
||||
</SelectItem>
|
||||
))
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)
|
||||
}
|
||||
38
shadcn-admin/src/components/sign-out-dialog.tsx
Normal file
38
shadcn-admin/src/components/sign-out-dialog.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import { useNavigate, useLocation } from '@tanstack/react-router'
|
||||
import { useAuthStore } from '@/stores/auth-store'
|
||||
import { ConfirmDialog } from '@/components/confirm-dialog'
|
||||
|
||||
interface SignOutDialogProps {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
}
|
||||
|
||||
export function SignOutDialog({ open, onOpenChange }: SignOutDialogProps) {
|
||||
const navigate = useNavigate()
|
||||
const location = useLocation()
|
||||
const { auth } = useAuthStore()
|
||||
|
||||
const handleSignOut = () => {
|
||||
auth.reset()
|
||||
// Preserve current location for redirect after sign-in
|
||||
const currentPath = location.href
|
||||
navigate({
|
||||
to: '/sign-in',
|
||||
search: { redirect: currentPath },
|
||||
replace: true,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<ConfirmDialog
|
||||
open={open}
|
||||
onOpenChange={onOpenChange}
|
||||
title='Sign out'
|
||||
desc='Are you sure you want to sign out? You will need to sign in again to access your account.'
|
||||
confirmText='Sign out'
|
||||
destructive
|
||||
handleConfirm={handleSignOut}
|
||||
className='sm:max-w-sm'
|
||||
/>
|
||||
)
|
||||
}
|
||||
10
shadcn-admin/src/components/skip-to-main.tsx
Normal file
10
shadcn-admin/src/components/skip-to-main.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
export function SkipToMain() {
|
||||
return (
|
||||
<a
|
||||
className={`fixed start-44 z-999 -translate-y-52 bg-primary px-4 py-2 text-sm font-medium whitespace-nowrap text-primary-foreground opacity-95 shadow-sm transition hover:bg-primary/90 focus:translate-y-3 focus:transform focus-visible:ring-1 focus-visible:ring-ring`}
|
||||
href='#content'
|
||||
>
|
||||
Skip to Main
|
||||
</a>
|
||||
)
|
||||
}
|
||||
58
shadcn-admin/src/components/theme-switch.tsx
Normal file
58
shadcn-admin/src/components/theme-switch.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import { useEffect } from 'react'
|
||||
import { Check, Moon, Sun } from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useTheme } from '@/context/theme-provider'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
|
||||
export function ThemeSwitch() {
|
||||
const { theme, setTheme } = useTheme()
|
||||
|
||||
/* Update theme-color meta tag
|
||||
* when theme is updated */
|
||||
useEffect(() => {
|
||||
const themeColor = theme === 'dark' ? '#020817' : '#fff'
|
||||
const metaThemeColor = document.querySelector("meta[name='theme-color']")
|
||||
if (metaThemeColor) metaThemeColor.setAttribute('content', themeColor)
|
||||
}, [theme])
|
||||
|
||||
return (
|
||||
<DropdownMenu modal={false}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant='ghost' size='icon' className='scale-95 rounded-full'>
|
||||
<Sun className='size-[1.2rem] scale-100 rotate-0 transition-all dark:scale-0 dark:-rotate-90' />
|
||||
<Moon className='absolute size-[1.2rem] scale-0 rotate-90 transition-all dark:scale-100 dark:rotate-0' />
|
||||
<span className='sr-only'>Toggle theme</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align='end'>
|
||||
<DropdownMenuItem onClick={() => setTheme('light')}>
|
||||
Light{' '}
|
||||
<Check
|
||||
size={14}
|
||||
className={cn('ms-auto', theme !== 'light' && 'hidden')}
|
||||
/>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setTheme('dark')}>
|
||||
Dark
|
||||
<Check
|
||||
size={14}
|
||||
className={cn('ms-auto', theme !== 'dark' && 'hidden')}
|
||||
/>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setTheme('system')}>
|
||||
System
|
||||
<Check
|
||||
size={14}
|
||||
className={cn('ms-auto', theme !== 'system' && 'hidden')}
|
||||
/>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
||||
154
shadcn-admin/src/components/ui/alert-dialog.tsx
Normal file
154
shadcn-admin/src/components/ui/alert-dialog.tsx
Normal file
@@ -0,0 +1,154 @@
|
||||
import * as React from 'react'
|
||||
import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { buttonVariants } from '@/components/ui/button'
|
||||
|
||||
function AlertDialog({
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
|
||||
return <AlertDialogPrimitive.Root data-slot='alert-dialog' {...props} />
|
||||
}
|
||||
|
||||
function AlertDialogTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Trigger data-slot='alert-dialog-trigger' {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDialogPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Portal data-slot='alert-dialog-portal' {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDialogOverlay({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Overlay
|
||||
data-slot='alert-dialog-overlay'
|
||||
className={cn(
|
||||
'fixed inset-0 z-50 bg-black/50 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDialogContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Content>) {
|
||||
return (
|
||||
<AlertDialogPortal>
|
||||
<AlertDialogOverlay />
|
||||
<AlertDialogPrimitive.Content
|
||||
data-slot='alert-dialog-content'
|
||||
className={cn(
|
||||
'fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border bg-background p-6 shadow-lg duration-200 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 sm:max-w-lg',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</AlertDialogPortal>
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDialogHeader({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot='alert-dialog-header'
|
||||
className={cn('flex flex-col gap-2 text-center sm:text-start', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDialogFooter({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot='alert-dialog-footer'
|
||||
className={cn(
|
||||
'flex flex-col-reverse gap-2 sm:flex-row sm:justify-end',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDialogTitle({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Title
|
||||
data-slot='alert-dialog-title'
|
||||
className={cn('text-lg font-semibold', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDialogDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Description
|
||||
data-slot='alert-dialog-description'
|
||||
className={cn('text-sm text-muted-foreground', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDialogAction({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Action>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Action
|
||||
className={cn(buttonVariants(), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDialogCancel({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Cancel
|
||||
className={cn(buttonVariants({ variant: 'outline' }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
AlertDialog,
|
||||
AlertDialogPortal,
|
||||
AlertDialogOverlay,
|
||||
AlertDialogTrigger,
|
||||
AlertDialogContent,
|
||||
AlertDialogHeader,
|
||||
AlertDialogFooter,
|
||||
AlertDialogTitle,
|
||||
AlertDialogDescription,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
}
|
||||
65
shadcn-admin/src/components/ui/alert.tsx
Normal file
65
shadcn-admin/src/components/ui/alert.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import * as React from 'react'
|
||||
import { cva, type VariantProps } from 'class-variance-authority'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const alertVariants = cva(
|
||||
'relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'bg-card text-card-foreground',
|
||||
destructive:
|
||||
'text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Alert({
|
||||
className,
|
||||
variant,
|
||||
...props
|
||||
}: React.ComponentProps<'div'> & VariantProps<typeof alertVariants>) {
|
||||
return (
|
||||
<div
|
||||
data-slot='alert'
|
||||
role='alert'
|
||||
className={cn(alertVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AlertTitle({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot='alert-title'
|
||||
className={cn(
|
||||
'col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot='alert-description'
|
||||
className={cn(
|
||||
'col-start-2 grid justify-items-start gap-1 text-sm text-muted-foreground [&_p]:leading-relaxed',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Alert, AlertTitle, AlertDescription }
|
||||
50
shadcn-admin/src/components/ui/avatar.tsx
Normal file
50
shadcn-admin/src/components/ui/avatar.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import * as React from 'react'
|
||||
import * as AvatarPrimitive from '@radix-ui/react-avatar'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
function Avatar({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AvatarPrimitive.Root>) {
|
||||
return (
|
||||
<AvatarPrimitive.Root
|
||||
data-slot='avatar'
|
||||
className={cn(
|
||||
'relative flex size-8 shrink-0 overflow-hidden rounded-full',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AvatarImage({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
|
||||
return (
|
||||
<AvatarPrimitive.Image
|
||||
data-slot='avatar-image'
|
||||
className={cn('aspect-square size-full', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AvatarFallback({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
|
||||
return (
|
||||
<AvatarPrimitive.Fallback
|
||||
data-slot='avatar-fallback'
|
||||
className={cn(
|
||||
'flex size-full items-center justify-center rounded-full bg-muted',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Avatar, AvatarImage, AvatarFallback }
|
||||
45
shadcn-admin/src/components/ui/badge.tsx
Normal file
45
shadcn-admin/src/components/ui/badge.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import * as React from 'react'
|
||||
import { Slot } from '@radix-ui/react-slot'
|
||||
import { cva, type VariantProps } from 'class-variance-authority'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const badgeVariants = cva(
|
||||
'inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
'border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90',
|
||||
secondary:
|
||||
'border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90',
|
||||
destructive:
|
||||
'border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
|
||||
outline:
|
||||
'text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Badge({
|
||||
className,
|
||||
variant,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<'span'> &
|
||||
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
||||
const Comp = asChild ? Slot : 'span'
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot='badge'
|
||||
className={cn(badgeVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants }
|
||||
58
shadcn-admin/src/components/ui/button.tsx
Normal file
58
shadcn-admin/src/components/ui/button.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import * as React from 'react'
|
||||
import { Slot } from '@radix-ui/react-slot'
|
||||
import { cva, type VariantProps } from 'class-variance-authority'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90',
|
||||
destructive:
|
||||
'bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
|
||||
outline:
|
||||
'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50',
|
||||
secondary:
|
||||
'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80',
|
||||
ghost:
|
||||
'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
|
||||
link: 'text-primary underline-offset-4 hover:underline',
|
||||
},
|
||||
size: {
|
||||
default: 'h-9 px-4 py-2 has-[>svg]:px-3',
|
||||
sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5',
|
||||
lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
|
||||
icon: 'size-9',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
size: 'default',
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Button({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<'button'> &
|
||||
VariantProps<typeof buttonVariants> & {
|
||||
asChild?: boolean
|
||||
}) {
|
||||
const Comp = asChild ? Slot : 'button'
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot='button'
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Button, buttonVariants }
|
||||
210
shadcn-admin/src/components/ui/calendar.tsx
Normal file
210
shadcn-admin/src/components/ui/calendar.tsx
Normal file
@@ -0,0 +1,210 @@
|
||||
import * as React from 'react'
|
||||
import {
|
||||
ChevronDownIcon,
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
} from 'lucide-react'
|
||||
import { DayButton, DayPicker, getDefaultClassNames } from 'react-day-picker'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Button, buttonVariants } from '@/components/ui/button'
|
||||
|
||||
function Calendar({
|
||||
className,
|
||||
classNames,
|
||||
showOutsideDays = true,
|
||||
captionLayout = 'label',
|
||||
buttonVariant = 'ghost',
|
||||
formatters,
|
||||
components,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DayPicker> & {
|
||||
buttonVariant?: React.ComponentProps<typeof Button>['variant']
|
||||
}) {
|
||||
const defaultClassNames = getDefaultClassNames()
|
||||
|
||||
return (
|
||||
<DayPicker
|
||||
showOutsideDays={showOutsideDays}
|
||||
className={cn(
|
||||
'group/calendar bg-background p-3 [--cell-size:--spacing(8)] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent',
|
||||
String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
|
||||
String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
|
||||
className
|
||||
)}
|
||||
captionLayout={captionLayout}
|
||||
formatters={{
|
||||
formatMonthDropdown: (date) =>
|
||||
date.toLocaleString('default', { month: 'short' }),
|
||||
...formatters,
|
||||
}}
|
||||
classNames={{
|
||||
root: cn('w-fit', defaultClassNames.root),
|
||||
months: cn(
|
||||
'flex gap-4 flex-col md:flex-row relative',
|
||||
defaultClassNames.months
|
||||
),
|
||||
month: cn('flex flex-col w-full gap-4', defaultClassNames.month),
|
||||
nav: cn(
|
||||
'flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between',
|
||||
defaultClassNames.nav
|
||||
),
|
||||
button_previous: cn(
|
||||
buttonVariants({ variant: buttonVariant }),
|
||||
'size-(--cell-size) aria-disabled:opacity-50 p-0 select-none',
|
||||
defaultClassNames.button_previous
|
||||
),
|
||||
button_next: cn(
|
||||
buttonVariants({ variant: buttonVariant }),
|
||||
'size-(--cell-size) aria-disabled:opacity-50 p-0 select-none',
|
||||
defaultClassNames.button_next
|
||||
),
|
||||
month_caption: cn(
|
||||
'flex items-center justify-center h-(--cell-size) w-full px-(--cell-size)',
|
||||
defaultClassNames.month_caption
|
||||
),
|
||||
dropdowns: cn(
|
||||
'w-full flex items-center text-sm font-medium justify-center h-(--cell-size) gap-1.5',
|
||||
defaultClassNames.dropdowns
|
||||
),
|
||||
dropdown_root: cn(
|
||||
'relative has-focus:border-ring border border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] rounded-md',
|
||||
defaultClassNames.dropdown_root
|
||||
),
|
||||
dropdown: cn(
|
||||
'absolute bg-popover inset-0 opacity-0',
|
||||
defaultClassNames.dropdown
|
||||
),
|
||||
caption_label: cn(
|
||||
'select-none font-medium',
|
||||
captionLayout === 'label'
|
||||
? 'text-sm'
|
||||
: 'rounded-md ps-2 pe-1 flex items-center gap-1 text-sm h-8 [&>svg]:text-muted-foreground [&>svg]:size-3.5',
|
||||
defaultClassNames.caption_label
|
||||
),
|
||||
table: 'w-full border-collapse',
|
||||
weekdays: cn('flex', defaultClassNames.weekdays),
|
||||
weekday: cn(
|
||||
'text-muted-foreground rounded-md flex-1 font-normal text-[0.8rem] select-none',
|
||||
defaultClassNames.weekday
|
||||
),
|
||||
week: cn('flex w-full mt-2', defaultClassNames.week),
|
||||
week_number_header: cn(
|
||||
'select-none w-(--cell-size)',
|
||||
defaultClassNames.week_number_header
|
||||
),
|
||||
week_number: cn(
|
||||
'text-[0.8rem] select-none text-muted-foreground',
|
||||
defaultClassNames.week_number
|
||||
),
|
||||
day: cn(
|
||||
'relative w-full h-full p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none',
|
||||
defaultClassNames.day
|
||||
),
|
||||
range_start: cn(
|
||||
'rounded-l-md bg-accent',
|
||||
defaultClassNames.range_start
|
||||
),
|
||||
range_middle: cn('rounded-none', defaultClassNames.range_middle),
|
||||
range_end: cn('rounded-r-md bg-accent', defaultClassNames.range_end),
|
||||
today: cn(
|
||||
'bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none',
|
||||
defaultClassNames.today
|
||||
),
|
||||
outside: cn(
|
||||
'text-muted-foreground aria-selected:text-muted-foreground',
|
||||
defaultClassNames.outside
|
||||
),
|
||||
disabled: cn(
|
||||
'text-muted-foreground opacity-50',
|
||||
defaultClassNames.disabled
|
||||
),
|
||||
hidden: cn('invisible', defaultClassNames.hidden),
|
||||
...classNames,
|
||||
}}
|
||||
components={{
|
||||
Root: ({ className, rootRef, ...props }) => {
|
||||
return (
|
||||
<div
|
||||
data-slot='calendar'
|
||||
ref={rootRef}
|
||||
className={cn(className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
},
|
||||
Chevron: ({ className, orientation, ...props }) => {
|
||||
if (orientation === 'left') {
|
||||
return (
|
||||
<ChevronLeftIcon className={cn('size-4', className)} {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
if (orientation === 'right') {
|
||||
return (
|
||||
<ChevronRightIcon
|
||||
className={cn('size-4', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<ChevronDownIcon className={cn('size-4', className)} {...props} />
|
||||
)
|
||||
},
|
||||
DayButton: CalendarDayButton,
|
||||
WeekNumber: ({ children, ...props }) => {
|
||||
return (
|
||||
<td {...props}>
|
||||
<div className='flex size-(--cell-size) items-center justify-center text-center'>
|
||||
{children}
|
||||
</div>
|
||||
</td>
|
||||
)
|
||||
},
|
||||
...components,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CalendarDayButton({
|
||||
className,
|
||||
day,
|
||||
modifiers,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DayButton>) {
|
||||
const defaultClassNames = getDefaultClassNames()
|
||||
|
||||
const ref = React.useRef<HTMLButtonElement>(null)
|
||||
React.useEffect(() => {
|
||||
if (modifiers.focused) ref.current?.focus()
|
||||
}, [modifiers.focused])
|
||||
|
||||
return (
|
||||
<Button
|
||||
ref={ref}
|
||||
variant='ghost'
|
||||
size='icon'
|
||||
data-day={day.date.toLocaleDateString()}
|
||||
data-selected-single={
|
||||
modifiers.selected &&
|
||||
!modifiers.range_start &&
|
||||
!modifiers.range_end &&
|
||||
!modifiers.range_middle
|
||||
}
|
||||
data-range-start={modifiers.range_start}
|
||||
data-range-end={modifiers.range_end}
|
||||
data-range-middle={modifiers.range_middle}
|
||||
className={cn(
|
||||
'flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-[3px] group-data-[focused=true]/day:ring-ring/50 data-[range-end=true]:rounded-md data-[range-end=true]:rounded-r-md data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground data-[range-middle=true]:rounded-none data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:rounded-md data-[range-start=true]:rounded-l-md data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground dark:hover:text-accent-foreground [&>span]:text-xs [&>span]:opacity-70',
|
||||
defaultClassNames.day,
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Calendar, CalendarDayButton }
|
||||
91
shadcn-admin/src/components/ui/card.tsx
Normal file
91
shadcn-admin/src/components/ui/card.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
import * as React from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
function Card({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot='card'
|
||||
className={cn(
|
||||
'flex flex-col gap-6 rounded-xl border bg-card py-6 text-card-foreground shadow-sm',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardHeader({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot='card-header'
|
||||
className={cn(
|
||||
'@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardTitle({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot='card-title'
|
||||
className={cn('leading-none font-semibold', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardDescription({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot='card-description'
|
||||
className={cn('text-sm text-muted-foreground', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardAction({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot='card-action'
|
||||
className={cn(
|
||||
'col-start-2 row-span-2 row-start-1 self-start justify-self-end',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardContent({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot='card-content'
|
||||
className={cn('px-6', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardFooter({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot='card-footer'
|
||||
className={cn('flex items-center px-6 [.border-t]:pt-6', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardFooter,
|
||||
CardTitle,
|
||||
CardAction,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
}
|
||||
29
shadcn-admin/src/components/ui/checkbox.tsx
Normal file
29
shadcn-admin/src/components/ui/checkbox.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import * as React from 'react'
|
||||
import * as CheckboxPrimitive from '@radix-ui/react-checkbox'
|
||||
import { CheckIcon } from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
function Checkbox({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
|
||||
return (
|
||||
<CheckboxPrimitive.Root
|
||||
data-slot='checkbox'
|
||||
className={cn(
|
||||
'peer size-4 shrink-0 rounded-[4px] border border-input shadow-xs transition-shadow outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:bg-input/30 dark:aria-invalid:ring-destructive/40 dark:data-[state=checked]:bg-primary',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator
|
||||
data-slot='checkbox-indicator'
|
||||
className='flex items-center justify-center text-current transition-none'
|
||||
>
|
||||
<CheckIcon className='size-3.5' />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
)
|
||||
}
|
||||
|
||||
export { Checkbox }
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user