mirror of
https://github.com/shadcn-ui/ui.git
synced 2026-06-12 02:11:33 +00:00
Compare commits
314 Commits
shadcn@2.0
...
v3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0826d58d3a | ||
|
|
995b6fd1a8 | ||
|
|
410998525b | ||
|
|
0f18c2775a | ||
|
|
b84c990e42 | ||
|
|
2773f9e2e2 | ||
|
|
c41c6ece86 | ||
|
|
11267f2fed | ||
|
|
9ad24d6a16 | ||
|
|
e8468793fc | ||
|
|
6f702f5fbf | ||
|
|
d0306774fe | ||
|
|
f1e5cc4666 | ||
|
|
e3ca257f6e | ||
|
|
d1a36d3e17 | ||
|
|
617483fe9c | ||
|
|
6d2728db2e | ||
|
|
625be136f4 | ||
|
|
1bd209a4db | ||
|
|
812e2300f1 | ||
|
|
b52fa4559c | ||
|
|
2fade2326a | ||
|
|
a6034127f9 | ||
|
|
eaf8156fc0 | ||
|
|
754a66061d | ||
|
|
cc53c2243e | ||
|
|
99fbf4cb77 | ||
|
|
a82db8395d | ||
|
|
79eed50e5b | ||
|
|
799a7f8aa7 | ||
|
|
79a6f54d4e | ||
|
|
26edb8275e | ||
|
|
e15e31e3cb | ||
|
|
d4247d52da | ||
|
|
9d908ae6ca | ||
|
|
883ad8cbc4 | ||
|
|
074eed5605 | ||
|
|
ca7fbc3b64 | ||
|
|
b3b2fe2755 | ||
|
|
1fcb318c56 | ||
|
|
2a783175c5 | ||
|
|
69fc8e23cc | ||
|
|
84ec2f6079 | ||
|
|
37e5192fe7 | ||
|
|
205bfc637e | ||
|
|
9eae13639c | ||
|
|
c77ddfc8b7 | ||
|
|
516ed3d17e | ||
|
|
bbda8c0b5a | ||
|
|
d5be468d32 | ||
|
|
9f094a13b9 | ||
|
|
829ef0cbf6 | ||
|
|
4ebfd6896f | ||
|
|
911c52022a | ||
|
|
61e21e3356 | ||
|
|
28d97b7fc1 | ||
|
|
5085e48241 | ||
|
|
b7afb5aa1e | ||
|
|
a6e2d3672f | ||
|
|
0f6efb9769 | ||
|
|
415b02b4bc | ||
|
|
f1dd9c6903 | ||
|
|
abde54987b | ||
|
|
79f2498b28 | ||
|
|
d999f803a8 | ||
|
|
1d8fe41d64 | ||
|
|
33b6146fde | ||
|
|
1d55420629 | ||
|
|
12d4cf2ab0 | ||
|
|
5466432b01 | ||
|
|
f9e4991f15 | ||
|
|
67e7364b3c | ||
|
|
2f6c3e74bb | ||
|
|
a106fc5412 | ||
|
|
19665adeed | ||
|
|
fc0e8c0c52 | ||
|
|
c16c58d0f9 | ||
|
|
a3fe5074c1 | ||
|
|
3baef994d7 | ||
|
|
a7b3dbf121 | ||
|
|
26dc03b545 | ||
|
|
be3c1a9a98 | ||
|
|
418b2d9e14 | ||
|
|
7eb77fb3b9 | ||
|
|
f759a25354 | ||
|
|
0b288883d2 | ||
|
|
cf4ccb34e2 | ||
|
|
dc80a326c2 | ||
|
|
92e5c7ab2c | ||
|
|
d44971b6c2 | ||
|
|
8539dd6eec | ||
|
|
bc7df68620 | ||
|
|
1832f258bd | ||
|
|
e2730f7276 | ||
|
|
22ba26152a | ||
|
|
76d6a59f9f | ||
|
|
3a2a87386f | ||
|
|
1822d95883 | ||
|
|
f90f5148eb | ||
|
|
99b0a5ac90 | ||
|
|
984cb2a1ea | ||
|
|
fed2bac1d9 | ||
|
|
1ebfcd7cd9 | ||
|
|
535a7d9220 | ||
|
|
e7f8cd4566 | ||
|
|
8506977f83 | ||
|
|
575c0214da | ||
|
|
a1ab28bf58 | ||
|
|
190ae2dcd8 | ||
|
|
d5920cc3c1 | ||
|
|
89652889db | ||
|
|
57d15bb2d5 | ||
|
|
779517a1d4 | ||
|
|
b567f7a6c1 | ||
|
|
839afa714f | ||
|
|
32f0bc0de9 | ||
|
|
7d2499c803 | ||
|
|
84b88440c3 | ||
|
|
a5122f9029 | ||
|
|
3b90317e3c | ||
|
|
9d7c7b8978 | ||
|
|
957df8997c | ||
|
|
460240a86c | ||
|
|
bfffb08f42 | ||
|
|
5282332e52 | ||
|
|
3db8a07b3f | ||
|
|
187959435e | ||
|
|
5953229417 | ||
|
|
d6159023ed | ||
|
|
b0774b0578 | ||
|
|
7cefabbe98 | ||
|
|
3740373f99 | ||
|
|
9c4419f249 | ||
|
|
ea9b81594d | ||
|
|
4810f744e3 | ||
|
|
38c5fb4ace | ||
|
|
f37425bdb0 | ||
|
|
592ef33658 | ||
|
|
1a6da427ff | ||
|
|
bd8533bd26 | ||
|
|
202131cd7b | ||
|
|
7977975c9d | ||
|
|
a23fec1c31 | ||
|
|
2af2979cf4 | ||
|
|
aee53d90e2 | ||
|
|
1a68715048 | ||
|
|
34c99c1728 | ||
|
|
86642840e2 | ||
|
|
fac0daac7b | ||
|
|
13fe24e1f8 | ||
|
|
1eb2d74d7c | ||
|
|
16d4d38f56 | ||
|
|
5234c46722 | ||
|
|
f9037239af | ||
|
|
14e4726400 | ||
|
|
9f4d65fc8f | ||
|
|
1e357cb20d | ||
|
|
6339aaa315 | ||
|
|
e2caa3cd35 | ||
|
|
c74a094f14 | ||
|
|
bfb5b6357e | ||
|
|
ac3d965c6e | ||
|
|
6e7cd11d68 | ||
|
|
a75b4ca80e | ||
|
|
475abbbb20 | ||
|
|
762cd73554 | ||
|
|
8c06932800 | ||
|
|
e516481394 | ||
|
|
d1eb24e23a | ||
|
|
9a14c1d092 | ||
|
|
5ef2bc5f45 | ||
|
|
8f6a64f176 | ||
|
|
e85920c191 | ||
|
|
c8c4027b6b | ||
|
|
699195ba77 | ||
|
|
9643db42cf | ||
|
|
dd71498762 | ||
|
|
ddf761e802 | ||
|
|
5f7957ab51 | ||
|
|
f07c7ad5d0 | ||
|
|
d5aa527f0b | ||
|
|
5ec990a474 | ||
|
|
cb742e9825 | ||
|
|
254198b4bf | ||
|
|
1081536246 | ||
|
|
811bb59a8f | ||
|
|
ea677cc74e | ||
|
|
9253b43f29 | ||
|
|
c8fda09a63 | ||
|
|
1ff01b1bb5 | ||
|
|
f10d59fee9 | ||
|
|
2f869a2590 | ||
|
|
102b0b0c62 | ||
|
|
387756c4ab | ||
|
|
05145e66d3 | ||
|
|
704991247c | ||
|
|
729b9ec8ca | ||
|
|
a1bed464f3 | ||
|
|
805ed4120a | ||
|
|
600a593c87 | ||
|
|
500dbe2664 | ||
|
|
c577ee0666 | ||
|
|
d5bf0018fd | ||
|
|
fb36ca4159 | ||
|
|
824a35ada1 | ||
|
|
8d520c8d49 | ||
|
|
0873835339 | ||
|
|
4a0d4cfdb9 | ||
|
|
c4c5d8d419 | ||
|
|
9253682b87 | ||
|
|
c7cd16a637 | ||
|
|
eff1918d41 | ||
|
|
366d6b656b | ||
|
|
e489c5e08e | ||
|
|
d87003e0a4 | ||
|
|
a8633075f7 | ||
|
|
432d5e6e28 | ||
|
|
8f0c26f22a | ||
|
|
149b321c1b | ||
|
|
c1ae5a57cc | ||
|
|
b8ed303d8c | ||
|
|
13c97acf9f | ||
|
|
bed277c54d | ||
|
|
f7c42169a6 | ||
|
|
2c2fe97eb9 | ||
|
|
d64374d009 | ||
|
|
e24e51a2fa | ||
|
|
cdfecd1d97 | ||
|
|
2c043e709f | ||
|
|
aed19aa911 | ||
|
|
9e35d229ae | ||
|
|
db1975ef4d | ||
|
|
961e0b62d7 | ||
|
|
70c684c224 | ||
|
|
4ff64ba818 | ||
|
|
500a353816 | ||
|
|
c830780d62 | ||
|
|
debd51a854 | ||
|
|
78426dd862 | ||
|
|
6e47a94a8f | ||
|
|
ab6a856930 | ||
|
|
b33d3868e9 | ||
|
|
9e0a86122a | ||
|
|
2b276de95a | ||
|
|
64739f8399 | ||
|
|
f0cff7e0eb | ||
|
|
e242adaa9c | ||
|
|
986c00ee0e | ||
|
|
d0eece06d4 | ||
|
|
0a0a566a4e | ||
|
|
bf5a79c4d4 | ||
|
|
f02b412478 | ||
|
|
0d31293c7b | ||
|
|
3d1d19fc1b | ||
|
|
e5b56c84a9 | ||
|
|
630afe836e | ||
|
|
52c12bc27a | ||
|
|
182f2083cd | ||
|
|
3febcdc523 | ||
|
|
ced2513137 | ||
|
|
93ae8bd67f | ||
|
|
3259fb7ca1 | ||
|
|
d8397d80a8 | ||
|
|
06e74fce78 | ||
|
|
bc9e5eaaab | ||
|
|
c9b69d0836 | ||
|
|
539212c49e | ||
|
|
123887c36c | ||
|
|
35c1ba57c2 | ||
|
|
b34516f471 | ||
|
|
66b95402c1 | ||
|
|
5460177a7a | ||
|
|
b0049c2266 | ||
|
|
4e6e21f094 | ||
|
|
3b808c83be | ||
|
|
0e6b37e99a | ||
|
|
9ec433838f | ||
|
|
444ff70590 | ||
|
|
27bc5deff1 | ||
|
|
cacd7c8798 | ||
|
|
f227f93742 | ||
|
|
87e099a3d7 | ||
|
|
303d65718c | ||
|
|
909219df14 | ||
|
|
e8ada4e3c7 | ||
|
|
8fc80836ff | ||
|
|
d0a308cc64 | ||
|
|
e461c02389 | ||
|
|
50c2f6045a | ||
|
|
14aca65eee | ||
|
|
14c952b594 | ||
|
|
1e9434e6f9 | ||
|
|
f3d14c48cb | ||
|
|
c668c35bb9 | ||
|
|
1297abc882 | ||
|
|
36ebbf26dc | ||
|
|
a2abc4ad95 | ||
|
|
4b546bfb13 | ||
|
|
5fc9ade413 | ||
|
|
96880e7c9a | ||
|
|
7dfdb029e7 | ||
|
|
28f34ed3c3 | ||
|
|
bd54184e60 | ||
|
|
ce3adfa075 | ||
|
|
674807c1b4 | ||
|
|
c62167a449 | ||
|
|
061083006f | ||
|
|
1af66c2d08 | ||
|
|
0993d98cc7 | ||
|
|
52d223393a | ||
|
|
207b69fe8d | ||
|
|
408760a93b | ||
|
|
a9ab7afebf | ||
|
|
b6221ea524 |
@@ -7,5 +7,5 @@
|
||||
"access": "public",
|
||||
"baseBranch": "main",
|
||||
"updateInternalDependencies": "patch",
|
||||
"ignore": ["www", "**-template"]
|
||||
"ignore": ["www", "v4"]
|
||||
}
|
||||
|
||||
5
.changeset/honest-humans-invite.md
Normal file
5
.changeset/honest-humans-invite.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"shadcn": patch
|
||||
---
|
||||
|
||||
resolved prefixing issue for Tailwind CSS v4 compatibility
|
||||
8
.eslintignore
Normal file
8
.eslintignore
Normal file
@@ -0,0 +1,8 @@
|
||||
node_modules/
|
||||
target/
|
||||
.next/
|
||||
build/
|
||||
dist/
|
||||
|
||||
/templates/
|
||||
/fixtures/
|
||||
3
.github/FUNDING.yml
vendored
Normal file
3
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: [shadcn]
|
||||
6
.github/dependabot.yml
vendored
Normal file
6
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
9
.github/workflows/code-check.yml
vendored
9
.github/workflows/code-check.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: 20
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
name: Install pnpm
|
||||
@@ -52,7 +52,7 @@ jobs:
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: 20
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
name: Install pnpm
|
||||
@@ -90,7 +90,7 @@ jobs:
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: 20
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
name: Install pnpm
|
||||
@@ -113,4 +113,7 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Build packages
|
||||
run: pnpm --filter=shadcn build
|
||||
|
||||
- run: pnpm typecheck
|
||||
|
||||
35
.github/workflows/issue-stale.yml
vendored
35
.github/workflows/issue-stale.yml
vendored
@@ -1,5 +1,5 @@
|
||||
# Adapted from vercel/next.js
|
||||
name: Issue Stale
|
||||
name: "Stale issue handler"
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
@@ -11,16 +11,35 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository_owner == 'shadcn-ui'
|
||||
steps:
|
||||
- uses: actions/stale@v4
|
||||
id: stale-no-repro
|
||||
name: "Close stale issues with no reproduction"
|
||||
- uses: actions/stale@v9
|
||||
id: issue-stale
|
||||
name: "Mark stale issues, close stale issues"
|
||||
with:
|
||||
repo-token: ${{ secrets.STALE_TOKEN }}
|
||||
close-issue-message: "This issue has been automatically closed because it received no activity for a while. If you think it was closed by accident, please leave a comment. Thank you."
|
||||
ascending: true
|
||||
days-before-issue-close: 7
|
||||
days-before-issue-stale: 15
|
||||
stale-pr-label: "stale?"
|
||||
days-before-pr-close: -1
|
||||
days-before-issue-stale: 365 # ~2 years
|
||||
days-before-pr-stale: -1
|
||||
days-before-pr-close: -1
|
||||
remove-issue-stale-when-updated: true
|
||||
stale-issue-label: "stale?"
|
||||
exempt-issue-labels: "roadmap,next,bug"
|
||||
stale-issue-message: "This issue has been automatically marked as stale due to one year of inactivity. It will be closed in 7 days unless there’s further input. If you believe this issue is still relevant, please leave a comment or provide updated details. Thank you."
|
||||
close-issue-message: "This issue has been automatically closed due to one year of inactivity. If you’re still experiencing a similar problem or have additional details to share, please open a new issue following our current issue template. Your updated report helps us investigate and address concerns more efficiently. Thank you for your understanding!"
|
||||
operations-per-run: 300 # 1 operation per 100 issues, the rest is to label/comment/close
|
||||
- uses: actions/stale@v9
|
||||
id: pr-state
|
||||
name: "Mark stale PRs, close stale PRs"
|
||||
with:
|
||||
repo-token: ${{ secrets.STALE_TOKEN }}
|
||||
ascending: true
|
||||
days-before-issue-close: -1
|
||||
days-before-issue-stale: -1
|
||||
days-before-pr-close: 7
|
||||
days-before-pr-stale: 365 # PRs with no activity in over 90 days will be marked as stale
|
||||
remove-pr-stale-when-updated: true
|
||||
exempt-pr-labels: "roadmap,nex,awaiting-approval,work-in-progress"
|
||||
stale-pr-label: "stale?"
|
||||
stale-pr-message: "This PR has been automatically marked as stale due to one year of inactivity. It will be closed in 7 days unless there’s further input. If you believe this PR is still relevant, please leave a comment or provide updated details. Thank you."
|
||||
close-pr-message: "This PR has been automatically closed due to one year of inactivity. Thank you for your understanding!"
|
||||
operations-per-run: 300 # 1 operation per 100 issues, the rest is to label/comment/close
|
||||
|
||||
2
.github/workflows/prerelease-comment.yml
vendored
2
.github/workflows/prerelease-comment.yml
vendored
@@ -49,7 +49,7 @@ jobs:
|
||||
A new prerelease is available for testing:
|
||||
|
||||
```sh
|
||||
npx shadcn@${{ env.BETA_PACKAGE_VERSION }}
|
||||
pnpm dlx shadcn@${{ env.BETA_PACKAGE_VERSION }}
|
||||
```
|
||||
|
||||
- name: "Remove the autorelease label once published"
|
||||
|
||||
2
.github/workflows/prerelease.yml
vendored
2
.github/workflows/prerelease.yml
vendored
@@ -54,7 +54,7 @@ jobs:
|
||||
path: packages/shadcn
|
||||
|
||||
- name: Upload packaged artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: npm-package-shadcn@${{ steps.package-version.outputs.current-version }}-pr-${{ github.event.number }} # encode the PR number into the artifact name
|
||||
path: packages/shadcn/dist/index.js
|
||||
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -33,4 +33,9 @@ yarn-error.log*
|
||||
.turbo
|
||||
|
||||
.contentlayer
|
||||
tsconfig.tsbuildinfo
|
||||
tsconfig.tsbuildinfo
|
||||
|
||||
# ide
|
||||
.idea
|
||||
.fleet
|
||||
.vscode
|
||||
|
||||
6
.vscode/settings.json
vendored
6
.vscode/settings.json
vendored
@@ -4,8 +4,10 @@
|
||||
{ "pattern": "packages/*/" }
|
||||
],
|
||||
"tailwindCSS.experimental.classRegex": [
|
||||
["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"],
|
||||
["cn\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"]
|
||||
["cva\\(((?:[^()]|\\([^()]*\\))*)\\)", "[\"'`]?([^\"'`]+)[\"'`]?"],
|
||||
["cn\\(((?:[^()]|\\([^()]*\\))*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"]
|
||||
// "cva\\(([^)]*)\\)",
|
||||
// "[\"'`]([^\"'`]*).*?[\"'`]"
|
||||
],
|
||||
"vitest.debugExclude": [
|
||||
"<node_internals>/**",
|
||||
|
||||
@@ -91,6 +91,42 @@ pnpm --filter=www dev
|
||||
pnpm --filter=shadcn-ui dev
|
||||
```
|
||||
|
||||
## Running the CLI Locally
|
||||
|
||||
To run the CLI locally, you can follow the workflow:
|
||||
|
||||
1. Start by running the registry (main site) to make sure the components are up to date:
|
||||
|
||||
```bash
|
||||
pnpm www:dev
|
||||
```
|
||||
|
||||
2. Run the development script for the CLI:
|
||||
|
||||
```bash
|
||||
pnpm shadcn:dev
|
||||
```
|
||||
|
||||
3. In another terminal tab, test the CLI by running:
|
||||
|
||||
```bash
|
||||
pnpm shadcn
|
||||
```
|
||||
|
||||
To test the CLI in a specific app, use a command like:
|
||||
|
||||
```bash
|
||||
pnpm shadcn <init | add | ...> -c ~/Desktop/my-app
|
||||
```
|
||||
|
||||
4. To run the tests for the CLI:
|
||||
|
||||
```bash
|
||||
pnpm --filter=shadcn test
|
||||
```
|
||||
|
||||
This workflow ensures that you are running the most recent version of the registry and testing the CLI properly in your local environment.
|
||||
|
||||
## Documentation
|
||||
|
||||
The documentation for this project is located in the `www` workspace. You can run the documentation locally by running the following command:
|
||||
|
||||
9
SECURITY.md
Normal file
9
SECURITY.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# Security Policy
|
||||
|
||||
If you believe you have found a security vulnerability, we encourage you to let us know right away.
|
||||
|
||||
We will investigate all legitimate reports and do our best to quickly fix the problem.
|
||||
|
||||
Our preference is that you make use of GitHub's private vulnerability reporting feature to disclose potential security vulnerabilities in our Open Source Software.
|
||||
|
||||
To do this, please visit the security tab of the repository and click the "Report a vulnerability" button.
|
||||
@@ -3,7 +3,12 @@
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/versions
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
@@ -23,9 +28,10 @@
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
# env files (can opt-in for committing if needed)
|
||||
.env*
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
6
apps/v4/.prettierignore
Normal file
6
apps/v4/.prettierignore
Normal file
@@ -0,0 +1,6 @@
|
||||
dist
|
||||
node_modules
|
||||
.next
|
||||
build
|
||||
.contentlayer
|
||||
registry/__index__.tsx
|
||||
1
apps/v4/README.md
Normal file
1
apps/v4/README.md
Normal file
@@ -0,0 +1 @@
|
||||
This is a wip registry for the `shadcn` canary version. It has React 19 and Tailwind v4 components.
|
||||
1
apps/v4/__registry__/.autogenerated
Normal file
1
apps/v4/__registry__/.autogenerated
Normal file
@@ -0,0 +1 @@
|
||||
// The content of this directory is autogenerated by the registry server.
|
||||
0
apps/v4/__registry__/.gitkeep
Normal file
0
apps/v4/__registry__/.gitkeep
Normal file
1
apps/v4/__registry__/README.md
Normal file
1
apps/v4/__registry__/README.md
Normal file
@@ -0,0 +1 @@
|
||||
> Files inside this directory is autogenerated by `./scripts/build-registry.ts`. **Do not edit them manually.** - shadcn
|
||||
3924
apps/v4/__registry__/index.tsx
Normal file
3924
apps/v4/__registry__/index.tsx
Normal file
File diff suppressed because it is too large
Load Diff
76
apps/v4/app/(app)/charts/charts.tsx
Normal file
76
apps/v4/app/(app)/charts/charts.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
export { ChartAreaDefault } from "@/registry/new-york-v4/charts/chart-area-default"
|
||||
export { ChartAreaLinear } from "@/registry/new-york-v4/charts/chart-area-linear"
|
||||
export { ChartAreaStep } from "@/registry/new-york-v4/charts/chart-area-step"
|
||||
export { ChartAreaLegend } from "@/registry/new-york-v4/charts/chart-area-legend"
|
||||
export { ChartAreaStacked } from "@/registry/new-york-v4/charts/chart-area-stacked"
|
||||
export { ChartAreaStackedExpand } from "@/registry/new-york-v4/charts/chart-area-stacked-expand"
|
||||
export { ChartAreaIcons } from "@/registry/new-york-v4/charts/chart-area-icons"
|
||||
export { ChartAreaGradient } from "@/registry/new-york-v4/charts/chart-area-gradient"
|
||||
export { ChartAreaAxes } from "@/registry/new-york-v4/charts/chart-area-axes"
|
||||
export { ChartAreaInteractive } from "@/registry/new-york-v4/charts/chart-area-interactive"
|
||||
|
||||
export { ChartBarDefault } from "@/registry/new-york-v4/charts/chart-bar-default"
|
||||
export { ChartBarHorizontal } from "@/registry/new-york-v4/charts/chart-bar-horizontal"
|
||||
export { ChartBarMultiple } from "@/registry/new-york-v4/charts/chart-bar-multiple"
|
||||
export { ChartBarStacked } from "@/registry/new-york-v4/charts/chart-bar-stacked"
|
||||
export { ChartBarLabel } from "@/registry/new-york-v4/charts/chart-bar-label"
|
||||
export { ChartBarLabelCustom } from "@/registry/new-york-v4/charts/chart-bar-label-custom"
|
||||
export { ChartBarMixed } from "@/registry/new-york-v4/charts/chart-bar-mixed"
|
||||
export { ChartBarActive } from "@/registry/new-york-v4/charts/chart-bar-active"
|
||||
export { ChartBarNegative } from "@/registry/new-york-v4/charts/chart-bar-negative"
|
||||
export { ChartBarInteractive } from "@/registry/new-york-v4/charts/chart-bar-interactive"
|
||||
|
||||
export { ChartLineDefault } from "@/registry/new-york-v4/charts/chart-line-default"
|
||||
export { ChartLineLinear } from "@/registry/new-york-v4/charts/chart-line-linear"
|
||||
export { ChartLineStep } from "@/registry/new-york-v4/charts/chart-line-step"
|
||||
export { ChartLineMultiple } from "@/registry/new-york-v4/charts/chart-line-multiple"
|
||||
export { ChartLineDots } from "@/registry/new-york-v4/charts/chart-line-dots"
|
||||
export { ChartLineDotsCustom } from "@/registry/new-york-v4/charts/chart-line-dots-custom"
|
||||
export { ChartLineDotsColors } from "@/registry/new-york-v4/charts/chart-line-dots-colors"
|
||||
export { ChartLineLabel } from "@/registry/new-york-v4/charts/chart-line-label"
|
||||
export { ChartLineLabelCustom } from "@/registry/new-york-v4/charts/chart-line-label-custom"
|
||||
export { ChartLineInteractive } from "@/registry/new-york-v4/charts/chart-line-interactive"
|
||||
|
||||
export { ChartPieSimple } from "@/registry/new-york-v4/charts/chart-pie-simple"
|
||||
export { ChartPieSeparatorNone } from "@/registry/new-york-v4/charts/chart-pie-separator-none"
|
||||
export { ChartPieLabel } from "@/registry/new-york-v4/charts/chart-pie-label"
|
||||
export { ChartPieLabelCustom } from "@/registry/new-york-v4/charts/chart-pie-label-custom"
|
||||
export { ChartPieLabelList } from "@/registry/new-york-v4/charts/chart-pie-label-list"
|
||||
export { ChartPieLegend } from "@/registry/new-york-v4/charts/chart-pie-legend"
|
||||
export { ChartPieDonut } from "@/registry/new-york-v4/charts/chart-pie-donut"
|
||||
export { ChartPieDonutActive } from "@/registry/new-york-v4/charts/chart-pie-donut-active"
|
||||
export { ChartPieDonutText } from "@/registry/new-york-v4/charts/chart-pie-donut-text"
|
||||
export { ChartPieStacked } from "@/registry/new-york-v4/charts/chart-pie-stacked"
|
||||
export { ChartPieInteractive } from "@/registry/new-york-v4/charts/chart-pie-interactive"
|
||||
|
||||
export { ChartRadarDefault } from "@/registry/new-york-v4/charts/chart-radar-default"
|
||||
export { ChartRadarDots } from "@/registry/new-york-v4/charts/chart-radar-dots"
|
||||
export { ChartRadarLinesOnly } from "@/registry/new-york-v4/charts/chart-radar-lines-only"
|
||||
export { ChartRadarLabelCustom } from "@/registry/new-york-v4/charts/chart-radar-label-custom"
|
||||
export { ChartRadarGridCustom } from "@/registry/new-york-v4/charts/chart-radar-grid-custom"
|
||||
export { ChartRadarGridNone } from "@/registry/new-york-v4/charts/chart-radar-grid-none"
|
||||
export { ChartRadarGridCircle } from "@/registry/new-york-v4/charts/chart-radar-grid-circle"
|
||||
export { ChartRadarGridCircleNoLines } from "@/registry/new-york-v4/charts/chart-radar-grid-circle-no-lines"
|
||||
export { ChartRadarGridCircleFill } from "@/registry/new-york-v4/charts/chart-radar-grid-circle-fill"
|
||||
export { ChartRadarGridFill } from "@/registry/new-york-v4/charts/chart-radar-grid-fill"
|
||||
export { ChartRadarMultiple } from "@/registry/new-york-v4/charts/chart-radar-multiple"
|
||||
export { ChartRadarLegend } from "@/registry/new-york-v4/charts/chart-radar-legend"
|
||||
export { ChartRadarIcons } from "@/registry/new-york-v4/charts/chart-radar-icons"
|
||||
export { ChartRadarRadius } from "@/registry/new-york-v4/charts/chart-radar-radius"
|
||||
|
||||
export { ChartRadialSimple } from "@/registry/new-york-v4/charts/chart-radial-simple"
|
||||
export { ChartRadialLabel } from "@/registry/new-york-v4/charts/chart-radial-label"
|
||||
export { ChartRadialGrid } from "@/registry/new-york-v4/charts/chart-radial-grid"
|
||||
export { ChartRadialText } from "@/registry/new-york-v4/charts/chart-radial-text"
|
||||
export { ChartRadialShape } from "@/registry/new-york-v4/charts/chart-radial-shape"
|
||||
export { ChartRadialStacked } from "@/registry/new-york-v4/charts/chart-radial-stacked"
|
||||
|
||||
export { ChartTooltipDefault } from "@/registry/new-york-v4/charts/chart-tooltip-default"
|
||||
export { ChartTooltipIndicatorLine } from "@/registry/new-york-v4/charts/chart-tooltip-indicator-line"
|
||||
export { ChartTooltipIndicatorNone } from "@/registry/new-york-v4/charts/chart-tooltip-indicator-none"
|
||||
export { ChartTooltipLabelCustom } from "@/registry/new-york-v4/charts/chart-tooltip-label-custom"
|
||||
export { ChartTooltipLabelFormatter } from "@/registry/new-york-v4/charts/chart-tooltip-label-formatter"
|
||||
export { ChartTooltipLabelNone } from "@/registry/new-york-v4/charts/chart-tooltip-label-none"
|
||||
export { ChartTooltipFormatter } from "@/registry/new-york-v4/charts/chart-tooltip-formatter"
|
||||
export { ChartTooltipIcons } from "@/registry/new-york-v4/charts/chart-tooltip-icons"
|
||||
export { ChartTooltipAdvanced } from "@/registry/new-york-v4/charts/chart-tooltip-advanced"
|
||||
20
apps/v4/app/(app)/charts/page.tsx
Normal file
20
apps/v4/app/(app)/charts/page.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { ComponentWrapper } from "@/components/component-wrapper"
|
||||
import * as Charts from "@/app/(app)/charts/charts"
|
||||
|
||||
export default function ChartsPage() {
|
||||
return (
|
||||
<div className="grid flex-1 grid-cols-3 items-start gap-4 p-4 2xl:grid-cols-4">
|
||||
{Object.entries(Charts)
|
||||
.sort()
|
||||
.map(([key, Component]) => (
|
||||
<ComponentWrapper
|
||||
key={key}
|
||||
name={key}
|
||||
className="w-auto data-[name=chartareainteractive]:col-span-3 data-[name=chartbarinteractive]:col-span-3 data-[name=chartlineinteractive]:col-span-3 **:data-[slot=card]:w-full"
|
||||
>
|
||||
<Component />
|
||||
</ComponentWrapper>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
14
apps/v4/app/(app)/forms/page.tsx
Normal file
14
apps/v4/app/(app)/forms/page.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import { FormsDemo } from "@/components/forms-demo"
|
||||
|
||||
export default function FormsPage() {
|
||||
return (
|
||||
<div className="flex flex-1 flex-col items-center justify-center gap-12 p-4 lg:flex-row">
|
||||
<div className="">
|
||||
<FormsDemo />
|
||||
</div>
|
||||
<div className="theme-scaled">
|
||||
<FormsDemo />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
44
apps/v4/app/(app)/layout.tsx
Normal file
44
apps/v4/app/(app)/layout.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import { cookies } from "next/headers"
|
||||
|
||||
import { AppSidebar } from "@/components/app-sidebar"
|
||||
import { ModeSwitcher } from "@/components/mode-switcher"
|
||||
import { NavHeader } from "@/components/nav-header"
|
||||
import { ThemeSelector } from "@/components/theme-selector"
|
||||
import { Separator } from "@/registry/new-york-v4/ui/separator"
|
||||
import {
|
||||
SidebarInset,
|
||||
SidebarProvider,
|
||||
SidebarTrigger,
|
||||
} from "@/registry/new-york-v4/ui/sidebar"
|
||||
|
||||
export default async function AppLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode
|
||||
}>) {
|
||||
const cookieStore = await cookies()
|
||||
const defaultOpen = cookieStore.get("sidebar_state")?.value === "true"
|
||||
|
||||
return (
|
||||
<SidebarProvider defaultOpen={defaultOpen}>
|
||||
<AppSidebar />
|
||||
<SidebarInset>
|
||||
<header className="bg-background sticky inset-x-0 top-0 isolate z-10 flex shrink-0 items-center gap-2 border-b">
|
||||
<div className="flex h-14 w-full items-center gap-2 px-4">
|
||||
<SidebarTrigger className="-ml-1.5" />
|
||||
<Separator
|
||||
orientation="vertical"
|
||||
className="mr-2 data-[orientation=vertical]:h-4"
|
||||
/>
|
||||
<NavHeader />
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
<ThemeSelector />
|
||||
<ModeSwitcher />
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
{children}
|
||||
</SidebarInset>
|
||||
</SidebarProvider>
|
||||
)
|
||||
}
|
||||
40
apps/v4/app/(app)/login/page.tsx
Normal file
40
apps/v4/app/(app)/login/page.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import {
|
||||
Manrope as FontManrope,
|
||||
Lexend as FontSans,
|
||||
Newsreader as FontSerif,
|
||||
} from "next/font/google"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { LoginForm } from "@/components/login-form"
|
||||
|
||||
const fontSans = FontSans({ subsets: ["latin"], variable: "--font-sans" })
|
||||
const fontSerif = FontSerif({ subsets: ["latin"], variable: "--font-serif" })
|
||||
const fontManrope = FontManrope({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-manrope",
|
||||
})
|
||||
export default function LoginPage() {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"bg-muted dark:bg-background flex flex-1 flex-col items-center justify-center gap-16 p-6 md:p-10",
|
||||
fontSans.variable,
|
||||
fontSerif.variable,
|
||||
fontManrope.variable
|
||||
)}
|
||||
>
|
||||
<div className="w-full max-w-sm md:max-w-3xl">
|
||||
<LoginForm />
|
||||
</div>
|
||||
<div className="theme-login-one w-full max-w-sm md:max-w-3xl">
|
||||
<LoginForm imageUrl="https://images.unsplash.com/photo-1482872376051-5ce74ebf0908?q=80&w=3050&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" />
|
||||
</div>
|
||||
<div className="theme-login-two w-full max-w-sm md:max-w-3xl">
|
||||
<LoginForm imageUrl="https://images.unsplash.com/photo-1498758536662-35b82cd15e29?q=80&w=3088&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" />
|
||||
</div>
|
||||
<div className="theme-login-three w-full max-w-sm md:max-w-3xl">
|
||||
<LoginForm imageUrl="https://images.unsplash.com/photo-1536147116438-62679a5e01f2?q=80&w=2688&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
196
apps/v4/app/(app)/page.tsx
Normal file
196
apps/v4/app/(app)/page.tsx
Normal file
@@ -0,0 +1,196 @@
|
||||
import { AccordionDemo } from "@/components/accordion-demo"
|
||||
import { AlertDemo } from "@/components/alert-demo"
|
||||
import { AlertDialogDemo } from "@/components/alert-dialog-demo"
|
||||
import { AspectRatioDemo } from "@/components/aspect-ratio-demo"
|
||||
import { AvatarDemo } from "@/components/avatar-demo"
|
||||
import { BadgeDemo } from "@/components/badge-demo"
|
||||
import { BreadcrumbDemo } from "@/components/breadcrumb-demo"
|
||||
import { ButtonDemo } from "@/components/button-demo"
|
||||
import { CalendarDemo } from "@/components/calendar-demo"
|
||||
import { CardDemo } from "@/components/card-demo"
|
||||
import { CarouselDemo } from "@/components/carousel-demo"
|
||||
import { ChartDemo } from "@/components/chart-demo"
|
||||
import { CheckboxDemo } from "@/components/checkbox-demo"
|
||||
import { CollapsibleDemo } from "@/components/collapsible-demo"
|
||||
import { ComboboxDemo } from "@/components/combobox-demo"
|
||||
import { CommandDemo } from "@/components/command-demo"
|
||||
import { ComponentWrapper } from "@/components/component-wrapper"
|
||||
import { ContextMenuDemo } from "@/components/context-menu-demo"
|
||||
import { DatePickerDemo } from "@/components/date-picker-demo"
|
||||
import { DialogDemo } from "@/components/dialog-demo"
|
||||
import { DrawerDemo } from "@/components/drawer-demo"
|
||||
import { DropdownMenuDemo } from "@/components/dropdown-menu-demo"
|
||||
import { FormDemo } from "@/components/form-demo"
|
||||
import { HoverCardDemo } from "@/components/hover-card-demo"
|
||||
import { InputDemo } from "@/components/input-demo"
|
||||
import { InputOTPDemo } from "@/components/input-otp-demo"
|
||||
import { LabelDemo } from "@/components/label-demo"
|
||||
import { MenubarDemo } from "@/components/menubar-demo"
|
||||
import { NavigationMenuDemo } from "@/components/navigation-menu-demo"
|
||||
import { PaginationDemo } from "@/components/pagination-demo"
|
||||
import { PopoverDemo } from "@/components/popover-demo"
|
||||
import { ProgressDemo } from "@/components/progress-demo"
|
||||
import { RadioGroupDemo } from "@/components/radio-group-demo"
|
||||
import { ResizableDemo } from "@/components/resizable-demo"
|
||||
import { ScrollAreaDemo } from "@/components/scroll-area-demo"
|
||||
import { SelectDemo } from "@/components/select-demo"
|
||||
import { SeparatorDemo } from "@/components/separator-demo"
|
||||
import { SheetDemo } from "@/components/sheet-demo"
|
||||
import { SkeletonDemo } from "@/components/skeleton-demo"
|
||||
import { SliderDemo } from "@/components/slider-demo"
|
||||
import { SonnerDemo } from "@/components/sonner-demo"
|
||||
import { SwitchDemo } from "@/components/switch-demo"
|
||||
import { TableDemo } from "@/components/table-demo"
|
||||
import { TabsDemo } from "@/components/tabs-demo"
|
||||
import { TextareaDemo } from "@/components/textarea-demo"
|
||||
import { ToggleDemo } from "@/components/toggle-demo"
|
||||
import { ToggleGroupDemo } from "@/components/toggle-group-demo"
|
||||
import { TooltipDemo } from "@/components/tooltip-demo"
|
||||
|
||||
export default function SinkPage() {
|
||||
return (
|
||||
<div className="@container grid flex-1 gap-4 p-4">
|
||||
<ComponentWrapper name="chart" className="w-full">
|
||||
<ChartDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="accordion">
|
||||
<AccordionDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="alert">
|
||||
<AlertDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="alert-dialog">
|
||||
<AlertDialogDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="aspect-ratio">
|
||||
<AspectRatioDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="avatar">
|
||||
<AvatarDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="badge">
|
||||
<BadgeDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="breadcrumb">
|
||||
<BreadcrumbDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="button">
|
||||
<ButtonDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="calendar">
|
||||
<CalendarDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="card">
|
||||
<CardDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="carousel">
|
||||
<CarouselDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="checkbox">
|
||||
<CheckboxDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="collapsible">
|
||||
<CollapsibleDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="combobox">
|
||||
<ComboboxDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="command">
|
||||
<CommandDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="context-menu">
|
||||
<ContextMenuDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="date-picker">
|
||||
<DatePickerDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="dialog">
|
||||
<DialogDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="drawer">
|
||||
<DrawerDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="dropdown-menu">
|
||||
<DropdownMenuDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="form">
|
||||
<FormDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="hover-card">
|
||||
<HoverCardDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="input">
|
||||
<InputDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="input-otp">
|
||||
<InputOTPDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="label">
|
||||
<LabelDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="menubar">
|
||||
<MenubarDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="navigation-menu">
|
||||
<NavigationMenuDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="pagination">
|
||||
<PaginationDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="popover">
|
||||
<PopoverDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="progress">
|
||||
<ProgressDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="radio-group">
|
||||
<RadioGroupDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="resizable">
|
||||
<ResizableDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="scroll-area">
|
||||
<ScrollAreaDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="select">
|
||||
<SelectDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="separator">
|
||||
<SeparatorDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="sheet">
|
||||
<SheetDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="skeleton">
|
||||
<SkeletonDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="slider">
|
||||
<SliderDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="sonner">
|
||||
<SonnerDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="switch">
|
||||
<SwitchDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="table">
|
||||
<TableDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="tabs">
|
||||
<TabsDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="textarea">
|
||||
<TextareaDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="toggle">
|
||||
<ToggleDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="toggle-group">
|
||||
<ToggleGroupDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="tooltip">
|
||||
<TooltipDemo />
|
||||
</ComponentWrapper>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { addDays, format } from "date-fns"
|
||||
import { CalendarIcon } from "lucide-react"
|
||||
import { DateRange } from "react-day-picker"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import { Calendar } from "@/registry/new-york-v4/ui/calendar"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/registry/new-york-v4/ui/popover"
|
||||
|
||||
export function AnalyticsDatePicker() {
|
||||
const [date, setDate] = React.useState<DateRange | undefined>({
|
||||
from: new Date(new Date().getFullYear(), 0, 20),
|
||||
to: addDays(new Date(new Date().getFullYear(), 0, 20), 20),
|
||||
})
|
||||
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
id="date"
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"w-fit justify-start px-2 font-normal",
|
||||
!date && "text-muted-foreground"
|
||||
)}
|
||||
>
|
||||
<CalendarIcon className="text-muted-foreground" />
|
||||
{date?.from ? (
|
||||
date.to ? (
|
||||
<>
|
||||
{format(date.from, "LLL dd, y")} -{" "}
|
||||
{format(date.to, "LLL dd, y")}
|
||||
</>
|
||||
) : (
|
||||
format(date.from, "LLL dd, y")
|
||||
)
|
||||
) : (
|
||||
<span>Pick a date</span>
|
||||
)}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0" align="end">
|
||||
<Calendar
|
||||
initialFocus
|
||||
mode="range"
|
||||
defaultMonth={date?.from}
|
||||
selected={date}
|
||||
onSelect={setDate}
|
||||
numberOfMonths={2}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import {
|
||||
ChartLineIcon,
|
||||
FileIcon,
|
||||
HomeIcon,
|
||||
LifeBuoy,
|
||||
Send,
|
||||
Settings2Icon,
|
||||
ShoppingBagIcon,
|
||||
ShoppingCartIcon,
|
||||
UserIcon,
|
||||
} from "lucide-react"
|
||||
|
||||
import { Sidebar, SidebarContent } from "@/registry/new-york-v4/ui/sidebar"
|
||||
import { NavMain } from "@/app/(examples)/dashboard-03/components/nav-main"
|
||||
import { NavSecondary } from "@/app/(examples)/dashboard-03/components/nav-secondary"
|
||||
|
||||
const data = {
|
||||
navMain: [
|
||||
{
|
||||
title: "Dashboard",
|
||||
url: "/dashboard",
|
||||
icon: HomeIcon,
|
||||
},
|
||||
{
|
||||
title: "Analytics",
|
||||
url: "/dashboard/analytics",
|
||||
icon: ChartLineIcon,
|
||||
},
|
||||
{
|
||||
title: "Orders",
|
||||
url: "/dashboard/orders",
|
||||
icon: ShoppingBagIcon,
|
||||
},
|
||||
{
|
||||
title: "Products",
|
||||
url: "/dashboard/products",
|
||||
icon: ShoppingCartIcon,
|
||||
},
|
||||
{
|
||||
title: "Invoices",
|
||||
url: "/dashboard/invoices",
|
||||
icon: FileIcon,
|
||||
},
|
||||
{
|
||||
title: "Customers",
|
||||
url: "/dashboard/customers",
|
||||
icon: UserIcon,
|
||||
},
|
||||
{
|
||||
title: "Settings",
|
||||
url: "/dashboard/settings",
|
||||
icon: Settings2Icon,
|
||||
},
|
||||
],
|
||||
navSecondary: [
|
||||
{
|
||||
title: "Support",
|
||||
url: "#",
|
||||
icon: LifeBuoy,
|
||||
},
|
||||
{
|
||||
title: "Feedback",
|
||||
url: "#",
|
||||
icon: Send,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
return (
|
||||
<Sidebar
|
||||
className="top-(--header-height) h-[calc(100svh-var(--header-height))]!"
|
||||
{...props}
|
||||
>
|
||||
<SidebarContent>
|
||||
<NavMain items={data.navMain} />
|
||||
<NavSecondary items={data.navSecondary} className="mt-auto" />
|
||||
</SidebarContent>
|
||||
</Sidebar>
|
||||
)
|
||||
}
|
||||
110
apps/v4/app/(examples)/dashboard-03/components/chart-revenue.tsx
Normal file
110
apps/v4/app/(examples)/dashboard-03/components/chart-revenue.tsx
Normal file
@@ -0,0 +1,110 @@
|
||||
"use client"
|
||||
|
||||
import { TrendingUp } from "lucide-react"
|
||||
import { Bar, BarChart, CartesianGrid, XAxis, YAxis } from "recharts"
|
||||
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/new-york-v4/ui/card"
|
||||
import {
|
||||
ChartConfig,
|
||||
ChartContainer,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
} from "@/registry/new-york-v4/ui/chart"
|
||||
|
||||
const chartData = [
|
||||
{ month: "January", desktop: 186, mobile: 80 },
|
||||
{ month: "February", desktop: 305, mobile: 200 },
|
||||
{ month: "March", desktop: 237, mobile: 120 },
|
||||
{ month: "April", desktop: 73, mobile: 190 },
|
||||
{ month: "May", desktop: 209, mobile: 130 },
|
||||
{ month: "June", desktop: 346, mobile: 140 },
|
||||
{ month: "July", desktop: 321, mobile: 275 },
|
||||
{ month: "August", desktop: 132, mobile: 95 },
|
||||
{ month: "September", desktop: 189, mobile: 225 },
|
||||
{ month: "October", desktop: 302, mobile: 248 },
|
||||
{ month: "November", desktop: 342, mobile: 285 },
|
||||
{ month: "December", desktop: 328, mobile: 290 },
|
||||
]
|
||||
|
||||
const chartConfig = {
|
||||
desktop: {
|
||||
label: "Desktop",
|
||||
color: "var(--chart-1)",
|
||||
},
|
||||
mobile: {
|
||||
label: "Mobile",
|
||||
color: "var(--chart-2)",
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
|
||||
export function ChartRevenue() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardDescription>January - June 2024</CardDescription>
|
||||
<CardTitle className="text-3xl font-bold tracking-tight">
|
||||
$45,231.89
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ChartContainer config={chartConfig} className="aspect-[3/1]">
|
||||
<BarChart
|
||||
accessibilityLayer
|
||||
data={chartData}
|
||||
margin={{
|
||||
left: -16,
|
||||
right: 0,
|
||||
}}
|
||||
>
|
||||
<CartesianGrid vertical={false} />
|
||||
<XAxis
|
||||
dataKey="month"
|
||||
tickLine={false}
|
||||
tickMargin={10}
|
||||
axisLine={false}
|
||||
tickFormatter={(value) => value.slice(0, 3)}
|
||||
/>
|
||||
<YAxis
|
||||
tickLine={false}
|
||||
tickMargin={10}
|
||||
axisLine={false}
|
||||
tickFormatter={(value) => value.toLocaleString()}
|
||||
domain={[0, "dataMax"]}
|
||||
/>
|
||||
<ChartTooltip
|
||||
cursor={false}
|
||||
content={<ChartTooltipContent hideIndicator />}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="desktop"
|
||||
fill="var(--color-desktop)"
|
||||
radius={[0, 0, 4, 4]}
|
||||
stackId={1}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="mobile"
|
||||
fill="var(--color-mobile)"
|
||||
radius={[4, 4, 0, 0]}
|
||||
stackId={1}
|
||||
/>
|
||||
</BarChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
<CardFooter className="flex-col items-start gap-2 text-sm">
|
||||
<div className="flex gap-2 leading-none font-medium">
|
||||
Trending up by 5.2% this month <TrendingUp className="h-4 w-4" />
|
||||
</div>
|
||||
<div className="text-muted-foreground leading-none">
|
||||
Showing total visitors for the last 6 months
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Label, Pie, PieChart, Sector } from "recharts"
|
||||
import { PieSectorDataItem } from "recharts/types/polar/Pie"
|
||||
|
||||
import {
|
||||
Card,
|
||||
CardAction,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/new-york-v4/ui/card"
|
||||
import {
|
||||
ChartConfig,
|
||||
ChartContainer,
|
||||
ChartStyle,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
} from "@/registry/new-york-v4/ui/chart"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/registry/new-york-v4/ui/select"
|
||||
|
||||
const desktopData = [
|
||||
{ month: "january", desktop: 186, fill: "var(--color-january)" },
|
||||
{ month: "february", desktop: 305, fill: "var(--color-february)" },
|
||||
{ month: "march", desktop: 237, fill: "var(--color-march)" },
|
||||
{ month: "april", desktop: 173, fill: "var(--color-april)" },
|
||||
{ month: "may", desktop: 209, fill: "var(--color-may)" },
|
||||
]
|
||||
|
||||
const chartConfig = {
|
||||
visitors: {
|
||||
label: "Visitors",
|
||||
},
|
||||
desktop: {
|
||||
label: "Desktop",
|
||||
},
|
||||
mobile: {
|
||||
label: "Mobile",
|
||||
},
|
||||
january: {
|
||||
label: "January",
|
||||
color: "var(--chart-1)",
|
||||
},
|
||||
february: {
|
||||
label: "February",
|
||||
color: "var(--chart-2)",
|
||||
},
|
||||
march: {
|
||||
label: "March",
|
||||
color: "var(--chart-3)",
|
||||
},
|
||||
april: {
|
||||
label: "April",
|
||||
color: "var(--chart-4)",
|
||||
},
|
||||
may: {
|
||||
label: "May",
|
||||
color: "var(--chart-5)",
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
|
||||
export function ChartVisitors() {
|
||||
const id = "pie-interactive"
|
||||
const [activeMonth, setActiveMonth] = React.useState(desktopData[0].month)
|
||||
|
||||
const activeIndex = React.useMemo(
|
||||
() => desktopData.findIndex((item) => item.month === activeMonth),
|
||||
[activeMonth]
|
||||
)
|
||||
const months = React.useMemo(() => desktopData.map((item) => item.month), [])
|
||||
|
||||
return (
|
||||
<Card data-chart={id}>
|
||||
<ChartStyle id={id} config={chartConfig} />
|
||||
<CardHeader>
|
||||
<CardDescription>January - June 2024</CardDescription>
|
||||
<CardTitle className="text-2xl font-bold">1,234 visitors</CardTitle>
|
||||
<CardAction>
|
||||
<Select value={activeMonth} onValueChange={setActiveMonth}>
|
||||
<SelectTrigger
|
||||
className="ml-auto h-8 w-[120px]"
|
||||
aria-label="Select a value"
|
||||
>
|
||||
<SelectValue placeholder="Select month" />
|
||||
</SelectTrigger>
|
||||
<SelectContent align="end">
|
||||
{months.map((key) => {
|
||||
const config = chartConfig[key as keyof typeof chartConfig]
|
||||
|
||||
if (!config) {
|
||||
return null
|
||||
}
|
||||
|
||||
const color = "color" in config ? config.color : undefined
|
||||
|
||||
return (
|
||||
<SelectItem key={key} value={key}>
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<span
|
||||
className="flex h-3 w-3 shrink-0 rounded-sm"
|
||||
style={{
|
||||
backgroundColor: color,
|
||||
}}
|
||||
/>
|
||||
{config?.label}
|
||||
</div>
|
||||
</SelectItem>
|
||||
)
|
||||
})}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-1 justify-center pb-0">
|
||||
<ChartContainer
|
||||
id={id}
|
||||
config={chartConfig}
|
||||
className="mx-auto aspect-square w-full max-w-[300px]"
|
||||
>
|
||||
<PieChart>
|
||||
<ChartTooltip
|
||||
cursor={false}
|
||||
content={<ChartTooltipContent hideLabel />}
|
||||
/>
|
||||
<Pie
|
||||
data={desktopData}
|
||||
dataKey="desktop"
|
||||
nameKey="month"
|
||||
innerRadius={60}
|
||||
strokeWidth={5}
|
||||
activeIndex={activeIndex}
|
||||
activeShape={({
|
||||
outerRadius = 0,
|
||||
...props
|
||||
}: PieSectorDataItem) => (
|
||||
<g>
|
||||
<Sector {...props} outerRadius={outerRadius + 10} />
|
||||
<Sector
|
||||
{...props}
|
||||
outerRadius={outerRadius + 25}
|
||||
innerRadius={outerRadius + 12}
|
||||
/>
|
||||
</g>
|
||||
)}
|
||||
>
|
||||
<Label
|
||||
content={({ viewBox }) => {
|
||||
if (viewBox && "cx" in viewBox && "cy" in viewBox) {
|
||||
return (
|
||||
<text
|
||||
x={viewBox.cx}
|
||||
y={viewBox.cy}
|
||||
textAnchor="middle"
|
||||
dominantBaseline="middle"
|
||||
>
|
||||
<tspan
|
||||
x={viewBox.cx}
|
||||
y={viewBox.cy}
|
||||
className="fill-foreground text-3xl font-bold"
|
||||
>
|
||||
{desktopData[activeIndex].desktop.toLocaleString()}
|
||||
</tspan>
|
||||
<tspan
|
||||
x={viewBox.cx}
|
||||
y={(viewBox.cy || 0) + 24}
|
||||
className="fill-muted-foreground"
|
||||
>
|
||||
Visitors
|
||||
</tspan>
|
||||
</text>
|
||||
)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Pie>
|
||||
</PieChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { MoonIcon, SunIcon } from "lucide-react"
|
||||
import { useTheme } from "next-themes"
|
||||
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
|
||||
export function ModeToggle() {
|
||||
const { setTheme, resolvedTheme } = useTheme()
|
||||
|
||||
const toggleTheme = React.useCallback(() => {
|
||||
setTheme(resolvedTheme === "dark" ? "light" : "dark")
|
||||
}, [resolvedTheme, setTheme])
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="icon"
|
||||
className="group/toggle size-8"
|
||||
onClick={toggleTheme}
|
||||
>
|
||||
<SunIcon className="hidden [html.dark_&]:block" />
|
||||
<MoonIcon className="hidden [html.light_&]:block" />
|
||||
<span className="sr-only">Toggle theme</span>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
91
apps/v4/app/(examples)/dashboard-03/components/nav-main.tsx
Normal file
91
apps/v4/app/(examples)/dashboard-03/components/nav-main.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
"use client"
|
||||
|
||||
import { usePathname } from "next/navigation"
|
||||
import { ChevronRight, type LucideIcon } from "lucide-react"
|
||||
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from "@/registry/new-york-v4/ui/collapsible"
|
||||
import {
|
||||
SidebarGroup,
|
||||
SidebarGroupLabel,
|
||||
SidebarMenu,
|
||||
SidebarMenuAction,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
SidebarMenuSub,
|
||||
SidebarMenuSubButton,
|
||||
SidebarMenuSubItem,
|
||||
} from "@/registry/new-york-v4/ui/sidebar"
|
||||
|
||||
export function NavMain({
|
||||
items,
|
||||
}: {
|
||||
items: {
|
||||
title: string
|
||||
url: string
|
||||
icon: LucideIcon
|
||||
isActive?: boolean
|
||||
items?: {
|
||||
title: string
|
||||
url: string
|
||||
}[]
|
||||
disabled?: boolean
|
||||
}[]
|
||||
}) {
|
||||
const pathname = usePathname()
|
||||
|
||||
return (
|
||||
<SidebarGroup>
|
||||
<SidebarGroupLabel>Dashboard</SidebarGroupLabel>
|
||||
<SidebarMenu>
|
||||
{items.map((item) => (
|
||||
<Collapsible key={item.title} asChild defaultOpen={item.isActive}>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton
|
||||
asChild
|
||||
tooltip={item.title}
|
||||
isActive={pathname === item.url}
|
||||
disabled={item.disabled}
|
||||
>
|
||||
<a
|
||||
href={item.disabled ? "#" : item.url}
|
||||
data-disabled={item.disabled}
|
||||
className="data-[disabled=true]:opacity-50"
|
||||
>
|
||||
<item.icon className="text-muted-foreground" />
|
||||
<span>{item.title}</span>
|
||||
</a>
|
||||
</SidebarMenuButton>
|
||||
{item.items?.length ? (
|
||||
<>
|
||||
<CollapsibleTrigger asChild>
|
||||
<SidebarMenuAction className="data-[state=open]:rotate-90">
|
||||
<ChevronRight />
|
||||
<span className="sr-only">Toggle</span>
|
||||
</SidebarMenuAction>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<SidebarMenuSub>
|
||||
{item.items?.map((subItem) => (
|
||||
<SidebarMenuSubItem key={subItem.title}>
|
||||
<SidebarMenuSubButton asChild>
|
||||
<a href={subItem.url}>
|
||||
<span>{subItem.title}</span>
|
||||
</a>
|
||||
</SidebarMenuSubButton>
|
||||
</SidebarMenuSubItem>
|
||||
))}
|
||||
</SidebarMenuSub>
|
||||
</CollapsibleContent>
|
||||
</>
|
||||
) : null}
|
||||
</SidebarMenuItem>
|
||||
</Collapsible>
|
||||
))}
|
||||
</SidebarMenu>
|
||||
</SidebarGroup>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import * as React from "react"
|
||||
import { type LucideIcon } from "lucide-react"
|
||||
|
||||
import {
|
||||
SidebarGroup,
|
||||
SidebarGroupContent,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
} from "@/registry/new-york-v4/ui/sidebar"
|
||||
|
||||
export function NavSecondary({
|
||||
items,
|
||||
...props
|
||||
}: {
|
||||
items: {
|
||||
title: string
|
||||
url: string
|
||||
icon: LucideIcon
|
||||
}[]
|
||||
} & React.ComponentPropsWithoutRef<typeof SidebarGroup>) {
|
||||
return (
|
||||
<SidebarGroup {...props}>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
{items.map((item) => (
|
||||
<SidebarMenuItem key={item.title}>
|
||||
<SidebarMenuButton asChild size="sm">
|
||||
<a href={item.url}>
|
||||
<item.icon />
|
||||
<span>{item.title}</span>
|
||||
</a>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
))}
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
)
|
||||
}
|
||||
90
apps/v4/app/(examples)/dashboard-03/components/nav-user.tsx
Normal file
90
apps/v4/app/(examples)/dashboard-03/components/nav-user.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
"use client"
|
||||
|
||||
import { BadgeCheck, Bell, CreditCard, LogOut, Sparkles } from "lucide-react"
|
||||
|
||||
import {
|
||||
Avatar,
|
||||
AvatarFallback,
|
||||
AvatarImage,
|
||||
} from "@/registry/new-york-v4/ui/avatar"
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/registry/new-york-v4/ui/dropdown-menu"
|
||||
|
||||
export function NavUser({
|
||||
user,
|
||||
}: {
|
||||
user: {
|
||||
name: string
|
||||
email: string
|
||||
avatar: string
|
||||
}
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon">
|
||||
<Avatar className="size-8 rounded-md">
|
||||
<AvatarImage src={user.avatar} alt={user.name} />
|
||||
<AvatarFallback className="rounded-lg">CN</AvatarFallback>
|
||||
</Avatar>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
|
||||
side="bottom"
|
||||
align="end"
|
||||
sideOffset={4}
|
||||
>
|
||||
<DropdownMenuLabel className="p-0 font-normal">
|
||||
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
|
||||
<Avatar className="h-8 w-8 rounded-lg">
|
||||
<AvatarImage src={user.avatar} alt={user.name} />
|
||||
<AvatarFallback className="rounded-lg">CN</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||
<span className="truncate font-medium">{user.name}</span>
|
||||
<span className="text-muted-foreground truncate text-xs">
|
||||
{user.email}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>
|
||||
<Sparkles />
|
||||
Upgrade to Pro
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>
|
||||
<BadgeCheck />
|
||||
Account
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<CreditCard />
|
||||
Billing
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<Bell />
|
||||
Notifications
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>
|
||||
<LogOut />
|
||||
Log out
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
import {
|
||||
ArrowUpDownIcon,
|
||||
EllipsisVerticalIcon,
|
||||
ListFilterIcon,
|
||||
PlusIcon,
|
||||
} from "lucide-react"
|
||||
|
||||
import { Badge } from "@/registry/new-york-v4/ui/badge"
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
} from "@/registry/new-york-v4/ui/card"
|
||||
import { Checkbox } from "@/registry/new-york-v4/ui/checkbox"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/registry/new-york-v4/ui/dropdown-menu"
|
||||
import {
|
||||
Pagination,
|
||||
PaginationContent,
|
||||
PaginationEllipsis,
|
||||
PaginationItem,
|
||||
PaginationLink,
|
||||
PaginationNext,
|
||||
PaginationPrevious,
|
||||
} from "@/registry/new-york-v4/ui/pagination"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/registry/new-york-v4/ui/select"
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/registry/new-york-v4/ui/table"
|
||||
import { Tabs, TabsList, TabsTrigger } from "@/registry/new-york-v4/ui/tabs"
|
||||
|
||||
export function ProductsTable({
|
||||
products,
|
||||
}: {
|
||||
products: {
|
||||
id: string
|
||||
name: string
|
||||
price: number
|
||||
stock: number
|
||||
dateAdded: string
|
||||
status: string
|
||||
}[]
|
||||
}) {
|
||||
return (
|
||||
<Card className="flex w-full flex-col gap-4">
|
||||
<CardHeader className="flex flex-row items-center justify-between">
|
||||
<Tabs defaultValue="all">
|
||||
<TabsList className="w-full @3xl/page:w-fit">
|
||||
<TabsTrigger value="all">All Products</TabsTrigger>
|
||||
<TabsTrigger value="in-stock">In Stock</TabsTrigger>
|
||||
<TabsTrigger value="low-stock">Low Stock</TabsTrigger>
|
||||
<TabsTrigger value="add-product" asChild>
|
||||
<button>
|
||||
<PlusIcon />
|
||||
</button>
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
<div className="hidden items-center gap-2 **:data-[slot=button]:size-8 **:data-[slot=select-trigger]:h-8 @3xl/page:flex">
|
||||
<Select defaultValue="all">
|
||||
<SelectTrigger>
|
||||
<span className="text-muted-foreground text-sm">Category:</span>
|
||||
<SelectValue placeholder="Select a product" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">All</SelectItem>
|
||||
<SelectItem value="in-stock">In Stock</SelectItem>
|
||||
<SelectItem value="low-stock">Low Stock</SelectItem>
|
||||
<SelectItem value="archived">Archived</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select defaultValue="all">
|
||||
<SelectTrigger>
|
||||
<span className="text-muted-foreground text-sm">Price:</span>
|
||||
<SelectValue placeholder="Select a product" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">$100-$200</SelectItem>
|
||||
<SelectItem value="in-stock">$200-$300</SelectItem>
|
||||
<SelectItem value="low-stock">$300-$400</SelectItem>
|
||||
<SelectItem value="archived">$400-$500</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select defaultValue="all">
|
||||
<SelectTrigger>
|
||||
<span className="text-muted-foreground text-sm">Status:</span>
|
||||
<SelectValue placeholder="Select a product" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">In Stock</SelectItem>
|
||||
<SelectItem value="in-stock">Low Stock</SelectItem>
|
||||
<SelectItem value="low-stock">Archived</SelectItem>
|
||||
<SelectItem value="archived">Archived</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button variant="outline" size="icon">
|
||||
<ListFilterIcon />
|
||||
</Button>
|
||||
<Button variant="outline" size="icon">
|
||||
<ArrowUpDownIcon />
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-12 px-4">
|
||||
<Checkbox />
|
||||
</TableHead>
|
||||
<TableHead>Product</TableHead>
|
||||
<TableHead className="text-right">Price</TableHead>
|
||||
<TableHead className="text-right">Stock</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Date Added</TableHead>
|
||||
<TableHead />
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody className="**:data-[slot=table-cell]:py-2.5">
|
||||
{products.map((product) => (
|
||||
<TableRow key={product.id}>
|
||||
<TableCell className="px-4">
|
||||
<Checkbox />
|
||||
</TableCell>
|
||||
<TableCell className="font-medium">{product.name}</TableCell>
|
||||
<TableCell className="text-right">
|
||||
${product.price.toFixed(2)}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">{product.stock}</TableCell>
|
||||
<TableCell>
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className={
|
||||
product.status === "Low Stock"
|
||||
? "border-orange-700 bg-transparent text-orange-700 dark:border-orange-700 dark:bg-transparent dark:text-orange-700"
|
||||
: "bg-green-100 text-green-800 dark:bg-green-950 dark:text-green-100"
|
||||
}
|
||||
>
|
||||
{product.status}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{new Date(product.dateAdded).toLocaleDateString("en-US", {
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
})}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="size-6">
|
||||
<EllipsisVerticalIcon />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem>Edit</DropdownMenuItem>
|
||||
<DropdownMenuItem variant="destructive">
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
<CardFooter className="flex flex-col items-center justify-between border-t pt-6 @3xl/page:flex-row">
|
||||
<div className="text-muted-foreground hidden text-sm @3xl/page:block">
|
||||
Showing 1-10 of 100 products
|
||||
</div>
|
||||
<Pagination className="mx-0 w-fit">
|
||||
<PaginationContent>
|
||||
<PaginationItem>
|
||||
<PaginationPrevious href="#" />
|
||||
</PaginationItem>
|
||||
<PaginationItem>
|
||||
<PaginationLink href="#">1</PaginationLink>
|
||||
</PaginationItem>
|
||||
<PaginationItem>
|
||||
<PaginationLink href="#" isActive>
|
||||
2
|
||||
</PaginationLink>
|
||||
</PaginationItem>
|
||||
<PaginationItem>
|
||||
<PaginationLink href="#">3</PaginationLink>
|
||||
</PaginationItem>
|
||||
<PaginationItem>
|
||||
<PaginationEllipsis />
|
||||
</PaginationItem>
|
||||
<PaginationItem>
|
||||
<PaginationNext href="#" />
|
||||
</PaginationItem>
|
||||
</PaginationContent>
|
||||
</Pagination>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Search } from "lucide-react"
|
||||
|
||||
import { Label } from "@/registry/new-york-v4/ui/label"
|
||||
import { SidebarInput } from "@/registry/new-york-v4/ui/sidebar"
|
||||
|
||||
export function SearchForm({ ...props }: React.ComponentProps<"form">) {
|
||||
return (
|
||||
<form {...props}>
|
||||
<div className="relative">
|
||||
<Label htmlFor="search" className="sr-only">
|
||||
Search
|
||||
</Label>
|
||||
<SidebarInput
|
||||
id="search"
|
||||
placeholder="Type to search..."
|
||||
className="h-8 pl-7"
|
||||
/>
|
||||
<Search className="pointer-events-none absolute top-1/2 left-2 size-4 -translate-y-1/2 opacity-50 select-none" />
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
103
apps/v4/app/(examples)/dashboard-03/components/site-header.tsx
Normal file
103
apps/v4/app/(examples)/dashboard-03/components/site-header.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
"use client"
|
||||
|
||||
import { Fragment, useMemo } from "react"
|
||||
import { usePathname } from "next/navigation"
|
||||
import { SidebarIcon } from "lucide-react"
|
||||
|
||||
import { ThemeSelector } from "@/components/theme-selector"
|
||||
import { SearchForm } from "@/registry/new-york-v4/blocks/sidebar-16/components/search-form"
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbList,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
} from "@/registry/new-york-v4/ui/breadcrumb"
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import { Separator } from "@/registry/new-york-v4/ui/separator"
|
||||
import { useSidebar } from "@/registry/new-york-v4/ui/sidebar"
|
||||
import { ModeToggle } from "@/app/(examples)/dashboard-03/components/mode-toggle"
|
||||
import { NavUser } from "@/app/(examples)/dashboard-03/components/nav-user"
|
||||
|
||||
export function SiteHeader() {
|
||||
const { toggleSidebar } = useSidebar()
|
||||
const pathname = usePathname()
|
||||
|
||||
// Faux breadcrumbs for demo.
|
||||
const breadcrumbs = useMemo(() => {
|
||||
return pathname
|
||||
.split("/")
|
||||
.filter((path) => path !== "")
|
||||
.map((path, index, array) => ({
|
||||
label: path,
|
||||
href: `/${array.slice(0, index + 1).join("/")}`,
|
||||
}))
|
||||
}, [pathname])
|
||||
|
||||
return (
|
||||
<header
|
||||
data-slot="site-header"
|
||||
className="bg-background sticky top-0 z-50 flex w-full items-center border-b"
|
||||
>
|
||||
<div className="flex h-(--header-height) w-full items-center gap-2 px-2 pr-4">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={toggleSidebar}
|
||||
className="gap-2.5 has-[>svg]:px-2"
|
||||
>
|
||||
<SidebarIcon />
|
||||
<span className="truncate font-medium">Acme Inc</span>
|
||||
</Button>
|
||||
<Separator
|
||||
orientation="vertical"
|
||||
className="mr-2 data-[orientation=vertical]:h-4"
|
||||
/>
|
||||
<Breadcrumb className="hidden sm:block">
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink href="/" className="capitalize">
|
||||
Home
|
||||
</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
{breadcrumbs.map((breadcrumb, index) =>
|
||||
index === breadcrumbs.length - 1 ? (
|
||||
<BreadcrumbItem key={index}>
|
||||
<BreadcrumbPage className="capitalize">
|
||||
{breadcrumb.label}
|
||||
</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
) : (
|
||||
<Fragment key={index}>
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink
|
||||
href={breadcrumb.href}
|
||||
className="capitalize"
|
||||
>
|
||||
{breadcrumb.label}
|
||||
</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
</Fragment>
|
||||
)
|
||||
)}
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
<SearchForm className="w-fullsm:w-auto" />
|
||||
<ThemeSelector />
|
||||
<ModeToggle />
|
||||
<NavUser
|
||||
user={{
|
||||
name: "shadcn",
|
||||
email: "m@example.com",
|
||||
avatar: "/avatars/shadcn.jpg",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
8
apps/v4/app/(examples)/dashboard-03/customers/page.tsx
Normal file
8
apps/v4/app/(examples)/dashboard-03/customers/page.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
export default function CustomersPage() {
|
||||
return (
|
||||
<div className="p-6">
|
||||
<div className="bg-input p-4">Input</div>
|
||||
<div className="bg-input/30 p-4">Input 50</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
31
apps/v4/app/(examples)/dashboard-03/layout.tsx
Normal file
31
apps/v4/app/(examples)/dashboard-03/layout.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import { cookies } from "next/headers"
|
||||
|
||||
import {
|
||||
SidebarInset,
|
||||
SidebarProvider,
|
||||
} from "@/registry/new-york-v4/ui/sidebar"
|
||||
import { AppSidebar } from "@/app/(examples)/dashboard-03/components/app-sidebar"
|
||||
import { SiteHeader } from "@/app/(examples)/dashboard-03/components/site-header"
|
||||
|
||||
import "../../themes.css"
|
||||
|
||||
export default async function DashboardLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
const cookieStore = await cookies()
|
||||
const defaultOpen = cookieStore.get("sidebar_state")?.value === "true"
|
||||
|
||||
return (
|
||||
<main className="[--header-height:calc(theme(spacing.14))]">
|
||||
<SidebarProvider defaultOpen={defaultOpen} className="flex flex-col">
|
||||
<SiteHeader />
|
||||
<div className="flex flex-1">
|
||||
<AppSidebar />
|
||||
<SidebarInset>{children}</SidebarInset>
|
||||
</div>
|
||||
</SidebarProvider>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
206
apps/v4/app/(examples)/dashboard-03/page.tsx
Normal file
206
apps/v4/app/(examples)/dashboard-03/page.tsx
Normal file
@@ -0,0 +1,206 @@
|
||||
import { Metadata } from "next"
|
||||
import {
|
||||
DownloadIcon,
|
||||
FilterIcon,
|
||||
TrendingDownIcon,
|
||||
TrendingUpIcon,
|
||||
} from "lucide-react"
|
||||
|
||||
import { Badge } from "@/registry/new-york-v4/ui/badge"
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/new-york-v4/ui/card"
|
||||
import {
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from "@/registry/new-york-v4/ui/tabs"
|
||||
import { AnalyticsDatePicker } from "@/app/(examples)/dashboard-03/components/analytics-date-picker"
|
||||
import { ChartRevenue } from "@/app/(examples)/dashboard-03/components/chart-revenue"
|
||||
import { ChartVisitors } from "@/app/(examples)/dashboard-03/components/chart-visitors"
|
||||
import { ProductsTable } from "@/app/(examples)/dashboard-03/components/products-table"
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Dashboard",
|
||||
description: "An example dashboard to test the new components.",
|
||||
}
|
||||
|
||||
// Load from database.
|
||||
const products = [
|
||||
{
|
||||
id: "1",
|
||||
name: "BJÖRKSNÄS Dining Table",
|
||||
price: 599.99,
|
||||
stock: 12,
|
||||
dateAdded: "2023-06-15",
|
||||
status: "In Stock",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: "POÄNG Armchair",
|
||||
price: 249.99,
|
||||
stock: 28,
|
||||
dateAdded: "2023-07-22",
|
||||
status: "In Stock",
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
name: "MALM Bed Frame",
|
||||
price: 399.99,
|
||||
stock: 15,
|
||||
dateAdded: "2023-08-05",
|
||||
status: "In Stock",
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
name: "KALLAX Shelf Unit",
|
||||
price: 179.99,
|
||||
stock: 32,
|
||||
dateAdded: "2023-09-12",
|
||||
status: "In Stock",
|
||||
},
|
||||
{
|
||||
id: "5",
|
||||
name: "STOCKHOLM Rug",
|
||||
price: 299.99,
|
||||
stock: 8,
|
||||
dateAdded: "2023-10-18",
|
||||
status: "Low Stock",
|
||||
},
|
||||
{
|
||||
id: "6",
|
||||
name: "KIVIK Sofa",
|
||||
price: 899.99,
|
||||
stock: 6,
|
||||
dateAdded: "2023-11-02",
|
||||
status: "Low Stock",
|
||||
},
|
||||
{
|
||||
id: "7",
|
||||
name: "LISABO Coffee Table",
|
||||
price: 149.99,
|
||||
stock: 22,
|
||||
dateAdded: "2023-11-29",
|
||||
status: "In Stock",
|
||||
},
|
||||
{
|
||||
id: "8",
|
||||
name: "HEMNES Bookcase",
|
||||
price: 249.99,
|
||||
stock: 17,
|
||||
dateAdded: "2023-12-10",
|
||||
status: "In Stock",
|
||||
},
|
||||
{
|
||||
id: "9",
|
||||
name: "EKEDALEN Dining Chairs (Set of 2)",
|
||||
price: 199.99,
|
||||
stock: 14,
|
||||
dateAdded: "2024-01-05",
|
||||
status: "In Stock",
|
||||
},
|
||||
{
|
||||
id: "10",
|
||||
name: "FRIHETEN Sleeper Sofa",
|
||||
price: 799.99,
|
||||
stock: 9,
|
||||
dateAdded: "2024-01-18",
|
||||
status: "Low Stock",
|
||||
},
|
||||
]
|
||||
|
||||
export default function DashboardPage() {
|
||||
return (
|
||||
<div className="@container/page flex flex-1 flex-col gap-8 p-6">
|
||||
<Tabs defaultValue="overview" className="gap-6">
|
||||
<div
|
||||
data-slot="dashboard-header"
|
||||
className="flex items-center justify-between"
|
||||
>
|
||||
<TabsList className="w-full @3xl/page:w-fit">
|
||||
<TabsTrigger value="overview">Overview</TabsTrigger>
|
||||
<TabsTrigger value="analytics">Analytics</TabsTrigger>
|
||||
<TabsTrigger value="reports">Reports</TabsTrigger>
|
||||
<TabsTrigger value="exports" disabled>
|
||||
Exports
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<div className="hidden items-center gap-2 @3xl/page:flex">
|
||||
<AnalyticsDatePicker />
|
||||
<Button variant="outline">
|
||||
<FilterIcon />
|
||||
Filter
|
||||
</Button>
|
||||
<Button variant="outline">
|
||||
<DownloadIcon />
|
||||
Export
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<TabsContent value="overview" className="flex flex-col gap-4">
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Total Revenue</CardTitle>
|
||||
<CardDescription>$1,250.00 in the last 30 days</CardDescription>
|
||||
</CardHeader>
|
||||
<CardFooter>
|
||||
<Badge variant="outline">
|
||||
<TrendingUpIcon />
|
||||
+12.5%
|
||||
</Badge>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>New Customers</CardTitle>
|
||||
<CardDescription>-12 customers from last month</CardDescription>
|
||||
</CardHeader>
|
||||
<CardFooter>
|
||||
<Badge variant="outline">
|
||||
<TrendingDownIcon />
|
||||
-20%
|
||||
</Badge>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Active Accounts</CardTitle>
|
||||
<CardDescription>+2,345 users from last month</CardDescription>
|
||||
</CardHeader>
|
||||
<CardFooter>
|
||||
<Badge variant="outline">
|
||||
<TrendingUpIcon />
|
||||
+12.5%
|
||||
</Badge>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Growth Rate</CardTitle>
|
||||
<CardDescription>+12.5% increase per month</CardDescription>
|
||||
</CardHeader>
|
||||
<CardFooter>
|
||||
<Badge variant="outline">
|
||||
<TrendingUpIcon />
|
||||
+4.5%
|
||||
</Badge>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-4 @4xl/page:grid-cols-[2fr_1fr]">
|
||||
<ChartRevenue />
|
||||
<ChartVisitors />
|
||||
</div>
|
||||
<ProductsTable products={products} />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
500
apps/v4/app/(examples)/dashboard-03/settings/page.tsx
Normal file
500
apps/v4/app/(examples)/dashboard-03/settings/page.tsx
Normal file
@@ -0,0 +1,500 @@
|
||||
import { Metadata } from "next"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardAction,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/new-york-v4/ui/card"
|
||||
import { Checkbox } from "@/registry/new-york-v4/ui/checkbox"
|
||||
import { Input } from "@/registry/new-york-v4/ui/input"
|
||||
import { Label } from "@/registry/new-york-v4/ui/label"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/registry/new-york-v4/ui/select"
|
||||
import { Switch } from "@/registry/new-york-v4/ui/switch"
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/registry/new-york-v4/ui/table"
|
||||
import {
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from "@/registry/new-york-v4/ui/tabs"
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Settings",
|
||||
description: "Manage your account settings",
|
||||
}
|
||||
|
||||
const timezones = [
|
||||
{
|
||||
label: "Americas",
|
||||
timezones: [
|
||||
{ value: "America/New_York", label: "(GMT-5) New York" },
|
||||
{ value: "America/Los_Angeles", label: "(GMT-8) Los Angeles" },
|
||||
{ value: "America/Chicago", label: "(GMT-6) Chicago" },
|
||||
{ value: "America/Toronto", label: "(GMT-5) Toronto" },
|
||||
{ value: "America/Vancouver", label: "(GMT-8) Vancouver" },
|
||||
{ value: "America/Sao_Paulo", label: "(GMT-3) São Paulo" },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Europe",
|
||||
timezones: [
|
||||
{ value: "Europe/London", label: "(GMT+0) London" },
|
||||
{ value: "Europe/Paris", label: "(GMT+1) Paris" },
|
||||
{ value: "Europe/Berlin", label: "(GMT+1) Berlin" },
|
||||
{ value: "Europe/Rome", label: "(GMT+1) Rome" },
|
||||
{ value: "Europe/Madrid", label: "(GMT+1) Madrid" },
|
||||
{ value: "Europe/Amsterdam", label: "(GMT+1) Amsterdam" },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Asia/Pacific",
|
||||
timezones: [
|
||||
{ value: "Asia/Tokyo", label: "(GMT+9) Tokyo" },
|
||||
{ value: "Asia/Shanghai", label: "(GMT+8) Shanghai" },
|
||||
{ value: "Asia/Singapore", label: "(GMT+8) Singapore" },
|
||||
{ value: "Asia/Dubai", label: "(GMT+4) Dubai" },
|
||||
{ value: "Australia/Sydney", label: "(GMT+11) Sydney" },
|
||||
{ value: "Asia/Seoul", label: "(GMT+9) Seoul" },
|
||||
],
|
||||
},
|
||||
] as const
|
||||
|
||||
const loginHistory = [
|
||||
{
|
||||
date: "2024-01-01",
|
||||
ip: "192.168.1.1",
|
||||
location: "New York, USA",
|
||||
},
|
||||
{
|
||||
date: "2023-12-29",
|
||||
ip: "172.16.0.100",
|
||||
location: "London, UK",
|
||||
},
|
||||
{
|
||||
date: "2023-12-28",
|
||||
ip: "10.0.0.50",
|
||||
location: "Toronto, Canada",
|
||||
},
|
||||
{
|
||||
date: "2023-12-25",
|
||||
ip: "192.168.2.15",
|
||||
location: "Sydney, Australia",
|
||||
},
|
||||
] as const
|
||||
|
||||
const activeSessions = [
|
||||
{
|
||||
device: "MacBook Pro",
|
||||
browser: "Chrome",
|
||||
os: "macOS",
|
||||
},
|
||||
{
|
||||
device: "iPhone",
|
||||
browser: "Safari",
|
||||
os: "iOS",
|
||||
},
|
||||
{
|
||||
device: "iPad",
|
||||
browser: "Safari",
|
||||
os: "iOS",
|
||||
},
|
||||
{
|
||||
device: "Android Phone",
|
||||
browser: "Chrome",
|
||||
os: "Android",
|
||||
},
|
||||
] as const
|
||||
|
||||
export default function SettingsPage() {
|
||||
return (
|
||||
<div className="@container/page flex flex-1 flex-col gap-8 p-6">
|
||||
<Tabs defaultValue="account" className="gap-6">
|
||||
<div
|
||||
data-slot="dashboard-header"
|
||||
className="flex items-center justify-between"
|
||||
>
|
||||
<TabsList>
|
||||
<TabsTrigger value="account">Account</TabsTrigger>
|
||||
<TabsTrigger value="security">Security</TabsTrigger>
|
||||
<TabsTrigger value="notifications">Notifications</TabsTrigger>
|
||||
<TabsTrigger value="privacy">Privacy</TabsTrigger>
|
||||
</TabsList>
|
||||
</div>
|
||||
<TabsContent value="account" className="grid gap-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Account Settings</CardTitle>
|
||||
<CardDescription>
|
||||
Make changes to your account here.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form id="form-account" className="@container">
|
||||
<FieldGroup>
|
||||
<Field>
|
||||
<Label htmlFor="name">Name</Label>
|
||||
<FieldControl>
|
||||
<Input
|
||||
id="name"
|
||||
placeholder="First and last name"
|
||||
required
|
||||
/>
|
||||
</FieldControl>
|
||||
<FieldDescription>
|
||||
This is your public display name.
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
<Field>
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<FieldControl>
|
||||
<Input
|
||||
id="email"
|
||||
placeholder="you@example.com"
|
||||
required
|
||||
/>
|
||||
</FieldControl>
|
||||
</Field>
|
||||
<Field>
|
||||
<Label htmlFor="timezone">Timezone</Label>
|
||||
<FieldControl>
|
||||
<Select>
|
||||
<SelectTrigger id="timezone">
|
||||
<SelectValue placeholder="Select a timezone" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{timezones.map((timezone) => (
|
||||
<SelectGroup key={timezone.label}>
|
||||
<SelectLabel>{timezone.label}</SelectLabel>
|
||||
{timezone.timezones.map((time) => (
|
||||
<SelectItem key={time.value} value={time.value}>
|
||||
{time.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FieldControl>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</form>
|
||||
</CardContent>
|
||||
<CardFooter className="border-t">
|
||||
<Button type="submit" form="form-account" variant="secondary">
|
||||
Save changes
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Notifications</CardTitle>
|
||||
<CardDescription>
|
||||
Manage how you receive notifications.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form id="form-notifications" className="@container">
|
||||
<FieldGroup>
|
||||
<Field>
|
||||
<Label htmlFor="channels">Notification Channels</Label>
|
||||
<FieldControl className="flex flex-col gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox id="notification-email" />
|
||||
<Label htmlFor="notification-email">Email</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox id="notification-sms" />
|
||||
<Label htmlFor="notification-sms">SMS</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox id="notification-push" />
|
||||
<Label htmlFor="notification-push">Push</Label>
|
||||
</div>
|
||||
</FieldControl>
|
||||
<FieldDescription>
|
||||
Choose how you want to receive notifications.
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
<Field>
|
||||
<Label htmlFor="types">Notification Types</Label>
|
||||
<FieldControl className="flex flex-col gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox id="notification-account" />
|
||||
<Label htmlFor="notification-account">
|
||||
Account Activity
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
id="notification-security"
|
||||
defaultChecked
|
||||
disabled
|
||||
/>
|
||||
<Label htmlFor="notification-security">
|
||||
Security Alerts
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox id="notification-marketing" />
|
||||
<Label htmlFor="notification-marketing">
|
||||
Marketing & Promotions
|
||||
</Label>
|
||||
</div>
|
||||
</FieldControl>
|
||||
<FieldDescription>
|
||||
Choose how you want to receive notifications.
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</form>
|
||||
</CardContent>
|
||||
<CardFooter className="border-t">
|
||||
<Button
|
||||
type="submit"
|
||||
form="form-notifications"
|
||||
variant="secondary"
|
||||
>
|
||||
Save changes
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
<TabsContent
|
||||
value="security"
|
||||
className="grid gap-6 @3xl/page:grid-cols-2"
|
||||
>
|
||||
<Card className="@3xl/page:col-span-2">
|
||||
<CardHeader>
|
||||
<CardTitle>Security Settings</CardTitle>
|
||||
<CardDescription>
|
||||
Make changes to your security settings here.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="@container">
|
||||
<form id="form-security">
|
||||
<FieldGroup>
|
||||
<Field>
|
||||
<Label htmlFor="current-password">Current Password</Label>
|
||||
<FieldControl>
|
||||
<Input
|
||||
id="current-password"
|
||||
placeholder="Current password"
|
||||
required
|
||||
/>
|
||||
</FieldControl>
|
||||
<FieldDescription>
|
||||
This is your current password.
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
<Field>
|
||||
<Label htmlFor="new-password">New Password</Label>
|
||||
<FieldControl>
|
||||
<Input
|
||||
id="new-password"
|
||||
placeholder="New password"
|
||||
required
|
||||
/>
|
||||
</FieldControl>
|
||||
</Field>
|
||||
<Field>
|
||||
<Label htmlFor="confirm-password">Confirm Password</Label>
|
||||
<FieldControl>
|
||||
<Input
|
||||
id="confirm-password"
|
||||
placeholder="Confirm password"
|
||||
/>
|
||||
</FieldControl>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldControl>
|
||||
<Switch
|
||||
id="enable-two-factor-auth"
|
||||
className="self-start"
|
||||
/>
|
||||
</FieldControl>
|
||||
<Label htmlFor="enable-two-factor-auth">
|
||||
Enable two-factor authentication
|
||||
</Label>
|
||||
<FieldDescription>
|
||||
This will add an extra layer of security to your account.
|
||||
Make this an extra long description to test the layout.
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</form>
|
||||
</CardContent>
|
||||
<CardFooter className="border-t">
|
||||
<Button type="submit" form="form-security" variant="secondary">
|
||||
Save changes
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Login History</CardTitle>
|
||||
<CardDescription>
|
||||
Recent login activities on your account.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Date</TableHead>
|
||||
<TableHead className="hidden @md/page:table-cell">
|
||||
IP
|
||||
</TableHead>
|
||||
<TableHead>Location</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{loginHistory.map((login) => (
|
||||
<TableRow key={login.date}>
|
||||
<TableCell>
|
||||
<div className="flex flex-col gap-1">
|
||||
{new Date(login.date).toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
})}
|
||||
<span className="flex @md/page:hidden">
|
||||
{login.ip}
|
||||
</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="hidden @md/page:table-cell">
|
||||
{login.ip}
|
||||
</TableCell>
|
||||
<TableCell>{login.location}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Active Sessions</CardTitle>
|
||||
<CardDescription>
|
||||
Current active sessions on your account.
|
||||
</CardDescription>
|
||||
<CardAction>
|
||||
<Button variant="outline" size="sm">
|
||||
<span className="hidden @md/card-header:block">
|
||||
Manage Sessions
|
||||
</span>
|
||||
<span className="block @md/card-header:hidden">Manage</span>
|
||||
</Button>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Device</TableHead>
|
||||
<TableHead>Browser</TableHead>
|
||||
<TableHead>OS</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{activeSessions.map((session) => (
|
||||
<TableRow key={session.device}>
|
||||
<TableCell>{session.device}</TableCell>
|
||||
<TableCell>{session.browser}</TableCell>
|
||||
<TableCell>{session.os}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function FieldGroup({ children }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="field-group"
|
||||
className="@container/field-group flex max-w-4xl min-w-0 flex-col gap-8 @3xl:gap-6"
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Field({ children, className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="field"
|
||||
className={cn(
|
||||
"grid auto-rows-min items-start gap-3 *:data-[slot=label]:col-start-1 *:data-[slot=label]:row-start-1 @3xl/field-group:grid-cols-2 @3xl/field-group:gap-6",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function FieldControl({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="field-control"
|
||||
className={cn(
|
||||
"@3xl/field-group:col-start-2 @3xl/field-group:row-span-2 @3xl/field-group:row-start-1 @3xl/field-group:self-start",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function FieldDescription({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"p">) {
|
||||
return (
|
||||
<p
|
||||
data-slot="field-description"
|
||||
className={cn(
|
||||
"text-muted-foreground text-sm @3xl/field-group:col-start-1 @3xl/field-group:row-start-1 @3xl/field-group:translate-y-6",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
181
apps/v4/app/(examples)/dashboard/components/app-sidebar.tsx
Normal file
181
apps/v4/app/(examples)/dashboard/components/app-sidebar.tsx
Normal file
@@ -0,0 +1,181 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import {
|
||||
IconCamera,
|
||||
IconChartBar,
|
||||
IconDashboard,
|
||||
IconDatabase,
|
||||
IconFileAi,
|
||||
IconFileDescription,
|
||||
IconFileWord,
|
||||
IconFolder,
|
||||
IconHelp,
|
||||
IconInnerShadowTop,
|
||||
IconListDetails,
|
||||
IconReport,
|
||||
IconSearch,
|
||||
IconSettings,
|
||||
IconUsers,
|
||||
} from "@tabler/icons-react"
|
||||
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarFooter,
|
||||
SidebarHeader,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
} from "@/registry/new-york-v4/ui/sidebar"
|
||||
import { NavDocuments } from "@/app/(examples)/dashboard/components/nav-documents"
|
||||
import { NavMain } from "@/app/(examples)/dashboard/components/nav-main"
|
||||
import { NavSecondary } from "@/app/(examples)/dashboard/components/nav-secondary"
|
||||
import { NavUser } from "@/app/(examples)/dashboard/components/nav-user"
|
||||
|
||||
const data = {
|
||||
user: {
|
||||
name: "shadcn",
|
||||
email: "m@example.com",
|
||||
avatar: "/avatars/shadcn.jpg",
|
||||
},
|
||||
navMain: [
|
||||
{
|
||||
title: "Dashboard",
|
||||
url: "#",
|
||||
icon: IconDashboard,
|
||||
},
|
||||
{
|
||||
title: "Lifecycle",
|
||||
url: "#",
|
||||
icon: IconListDetails,
|
||||
},
|
||||
{
|
||||
title: "Analytics",
|
||||
url: "#",
|
||||
icon: IconChartBar,
|
||||
},
|
||||
{
|
||||
title: "Projects",
|
||||
url: "#",
|
||||
icon: IconFolder,
|
||||
},
|
||||
{
|
||||
title: "Team",
|
||||
url: "#",
|
||||
icon: IconUsers,
|
||||
},
|
||||
],
|
||||
navClouds: [
|
||||
{
|
||||
title: "Capture",
|
||||
icon: IconCamera,
|
||||
isActive: true,
|
||||
url: "#",
|
||||
items: [
|
||||
{
|
||||
title: "Active Proposals",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "Archived",
|
||||
url: "#",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Proposal",
|
||||
icon: IconFileDescription,
|
||||
url: "#",
|
||||
items: [
|
||||
{
|
||||
title: "Active Proposals",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "Archived",
|
||||
url: "#",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Prompts",
|
||||
icon: IconFileAi,
|
||||
url: "#",
|
||||
items: [
|
||||
{
|
||||
title: "Active Proposals",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "Archived",
|
||||
url: "#",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
navSecondary: [
|
||||
{
|
||||
title: "Settings",
|
||||
url: "#",
|
||||
icon: IconSettings,
|
||||
},
|
||||
{
|
||||
title: "Get Help",
|
||||
url: "#",
|
||||
icon: IconHelp,
|
||||
},
|
||||
{
|
||||
title: "Search",
|
||||
url: "#",
|
||||
icon: IconSearch,
|
||||
},
|
||||
],
|
||||
documents: [
|
||||
{
|
||||
name: "Data Library",
|
||||
url: "#",
|
||||
icon: IconDatabase,
|
||||
},
|
||||
{
|
||||
name: "Reports",
|
||||
url: "#",
|
||||
icon: IconReport,
|
||||
},
|
||||
{
|
||||
name: "Word Assistant",
|
||||
url: "#",
|
||||
icon: IconFileWord,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
return (
|
||||
<Sidebar collapsible="offcanvas" {...props}>
|
||||
<SidebarHeader>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton
|
||||
asChild
|
||||
className="data-[slot=sidebar-menu-button]:!p-1.5"
|
||||
>
|
||||
<a href="#">
|
||||
<IconInnerShadowTop className="!size-5" />
|
||||
<span className="text-base font-semibold">Acme Inc.</span>
|
||||
</a>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarHeader>
|
||||
<SidebarContent>
|
||||
<NavMain items={data.navMain} />
|
||||
<NavDocuments items={data.documents} />
|
||||
<NavSecondary items={data.navSecondary} className="mt-auto" />
|
||||
</SidebarContent>
|
||||
<SidebarFooter>
|
||||
<NavUser user={data.user} />
|
||||
</SidebarFooter>
|
||||
</Sidebar>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,292 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Area, AreaChart, CartesianGrid, XAxis } from "recharts"
|
||||
|
||||
import { useIsMobile } from "@/registry/new-york-v4/hooks/use-mobile"
|
||||
import {
|
||||
Card,
|
||||
CardAction,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/new-york-v4/ui/card"
|
||||
import {
|
||||
ChartConfig,
|
||||
ChartContainer,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
} from "@/registry/new-york-v4/ui/chart"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/registry/new-york-v4/ui/select"
|
||||
import {
|
||||
ToggleGroup,
|
||||
ToggleGroupItem,
|
||||
} from "@/registry/new-york-v4/ui/toggle-group"
|
||||
|
||||
export const description = "An interactive area chart"
|
||||
|
||||
const chartData = [
|
||||
{ date: "2024-04-01", desktop: 222, mobile: 150 },
|
||||
{ date: "2024-04-02", desktop: 97, mobile: 180 },
|
||||
{ date: "2024-04-03", desktop: 167, mobile: 120 },
|
||||
{ date: "2024-04-04", desktop: 242, mobile: 260 },
|
||||
{ date: "2024-04-05", desktop: 373, mobile: 290 },
|
||||
{ date: "2024-04-06", desktop: 301, mobile: 340 },
|
||||
{ date: "2024-04-07", desktop: 245, mobile: 180 },
|
||||
{ date: "2024-04-08", desktop: 409, mobile: 320 },
|
||||
{ date: "2024-04-09", desktop: 59, mobile: 110 },
|
||||
{ date: "2024-04-10", desktop: 261, mobile: 190 },
|
||||
{ date: "2024-04-11", desktop: 327, mobile: 350 },
|
||||
{ date: "2024-04-12", desktop: 292, mobile: 210 },
|
||||
{ date: "2024-04-13", desktop: 342, mobile: 380 },
|
||||
{ date: "2024-04-14", desktop: 137, mobile: 220 },
|
||||
{ date: "2024-04-15", desktop: 120, mobile: 170 },
|
||||
{ date: "2024-04-16", desktop: 138, mobile: 190 },
|
||||
{ date: "2024-04-17", desktop: 446, mobile: 360 },
|
||||
{ date: "2024-04-18", desktop: 364, mobile: 410 },
|
||||
{ date: "2024-04-19", desktop: 243, mobile: 180 },
|
||||
{ date: "2024-04-20", desktop: 89, mobile: 150 },
|
||||
{ date: "2024-04-21", desktop: 137, mobile: 200 },
|
||||
{ date: "2024-04-22", desktop: 224, mobile: 170 },
|
||||
{ date: "2024-04-23", desktop: 138, mobile: 230 },
|
||||
{ date: "2024-04-24", desktop: 387, mobile: 290 },
|
||||
{ date: "2024-04-25", desktop: 215, mobile: 250 },
|
||||
{ date: "2024-04-26", desktop: 75, mobile: 130 },
|
||||
{ date: "2024-04-27", desktop: 383, mobile: 420 },
|
||||
{ date: "2024-04-28", desktop: 122, mobile: 180 },
|
||||
{ date: "2024-04-29", desktop: 315, mobile: 240 },
|
||||
{ date: "2024-04-30", desktop: 454, mobile: 380 },
|
||||
{ date: "2024-05-01", desktop: 165, mobile: 220 },
|
||||
{ date: "2024-05-02", desktop: 293, mobile: 310 },
|
||||
{ date: "2024-05-03", desktop: 247, mobile: 190 },
|
||||
{ date: "2024-05-04", desktop: 385, mobile: 420 },
|
||||
{ date: "2024-05-05", desktop: 481, mobile: 390 },
|
||||
{ date: "2024-05-06", desktop: 498, mobile: 520 },
|
||||
{ date: "2024-05-07", desktop: 388, mobile: 300 },
|
||||
{ date: "2024-05-08", desktop: 149, mobile: 210 },
|
||||
{ date: "2024-05-09", desktop: 227, mobile: 180 },
|
||||
{ date: "2024-05-10", desktop: 293, mobile: 330 },
|
||||
{ date: "2024-05-11", desktop: 335, mobile: 270 },
|
||||
{ date: "2024-05-12", desktop: 197, mobile: 240 },
|
||||
{ date: "2024-05-13", desktop: 197, mobile: 160 },
|
||||
{ date: "2024-05-14", desktop: 448, mobile: 490 },
|
||||
{ date: "2024-05-15", desktop: 473, mobile: 380 },
|
||||
{ date: "2024-05-16", desktop: 338, mobile: 400 },
|
||||
{ date: "2024-05-17", desktop: 499, mobile: 420 },
|
||||
{ date: "2024-05-18", desktop: 315, mobile: 350 },
|
||||
{ date: "2024-05-19", desktop: 235, mobile: 180 },
|
||||
{ date: "2024-05-20", desktop: 177, mobile: 230 },
|
||||
{ date: "2024-05-21", desktop: 82, mobile: 140 },
|
||||
{ date: "2024-05-22", desktop: 81, mobile: 120 },
|
||||
{ date: "2024-05-23", desktop: 252, mobile: 290 },
|
||||
{ date: "2024-05-24", desktop: 294, mobile: 220 },
|
||||
{ date: "2024-05-25", desktop: 201, mobile: 250 },
|
||||
{ date: "2024-05-26", desktop: 213, mobile: 170 },
|
||||
{ date: "2024-05-27", desktop: 420, mobile: 460 },
|
||||
{ date: "2024-05-28", desktop: 233, mobile: 190 },
|
||||
{ date: "2024-05-29", desktop: 78, mobile: 130 },
|
||||
{ date: "2024-05-30", desktop: 340, mobile: 280 },
|
||||
{ date: "2024-05-31", desktop: 178, mobile: 230 },
|
||||
{ date: "2024-06-01", desktop: 178, mobile: 200 },
|
||||
{ date: "2024-06-02", desktop: 470, mobile: 410 },
|
||||
{ date: "2024-06-03", desktop: 103, mobile: 160 },
|
||||
{ date: "2024-06-04", desktop: 439, mobile: 380 },
|
||||
{ date: "2024-06-05", desktop: 88, mobile: 140 },
|
||||
{ date: "2024-06-06", desktop: 294, mobile: 250 },
|
||||
{ date: "2024-06-07", desktop: 323, mobile: 370 },
|
||||
{ date: "2024-06-08", desktop: 385, mobile: 320 },
|
||||
{ date: "2024-06-09", desktop: 438, mobile: 480 },
|
||||
{ date: "2024-06-10", desktop: 155, mobile: 200 },
|
||||
{ date: "2024-06-11", desktop: 92, mobile: 150 },
|
||||
{ date: "2024-06-12", desktop: 492, mobile: 420 },
|
||||
{ date: "2024-06-13", desktop: 81, mobile: 130 },
|
||||
{ date: "2024-06-14", desktop: 426, mobile: 380 },
|
||||
{ date: "2024-06-15", desktop: 307, mobile: 350 },
|
||||
{ date: "2024-06-16", desktop: 371, mobile: 310 },
|
||||
{ date: "2024-06-17", desktop: 475, mobile: 520 },
|
||||
{ date: "2024-06-18", desktop: 107, mobile: 170 },
|
||||
{ date: "2024-06-19", desktop: 341, mobile: 290 },
|
||||
{ date: "2024-06-20", desktop: 408, mobile: 450 },
|
||||
{ date: "2024-06-21", desktop: 169, mobile: 210 },
|
||||
{ date: "2024-06-22", desktop: 317, mobile: 270 },
|
||||
{ date: "2024-06-23", desktop: 480, mobile: 530 },
|
||||
{ date: "2024-06-24", desktop: 132, mobile: 180 },
|
||||
{ date: "2024-06-25", desktop: 141, mobile: 190 },
|
||||
{ date: "2024-06-26", desktop: 434, mobile: 380 },
|
||||
{ date: "2024-06-27", desktop: 448, mobile: 490 },
|
||||
{ date: "2024-06-28", desktop: 149, mobile: 200 },
|
||||
{ date: "2024-06-29", desktop: 103, mobile: 160 },
|
||||
{ date: "2024-06-30", desktop: 446, mobile: 400 },
|
||||
]
|
||||
|
||||
const chartConfig = {
|
||||
visitors: {
|
||||
label: "Visitors",
|
||||
},
|
||||
desktop: {
|
||||
label: "Desktop",
|
||||
color: "var(--primary)",
|
||||
},
|
||||
mobile: {
|
||||
label: "Mobile",
|
||||
color: "var(--primary)",
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
|
||||
export function ChartAreaInteractive() {
|
||||
const isMobile = useIsMobile()
|
||||
const [timeRange, setTimeRange] = React.useState("90d")
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isMobile) {
|
||||
setTimeRange("7d")
|
||||
}
|
||||
}, [isMobile])
|
||||
|
||||
const filteredData = chartData.filter((item) => {
|
||||
const date = new Date(item.date)
|
||||
const referenceDate = new Date("2024-06-30")
|
||||
let daysToSubtract = 90
|
||||
if (timeRange === "30d") {
|
||||
daysToSubtract = 30
|
||||
} else if (timeRange === "7d") {
|
||||
daysToSubtract = 7
|
||||
}
|
||||
const startDate = new Date(referenceDate)
|
||||
startDate.setDate(startDate.getDate() - daysToSubtract)
|
||||
return date >= startDate
|
||||
})
|
||||
|
||||
return (
|
||||
<Card className="@container/card">
|
||||
<CardHeader>
|
||||
<CardTitle>Total Visitors</CardTitle>
|
||||
<CardDescription>
|
||||
<span className="hidden @[540px]/card:block">
|
||||
Total for the last 3 months
|
||||
</span>
|
||||
<span className="@[540px]/card:hidden">Last 3 months</span>
|
||||
</CardDescription>
|
||||
<CardAction>
|
||||
<ToggleGroup
|
||||
type="single"
|
||||
value={timeRange}
|
||||
onValueChange={setTimeRange}
|
||||
variant="outline"
|
||||
className="hidden *:data-[slot=toggle-group-item]:!px-4 @[767px]/card:flex"
|
||||
>
|
||||
<ToggleGroupItem value="90d">Last 3 months</ToggleGroupItem>
|
||||
<ToggleGroupItem value="30d">Last 30 days</ToggleGroupItem>
|
||||
<ToggleGroupItem value="7d">Last 7 days</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
<Select value={timeRange} onValueChange={setTimeRange}>
|
||||
<SelectTrigger
|
||||
className="flex w-40 **:data-[slot=select-value]:block **:data-[slot=select-value]:truncate @[767px]/card:hidden"
|
||||
size="sm"
|
||||
aria-label="Select a value"
|
||||
>
|
||||
<SelectValue placeholder="Last 3 months" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="rounded-xl">
|
||||
<SelectItem value="90d" className="rounded-lg">
|
||||
Last 3 months
|
||||
</SelectItem>
|
||||
<SelectItem value="30d" className="rounded-lg">
|
||||
Last 30 days
|
||||
</SelectItem>
|
||||
<SelectItem value="7d" className="rounded-lg">
|
||||
Last 7 days
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<CardContent className="px-2 pt-4 sm:px-6 sm:pt-6">
|
||||
<ChartContainer
|
||||
config={chartConfig}
|
||||
className="aspect-auto h-[250px] w-full"
|
||||
>
|
||||
<AreaChart data={filteredData}>
|
||||
<defs>
|
||||
<linearGradient id="fillDesktop" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop
|
||||
offset="5%"
|
||||
stopColor="var(--color-desktop)"
|
||||
stopOpacity={1.0}
|
||||
/>
|
||||
<stop
|
||||
offset="95%"
|
||||
stopColor="var(--color-desktop)"
|
||||
stopOpacity={0.1}
|
||||
/>
|
||||
</linearGradient>
|
||||
<linearGradient id="fillMobile" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop
|
||||
offset="5%"
|
||||
stopColor="var(--color-mobile)"
|
||||
stopOpacity={0.8}
|
||||
/>
|
||||
<stop
|
||||
offset="95%"
|
||||
stopColor="var(--color-mobile)"
|
||||
stopOpacity={0.1}
|
||||
/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<CartesianGrid vertical={false} />
|
||||
<XAxis
|
||||
dataKey="date"
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={8}
|
||||
minTickGap={32}
|
||||
tickFormatter={(value) => {
|
||||
const date = new Date(value)
|
||||
return date.toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
})
|
||||
}}
|
||||
/>
|
||||
<ChartTooltip
|
||||
cursor={false}
|
||||
defaultIndex={isMobile ? -1 : 10}
|
||||
content={
|
||||
<ChartTooltipContent
|
||||
labelFormatter={(value) => {
|
||||
return new Date(value).toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
})
|
||||
}}
|
||||
indicator="dot"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Area
|
||||
dataKey="mobile"
|
||||
type="natural"
|
||||
fill="url(#fillMobile)"
|
||||
stroke="var(--color-mobile)"
|
||||
stackId="a"
|
||||
/>
|
||||
<Area
|
||||
dataKey="desktop"
|
||||
type="natural"
|
||||
fill="url(#fillDesktop)"
|
||||
stroke="var(--color-desktop)"
|
||||
stackId="a"
|
||||
/>
|
||||
</AreaChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
807
apps/v4/app/(examples)/dashboard/components/data-table.tsx
Normal file
807
apps/v4/app/(examples)/dashboard/components/data-table.tsx
Normal file
@@ -0,0 +1,807 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import {
|
||||
closestCenter,
|
||||
DndContext,
|
||||
KeyboardSensor,
|
||||
MouseSensor,
|
||||
TouchSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
type DragEndEvent,
|
||||
type UniqueIdentifier,
|
||||
} from "@dnd-kit/core"
|
||||
import { restrictToVerticalAxis } from "@dnd-kit/modifiers"
|
||||
import {
|
||||
arrayMove,
|
||||
SortableContext,
|
||||
useSortable,
|
||||
verticalListSortingStrategy,
|
||||
} from "@dnd-kit/sortable"
|
||||
import { CSS } from "@dnd-kit/utilities"
|
||||
import {
|
||||
IconChevronDown,
|
||||
IconChevronLeft,
|
||||
IconChevronRight,
|
||||
IconChevronsLeft,
|
||||
IconChevronsRight,
|
||||
IconCircleCheckFilled,
|
||||
IconDotsVertical,
|
||||
IconGripVertical,
|
||||
IconLayoutColumns,
|
||||
IconLoader,
|
||||
IconPlus,
|
||||
IconTrendingUp,
|
||||
} from "@tabler/icons-react"
|
||||
import {
|
||||
ColumnDef,
|
||||
ColumnFiltersState,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
getFacetedRowModel,
|
||||
getFacetedUniqueValues,
|
||||
getFilteredRowModel,
|
||||
getPaginationRowModel,
|
||||
getSortedRowModel,
|
||||
Row,
|
||||
SortingState,
|
||||
useReactTable,
|
||||
VisibilityState,
|
||||
} from "@tanstack/react-table"
|
||||
import { Area, AreaChart, CartesianGrid, XAxis } from "recharts"
|
||||
import { toast } from "sonner"
|
||||
import { z } from "zod"
|
||||
|
||||
import { useIsMobile } from "@/registry/new-york-v4/hooks/use-mobile"
|
||||
import { Badge } from "@/registry/new-york-v4/ui/badge"
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import {
|
||||
ChartConfig,
|
||||
ChartContainer,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
} from "@/registry/new-york-v4/ui/chart"
|
||||
import { Checkbox } from "@/registry/new-york-v4/ui/checkbox"
|
||||
import {
|
||||
Drawer,
|
||||
DrawerClose,
|
||||
DrawerContent,
|
||||
DrawerDescription,
|
||||
DrawerFooter,
|
||||
DrawerHeader,
|
||||
DrawerTitle,
|
||||
DrawerTrigger,
|
||||
} from "@/registry/new-york-v4/ui/drawer"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/registry/new-york-v4/ui/dropdown-menu"
|
||||
import { Input } from "@/registry/new-york-v4/ui/input"
|
||||
import { Label } from "@/registry/new-york-v4/ui/label"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/registry/new-york-v4/ui/select"
|
||||
import { Separator } from "@/registry/new-york-v4/ui/separator"
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/registry/new-york-v4/ui/table"
|
||||
import {
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from "@/registry/new-york-v4/ui/tabs"
|
||||
|
||||
export const schema = z.object({
|
||||
id: z.number(),
|
||||
header: z.string(),
|
||||
type: z.string(),
|
||||
status: z.string(),
|
||||
target: z.string(),
|
||||
limit: z.string(),
|
||||
reviewer: z.string(),
|
||||
})
|
||||
|
||||
// Create a separate component for the drag handle
|
||||
function DragHandle({ id }: { id: number }) {
|
||||
const { attributes, listeners } = useSortable({
|
||||
id,
|
||||
})
|
||||
|
||||
return (
|
||||
<Button
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="text-muted-foreground size-7 hover:bg-transparent"
|
||||
>
|
||||
<IconGripVertical className="text-muted-foreground size-3" />
|
||||
<span className="sr-only">Drag to reorder</span>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
const columns: ColumnDef<z.infer<typeof schema>>[] = [
|
||||
{
|
||||
id: "drag",
|
||||
header: () => null,
|
||||
cell: ({ row }) => <DragHandle id={row.original.id} />,
|
||||
},
|
||||
{
|
||||
id: "select",
|
||||
header: ({ table }) => (
|
||||
<div className="flex items-center justify-center">
|
||||
<Checkbox
|
||||
checked={
|
||||
table.getIsAllPageRowsSelected() ||
|
||||
(table.getIsSomePageRowsSelected() && "indeterminate")
|
||||
}
|
||||
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
|
||||
aria-label="Select all"
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<div className="flex items-center justify-center">
|
||||
<Checkbox
|
||||
checked={row.getIsSelected()}
|
||||
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
||||
aria-label="Select row"
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
},
|
||||
{
|
||||
accessorKey: "header",
|
||||
header: "Header",
|
||||
cell: ({ row }) => {
|
||||
return <TableCellViewer item={row.original} />
|
||||
},
|
||||
enableHiding: false,
|
||||
},
|
||||
{
|
||||
accessorKey: "type",
|
||||
header: "Section Type",
|
||||
cell: ({ row }) => (
|
||||
<div className="w-32">
|
||||
<Badge variant="outline" className="text-muted-foreground px-1.5">
|
||||
{row.original.type}
|
||||
</Badge>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "status",
|
||||
header: "Status",
|
||||
cell: ({ row }) => (
|
||||
<Badge variant="outline" className="text-muted-foreground px-1.5">
|
||||
{row.original.status === "Done" ? (
|
||||
<IconCircleCheckFilled className="fill-green-500 dark:fill-green-400" />
|
||||
) : (
|
||||
<IconLoader />
|
||||
)}
|
||||
{row.original.status}
|
||||
</Badge>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "target",
|
||||
header: () => <div className="w-full text-right">Target</div>,
|
||||
cell: ({ row }) => (
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault()
|
||||
toast.promise(new Promise((resolve) => setTimeout(resolve, 1000)), {
|
||||
loading: `Saving ${row.original.header}`,
|
||||
success: "Done",
|
||||
error: "Error",
|
||||
})
|
||||
}}
|
||||
>
|
||||
<Label htmlFor={`${row.original.id}-target`} className="sr-only">
|
||||
Target
|
||||
</Label>
|
||||
<Input
|
||||
className="hover:bg-input/30 focus-visible:bg-background dark:hover:bg-input/30 dark:focus-visible:bg-input/30 h-8 w-16 border-transparent bg-transparent text-right shadow-none focus-visible:border dark:bg-transparent"
|
||||
defaultValue={row.original.target}
|
||||
id={`${row.original.id}-target`}
|
||||
/>
|
||||
</form>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "limit",
|
||||
header: () => <div className="w-full text-right">Limit</div>,
|
||||
cell: ({ row }) => (
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault()
|
||||
toast.promise(new Promise((resolve) => setTimeout(resolve, 1000)), {
|
||||
loading: `Saving ${row.original.header}`,
|
||||
success: "Done",
|
||||
error: "Error",
|
||||
})
|
||||
}}
|
||||
>
|
||||
<Label htmlFor={`${row.original.id}-limit`} className="sr-only">
|
||||
Limit
|
||||
</Label>
|
||||
<Input
|
||||
className="hover:bg-input/30 focus-visible:bg-background dark:hover:bg-input/30 dark:focus-visible:bg-input/30 h-8 w-16 border-transparent bg-transparent text-right shadow-none focus-visible:border dark:bg-transparent"
|
||||
defaultValue={row.original.limit}
|
||||
id={`${row.original.id}-limit`}
|
||||
/>
|
||||
</form>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "reviewer",
|
||||
header: "Reviewer",
|
||||
cell: ({ row }) => {
|
||||
const isAssigned = row.original.reviewer !== "Assign reviewer"
|
||||
|
||||
if (isAssigned) {
|
||||
return row.original.reviewer
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Label htmlFor={`${row.original.id}-reviewer`} className="sr-only">
|
||||
Reviewer
|
||||
</Label>
|
||||
<Select>
|
||||
<SelectTrigger
|
||||
className="w-38 **:data-[slot=select-value]:block **:data-[slot=select-value]:truncate"
|
||||
size="sm"
|
||||
id={`${row.original.id}-reviewer`}
|
||||
>
|
||||
<SelectValue placeholder="Assign reviewer" />
|
||||
</SelectTrigger>
|
||||
<SelectContent align="end">
|
||||
<SelectItem value="Eddie Lake">Eddie Lake</SelectItem>
|
||||
<SelectItem value="Jamik Tashpulatov">
|
||||
Jamik Tashpulatov
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
cell: () => (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="data-[state=open]:bg-muted text-muted-foreground flex size-8"
|
||||
size="icon"
|
||||
>
|
||||
<IconDotsVertical />
|
||||
<span className="sr-only">Open menu</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-32">
|
||||
<DropdownMenuItem>Edit</DropdownMenuItem>
|
||||
<DropdownMenuItem>Make a copy</DropdownMenuItem>
|
||||
<DropdownMenuItem>Favorite</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem variant="destructive">Delete</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
function DraggableRow({ row }: { row: Row<z.infer<typeof schema>> }) {
|
||||
const { transform, transition, setNodeRef, isDragging } = useSortable({
|
||||
id: row.original.id,
|
||||
})
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
data-state={row.getIsSelected() && "selected"}
|
||||
data-dragging={isDragging}
|
||||
ref={setNodeRef}
|
||||
className="relative z-0 data-[dragging=true]:z-10 data-[dragging=true]:opacity-80"
|
||||
style={{
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition: transition,
|
||||
}}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id}>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
)
|
||||
}
|
||||
|
||||
export function DataTable({
|
||||
data: initialData,
|
||||
}: {
|
||||
data: z.infer<typeof schema>[]
|
||||
}) {
|
||||
const [data, setData] = React.useState(() => initialData)
|
||||
const [rowSelection, setRowSelection] = React.useState({})
|
||||
const [columnVisibility, setColumnVisibility] =
|
||||
React.useState<VisibilityState>({})
|
||||
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
|
||||
[]
|
||||
)
|
||||
const [sorting, setSorting] = React.useState<SortingState>([])
|
||||
const [pagination, setPagination] = React.useState({
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
})
|
||||
const sortableId = React.useId()
|
||||
const sensors = useSensors(
|
||||
useSensor(MouseSensor, {}),
|
||||
useSensor(TouchSensor, {}),
|
||||
useSensor(KeyboardSensor, {})
|
||||
)
|
||||
|
||||
const dataIds = React.useMemo<UniqueIdentifier[]>(
|
||||
() => data?.map(({ id }) => id) || [],
|
||||
[data]
|
||||
)
|
||||
|
||||
const table = useReactTable({
|
||||
data,
|
||||
columns,
|
||||
state: {
|
||||
sorting,
|
||||
columnVisibility,
|
||||
rowSelection,
|
||||
columnFilters,
|
||||
pagination,
|
||||
},
|
||||
getRowId: (row) => row.id.toString(),
|
||||
enableRowSelection: true,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
onSortingChange: setSorting,
|
||||
onColumnFiltersChange: setColumnFilters,
|
||||
onColumnVisibilityChange: setColumnVisibility,
|
||||
onPaginationChange: setPagination,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getFilteredRowModel: getFilteredRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
getFacetedRowModel: getFacetedRowModel(),
|
||||
getFacetedUniqueValues: getFacetedUniqueValues(),
|
||||
})
|
||||
|
||||
function handleDragEnd(event: DragEndEvent) {
|
||||
const { active, over } = event
|
||||
if (active && over && active.id !== over.id) {
|
||||
setData((data) => {
|
||||
const oldIndex = dataIds.indexOf(active.id)
|
||||
const newIndex = dataIds.indexOf(over.id)
|
||||
return arrayMove(data, oldIndex, newIndex)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Tabs
|
||||
defaultValue="outline"
|
||||
className="w-full flex-col justify-start gap-6"
|
||||
>
|
||||
<div className="flex items-center justify-between px-4 lg:px-6">
|
||||
<Label htmlFor="view-selector" className="sr-only">
|
||||
View
|
||||
</Label>
|
||||
<Select defaultValue="outline">
|
||||
<SelectTrigger
|
||||
className="flex w-fit @4xl/main:hidden"
|
||||
size="sm"
|
||||
id="view-selector"
|
||||
>
|
||||
<SelectValue placeholder="Select a view" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="outline">Outline</SelectItem>
|
||||
<SelectItem value="past-performance">Past Performance</SelectItem>
|
||||
<SelectItem value="key-personnel">Key Personnel</SelectItem>
|
||||
<SelectItem value="focus-documents">Focus Documents</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<TabsList className="**:data-[slot=badge]:bg-muted-foreground/30 hidden **:data-[slot=badge]:size-5 **:data-[slot=badge]:rounded-full **:data-[slot=badge]:px-1 @4xl/main:flex">
|
||||
<TabsTrigger value="outline">Outline</TabsTrigger>
|
||||
<TabsTrigger value="past-performance">
|
||||
Past Performance <Badge variant="secondary">3</Badge>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="key-personnel">
|
||||
Key Personnel <Badge variant="secondary">2</Badge>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="focus-documents">Focus Documents</TabsTrigger>
|
||||
</TabsList>
|
||||
<div className="flex items-center gap-2">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" size="sm">
|
||||
<IconLayoutColumns />
|
||||
<span className="hidden lg:inline">Customize Columns</span>
|
||||
<span className="lg:hidden">Columns</span>
|
||||
<IconChevronDown />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-56">
|
||||
{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>
|
||||
<Button variant="outline" size="sm">
|
||||
<IconPlus />
|
||||
<span className="hidden lg:inline">Add Section</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<TabsContent
|
||||
value="outline"
|
||||
className="relative flex flex-col gap-4 overflow-auto px-4 lg:px-6"
|
||||
>
|
||||
<div className="overflow-hidden rounded-lg border">
|
||||
<DndContext
|
||||
collisionDetection={closestCenter}
|
||||
modifiers={[restrictToVerticalAxis]}
|
||||
onDragEnd={handleDragEnd}
|
||||
sensors={sensors}
|
||||
id={sortableId}
|
||||
>
|
||||
<Table>
|
||||
<TableHeader className="bg-muted sticky top-0 z-10">
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<TableHead key={header.id} colSpan={header.colSpan}>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)}
|
||||
</TableHead>
|
||||
)
|
||||
})}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody className="**:data-[slot=table-cell]:first:w-8">
|
||||
{table.getRowModel().rows?.length ? (
|
||||
<SortableContext
|
||||
items={dataIds}
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
{table.getRowModel().rows.map((row) => (
|
||||
<DraggableRow key={row.id} row={row} />
|
||||
))}
|
||||
</SortableContext>
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={columns.length}
|
||||
className="h-24 text-center"
|
||||
>
|
||||
No results.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</DndContext>
|
||||
</div>
|
||||
<div className="flex items-center justify-between px-4">
|
||||
<div className="text-muted-foreground hidden flex-1 text-sm lg:flex">
|
||||
{table.getFilteredSelectedRowModel().rows.length} of{" "}
|
||||
{table.getFilteredRowModel().rows.length} row(s) selected.
|
||||
</div>
|
||||
<div className="flex w-full items-center gap-8 lg:w-fit">
|
||||
<div className="hidden items-center gap-2 lg:flex">
|
||||
<Label htmlFor="rows-per-page" className="text-sm font-medium">
|
||||
Rows per page
|
||||
</Label>
|
||||
<Select
|
||||
value={`${table.getState().pagination.pageSize}`}
|
||||
onValueChange={(value) => {
|
||||
table.setPageSize(Number(value))
|
||||
}}
|
||||
>
|
||||
<SelectTrigger size="sm" className="w-20" id="rows-per-page">
|
||||
<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>
|
||||
</div>
|
||||
<div className="flex w-fit items-center justify-center text-sm font-medium">
|
||||
Page {table.getState().pagination.pageIndex + 1} of{" "}
|
||||
{table.getPageCount()}
|
||||
</div>
|
||||
<div className="ml-auto flex items-center gap-2 lg:ml-0">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="hidden h-8 w-8 p-0 lg:flex"
|
||||
onClick={() => table.setPageIndex(0)}
|
||||
disabled={!table.getCanPreviousPage()}
|
||||
>
|
||||
<span className="sr-only">Go to first page</span>
|
||||
<IconChevronsLeft />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="size-8"
|
||||
size="icon"
|
||||
onClick={() => table.previousPage()}
|
||||
disabled={!table.getCanPreviousPage()}
|
||||
>
|
||||
<span className="sr-only">Go to previous page</span>
|
||||
<IconChevronLeft />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="size-8"
|
||||
size="icon"
|
||||
onClick={() => table.nextPage()}
|
||||
disabled={!table.getCanNextPage()}
|
||||
>
|
||||
<span className="sr-only">Go to next page</span>
|
||||
<IconChevronRight />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="hidden size-8 lg:flex"
|
||||
size="icon"
|
||||
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
|
||||
disabled={!table.getCanNextPage()}
|
||||
>
|
||||
<span className="sr-only">Go to last page</span>
|
||||
<IconChevronsRight />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent
|
||||
value="past-performance"
|
||||
className="flex flex-col px-4 lg:px-6"
|
||||
>
|
||||
<div className="aspect-video w-full flex-1 rounded-lg border border-dashed"></div>
|
||||
</TabsContent>
|
||||
<TabsContent value="key-personnel" className="flex flex-col px-4 lg:px-6">
|
||||
<div className="aspect-video w-full flex-1 rounded-lg border border-dashed"></div>
|
||||
</TabsContent>
|
||||
<TabsContent
|
||||
value="focus-documents"
|
||||
className="flex flex-col px-4 lg:px-6"
|
||||
>
|
||||
<div className="aspect-video w-full flex-1 rounded-lg border border-dashed"></div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
)
|
||||
}
|
||||
|
||||
const chartData = [
|
||||
{ month: "January", desktop: 186, mobile: 80 },
|
||||
{ month: "February", desktop: 305, mobile: 200 },
|
||||
{ month: "March", desktop: 237, mobile: 120 },
|
||||
{ month: "April", desktop: 73, mobile: 190 },
|
||||
{ month: "May", desktop: 209, mobile: 130 },
|
||||
{ month: "June", desktop: 214, mobile: 140 },
|
||||
]
|
||||
|
||||
const chartConfig = {
|
||||
desktop: {
|
||||
label: "Desktop",
|
||||
color: "var(--primary)",
|
||||
},
|
||||
mobile: {
|
||||
label: "Mobile",
|
||||
color: "var(--primary)",
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
|
||||
function TableCellViewer({ item }: { item: z.infer<typeof schema> }) {
|
||||
const isMobile = useIsMobile()
|
||||
|
||||
return (
|
||||
<Drawer direction={isMobile ? "bottom" : "right"}>
|
||||
<DrawerTrigger asChild>
|
||||
<Button variant="link" className="text-foreground w-fit px-0 text-left">
|
||||
{item.header}
|
||||
</Button>
|
||||
</DrawerTrigger>
|
||||
<DrawerContent>
|
||||
<DrawerHeader className="gap-1">
|
||||
<DrawerTitle>{item.header}</DrawerTitle>
|
||||
<DrawerDescription>
|
||||
Showing total visitors for the last 6 months
|
||||
</DrawerDescription>
|
||||
</DrawerHeader>
|
||||
<div className="flex flex-col gap-4 overflow-y-auto px-4 text-sm">
|
||||
{!isMobile && (
|
||||
<>
|
||||
<ChartContainer config={chartConfig}>
|
||||
<AreaChart
|
||||
accessibilityLayer
|
||||
data={chartData}
|
||||
margin={{
|
||||
left: 0,
|
||||
right: 10,
|
||||
}}
|
||||
>
|
||||
<CartesianGrid vertical={false} />
|
||||
<XAxis
|
||||
dataKey="month"
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={8}
|
||||
tickFormatter={(value) => value.slice(0, 3)}
|
||||
hide
|
||||
/>
|
||||
<ChartTooltip
|
||||
cursor={false}
|
||||
content={<ChartTooltipContent indicator="dot" />}
|
||||
/>
|
||||
<Area
|
||||
dataKey="mobile"
|
||||
type="natural"
|
||||
fill="var(--color-mobile)"
|
||||
fillOpacity={0.6}
|
||||
stroke="var(--color-mobile)"
|
||||
stackId="a"
|
||||
/>
|
||||
<Area
|
||||
dataKey="desktop"
|
||||
type="natural"
|
||||
fill="var(--color-desktop)"
|
||||
fillOpacity={0.4}
|
||||
stroke="var(--color-desktop)"
|
||||
stackId="a"
|
||||
/>
|
||||
</AreaChart>
|
||||
</ChartContainer>
|
||||
<Separator />
|
||||
<div className="grid gap-2">
|
||||
<div className="flex gap-2 leading-none font-medium">
|
||||
Trending up by 5.2% this month{" "}
|
||||
<IconTrendingUp className="size-4" />
|
||||
</div>
|
||||
<div className="text-muted-foreground">
|
||||
Showing total visitors for the last 6 months. This is just
|
||||
some random text to test the layout. It spans multiple lines
|
||||
and should wrap around.
|
||||
</div>
|
||||
</div>
|
||||
<Separator />
|
||||
</>
|
||||
)}
|
||||
<form className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-3">
|
||||
<Label htmlFor="header">Header</Label>
|
||||
<Input id="header" defaultValue={item.header} />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="flex flex-col gap-3">
|
||||
<Label htmlFor="type">Type</Label>
|
||||
<Select defaultValue={item.type}>
|
||||
<SelectTrigger id="type" className="w-full">
|
||||
<SelectValue placeholder="Select a type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="Table of Contents">
|
||||
Table of Contents
|
||||
</SelectItem>
|
||||
<SelectItem value="Executive Summary">
|
||||
Executive Summary
|
||||
</SelectItem>
|
||||
<SelectItem value="Technical Approach">
|
||||
Technical Approach
|
||||
</SelectItem>
|
||||
<SelectItem value="Design">Design</SelectItem>
|
||||
<SelectItem value="Capabilities">Capabilities</SelectItem>
|
||||
<SelectItem value="Focus Documents">
|
||||
Focus Documents
|
||||
</SelectItem>
|
||||
<SelectItem value="Narrative">Narrative</SelectItem>
|
||||
<SelectItem value="Cover Page">Cover Page</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<Label htmlFor="status">Status</Label>
|
||||
<Select defaultValue={item.status}>
|
||||
<SelectTrigger id="status" className="w-full">
|
||||
<SelectValue placeholder="Select a status" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="Done">Done</SelectItem>
|
||||
<SelectItem value="In Progress">In Progress</SelectItem>
|
||||
<SelectItem value="Not Started">Not Started</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="flex flex-col gap-3">
|
||||
<Label htmlFor="target">Target</Label>
|
||||
<Input id="target" defaultValue={item.target} />
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<Label htmlFor="limit">Limit</Label>
|
||||
<Input id="limit" defaultValue={item.limit} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<Label htmlFor="reviewer">Reviewer</Label>
|
||||
<Select defaultValue={item.reviewer}>
|
||||
<SelectTrigger id="reviewer" className="w-full">
|
||||
<SelectValue placeholder="Select a reviewer" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="Eddie Lake">Eddie Lake</SelectItem>
|
||||
<SelectItem value="Jamik Tashpulatov">
|
||||
Jamik Tashpulatov
|
||||
</SelectItem>
|
||||
<SelectItem value="Emily Whalen">Emily Whalen</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<DrawerFooter>
|
||||
<Button>Submit</Button>
|
||||
<DrawerClose asChild>
|
||||
<Button variant="outline">Done</Button>
|
||||
</DrawerClose>
|
||||
</DrawerFooter>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
)
|
||||
}
|
||||
27
apps/v4/app/(examples)/dashboard/components/mode-toggle.tsx
Normal file
27
apps/v4/app/(examples)/dashboard/components/mode-toggle.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { IconBrightness } from "@tabler/icons-react"
|
||||
import { useTheme } from "next-themes"
|
||||
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
|
||||
export function ModeToggle() {
|
||||
const { setTheme, resolvedTheme } = useTheme()
|
||||
|
||||
const toggleTheme = React.useCallback(() => {
|
||||
setTheme(resolvedTheme === "dark" ? "light" : "dark")
|
||||
}, [resolvedTheme, setTheme])
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="icon"
|
||||
className="group/toggle size-8"
|
||||
onClick={toggleTheme}
|
||||
>
|
||||
<IconBrightness />
|
||||
<span className="sr-only">Toggle theme</span>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
"use client"
|
||||
|
||||
import {
|
||||
IconDots,
|
||||
IconFolder,
|
||||
IconShare3,
|
||||
IconTrash,
|
||||
type Icon,
|
||||
} from "@tabler/icons-react"
|
||||
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/registry/new-york-v4/ui/dropdown-menu"
|
||||
import {
|
||||
SidebarGroup,
|
||||
SidebarGroupLabel,
|
||||
SidebarMenu,
|
||||
SidebarMenuAction,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
useSidebar,
|
||||
} from "@/registry/new-york-v4/ui/sidebar"
|
||||
|
||||
export function NavDocuments({
|
||||
items,
|
||||
}: {
|
||||
items: {
|
||||
name: string
|
||||
url: string
|
||||
icon: Icon
|
||||
}[]
|
||||
}) {
|
||||
const { isMobile } = useSidebar()
|
||||
|
||||
return (
|
||||
<SidebarGroup className="group-data-[collapsible=icon]:hidden">
|
||||
<SidebarGroupLabel>Documents</SidebarGroupLabel>
|
||||
<SidebarMenu>
|
||||
{items.map((item) => (
|
||||
<SidebarMenuItem key={item.name}>
|
||||
<SidebarMenuButton asChild>
|
||||
<a href={item.url}>
|
||||
<item.icon />
|
||||
<span>{item.name}</span>
|
||||
</a>
|
||||
</SidebarMenuButton>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<SidebarMenuAction
|
||||
showOnHover
|
||||
className="data-[state=open]:bg-accent rounded-sm"
|
||||
>
|
||||
<IconDots />
|
||||
<span className="sr-only">More</span>
|
||||
</SidebarMenuAction>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
className="w-24 rounded-lg"
|
||||
side={isMobile ? "bottom" : "right"}
|
||||
align={isMobile ? "end" : "start"}
|
||||
>
|
||||
<DropdownMenuItem>
|
||||
<IconFolder />
|
||||
<span>Open</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<IconShare3 />
|
||||
<span>Share</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem variant="destructive">
|
||||
<IconTrash />
|
||||
<span>Delete</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</SidebarMenuItem>
|
||||
))}
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton className="text-sidebar-foreground/70">
|
||||
<IconDots className="text-sidebar-foreground/70" />
|
||||
<span>More</span>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarGroup>
|
||||
)
|
||||
}
|
||||
58
apps/v4/app/(examples)/dashboard/components/nav-main.tsx
Normal file
58
apps/v4/app/(examples)/dashboard/components/nav-main.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
"use client"
|
||||
|
||||
import { IconCirclePlusFilled, IconMail, type Icon } from "@tabler/icons-react"
|
||||
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import {
|
||||
SidebarGroup,
|
||||
SidebarGroupContent,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
} from "@/registry/new-york-v4/ui/sidebar"
|
||||
|
||||
export function NavMain({
|
||||
items,
|
||||
}: {
|
||||
items: {
|
||||
title: string
|
||||
url: string
|
||||
icon?: Icon
|
||||
}[]
|
||||
}) {
|
||||
return (
|
||||
<SidebarGroup>
|
||||
<SidebarGroupContent className="flex flex-col gap-2">
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem className="flex items-center gap-2">
|
||||
<SidebarMenuButton
|
||||
tooltip="Quick Create"
|
||||
className="bg-primary text-primary-foreground hover:bg-primary/90 hover:text-primary-foreground active:bg-primary/90 active:text-primary-foreground min-w-8 duration-200 ease-linear"
|
||||
>
|
||||
<IconCirclePlusFilled />
|
||||
<span>Quick Create</span>
|
||||
</SidebarMenuButton>
|
||||
<Button
|
||||
size="icon"
|
||||
className="size-8 group-data-[collapsible=icon]:opacity-0"
|
||||
variant="outline"
|
||||
>
|
||||
<IconMail />
|
||||
<span className="sr-only">Inbox</span>
|
||||
</Button>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
<SidebarMenu>
|
||||
{items.map((item) => (
|
||||
<SidebarMenuItem key={item.title}>
|
||||
<SidebarMenuButton tooltip={item.title}>
|
||||
{item.icon && <item.icon />}
|
||||
<span>{item.title}</span>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
))}
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { IconBrightness, type Icon } from "@tabler/icons-react"
|
||||
import { useTheme } from "next-themes"
|
||||
|
||||
import {
|
||||
SidebarGroup,
|
||||
SidebarGroupContent,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
} from "@/registry/new-york-v4/ui/sidebar"
|
||||
import { Skeleton } from "@/registry/new-york-v4/ui/skeleton"
|
||||
import { Switch } from "@/registry/new-york-v4/ui/switch"
|
||||
|
||||
export function NavSecondary({
|
||||
items,
|
||||
...props
|
||||
}: {
|
||||
items: {
|
||||
title: string
|
||||
url: string
|
||||
icon: Icon
|
||||
}[]
|
||||
} & React.ComponentPropsWithoutRef<typeof SidebarGroup>) {
|
||||
const { resolvedTheme, setTheme } = useTheme()
|
||||
const [mounted, setMounted] = React.useState(false)
|
||||
|
||||
React.useEffect(() => {
|
||||
setMounted(true)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<SidebarGroup {...props}>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
{items.map((item) => (
|
||||
<SidebarMenuItem key={item.title}>
|
||||
<SidebarMenuButton asChild>
|
||||
<a href={item.url}>
|
||||
<item.icon />
|
||||
<span>{item.title}</span>
|
||||
</a>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
))}
|
||||
<SidebarMenuItem className="group-data-[collapsible=icon]:hidden">
|
||||
<SidebarMenuButton asChild>
|
||||
<label>
|
||||
<IconBrightness />
|
||||
<span>Dark Mode</span>
|
||||
{mounted ? (
|
||||
<Switch
|
||||
className="ml-auto"
|
||||
checked={resolvedTheme !== "light"}
|
||||
onCheckedChange={() =>
|
||||
setTheme(resolvedTheme === "dark" ? "light" : "dark")
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<Skeleton className="ml-auto h-4 w-8 rounded-full" />
|
||||
)}
|
||||
</label>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
)
|
||||
}
|
||||
110
apps/v4/app/(examples)/dashboard/components/nav-user.tsx
Normal file
110
apps/v4/app/(examples)/dashboard/components/nav-user.tsx
Normal file
@@ -0,0 +1,110 @@
|
||||
"use client"
|
||||
|
||||
import {
|
||||
IconCreditCard,
|
||||
IconDotsVertical,
|
||||
IconLogout,
|
||||
IconNotification,
|
||||
IconUserCircle,
|
||||
} from "@tabler/icons-react"
|
||||
|
||||
import {
|
||||
Avatar,
|
||||
AvatarFallback,
|
||||
AvatarImage,
|
||||
} from "@/registry/new-york-v4/ui/avatar"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/registry/new-york-v4/ui/dropdown-menu"
|
||||
import {
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
useSidebar,
|
||||
} from "@/registry/new-york-v4/ui/sidebar"
|
||||
|
||||
export function NavUser({
|
||||
user,
|
||||
}: {
|
||||
user: {
|
||||
name: string
|
||||
email: string
|
||||
avatar: string
|
||||
}
|
||||
}) {
|
||||
const { isMobile } = useSidebar()
|
||||
|
||||
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 grayscale">
|
||||
<AvatarImage src={user.avatar} alt={user.name} />
|
||||
<AvatarFallback className="rounded-lg">CN</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||
<span className="truncate font-medium">{user.name}</span>
|
||||
<span className="text-muted-foreground truncate text-xs">
|
||||
{user.email}
|
||||
</span>
|
||||
</div>
|
||||
<IconDotsVertical className="ml-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-left text-sm">
|
||||
<Avatar className="h-8 w-8 rounded-lg">
|
||||
<AvatarImage src={user.avatar} alt={user.name} />
|
||||
<AvatarFallback className="rounded-lg">CN</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||
<span className="truncate font-medium">{user.name}</span>
|
||||
<span className="text-muted-foreground truncate text-xs">
|
||||
{user.email}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>
|
||||
<IconUserCircle />
|
||||
Account
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<IconCreditCard />
|
||||
Billing
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<IconNotification />
|
||||
Notifications
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>
|
||||
<IconLogout />
|
||||
Log out
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
)
|
||||
}
|
||||
102
apps/v4/app/(examples)/dashboard/components/section-cards.tsx
Normal file
102
apps/v4/app/(examples)/dashboard/components/section-cards.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
import { IconTrendingDown, IconTrendingUp } from "@tabler/icons-react"
|
||||
|
||||
import { Badge } from "@/registry/new-york-v4/ui/badge"
|
||||
import {
|
||||
Card,
|
||||
CardAction,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/new-york-v4/ui/card"
|
||||
|
||||
export function SectionCards() {
|
||||
return (
|
||||
<div className="*:data-[slot=card]:from-primary/5 *:data-[slot=card]:to-card dark:*:data-[slot=card]:bg-card grid grid-cols-1 gap-4 px-4 *:data-[slot=card]:bg-gradient-to-t *:data-[slot=card]:shadow-xs lg:px-6 @xl/main:grid-cols-2 @5xl/main:grid-cols-4">
|
||||
<Card className="@container/card">
|
||||
<CardHeader>
|
||||
<CardDescription>Total Revenue</CardDescription>
|
||||
<CardTitle className="text-2xl font-semibold tabular-nums @[250px]/card:text-3xl">
|
||||
$1,250.00
|
||||
</CardTitle>
|
||||
<CardAction>
|
||||
<Badge variant="outline">
|
||||
<IconTrendingUp />
|
||||
+12.5%
|
||||
</Badge>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<CardFooter className="flex-col items-start gap-1.5 text-sm">
|
||||
<div className="line-clamp-1 flex gap-2 font-medium">
|
||||
Trending up this month <IconTrendingUp className="size-4" />
|
||||
</div>
|
||||
<div className="text-muted-foreground">
|
||||
Visitors for the last 6 months
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
<Card className="@container/card">
|
||||
<CardHeader>
|
||||
<CardDescription>New Customers</CardDescription>
|
||||
<CardTitle className="text-2xl font-semibold tabular-nums @[250px]/card:text-3xl">
|
||||
1,234
|
||||
</CardTitle>
|
||||
<CardAction>
|
||||
<Badge variant="outline">
|
||||
<IconTrendingDown />
|
||||
-20%
|
||||
</Badge>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<CardFooter className="flex-col items-start gap-1.5 text-sm">
|
||||
<div className="line-clamp-1 flex gap-2 font-medium">
|
||||
Down 20% this period <IconTrendingDown className="size-4" />
|
||||
</div>
|
||||
<div className="text-muted-foreground">
|
||||
Acquisition needs attention
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
<Card className="@container/card">
|
||||
<CardHeader>
|
||||
<CardDescription>Active Accounts</CardDescription>
|
||||
<CardTitle className="text-2xl font-semibold tabular-nums @[250px]/card:text-3xl">
|
||||
45,678
|
||||
</CardTitle>
|
||||
<CardAction>
|
||||
<Badge variant="outline">
|
||||
<IconTrendingUp />
|
||||
+12.5%
|
||||
</Badge>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<CardFooter className="flex-col items-start gap-1.5 text-sm">
|
||||
<div className="line-clamp-1 flex gap-2 font-medium">
|
||||
Strong user retention <IconTrendingUp className="size-4" />
|
||||
</div>
|
||||
<div className="text-muted-foreground">Engagement exceed targets</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
<Card className="@container/card">
|
||||
<CardHeader>
|
||||
<CardDescription>Growth Rate</CardDescription>
|
||||
<CardTitle className="text-2xl font-semibold tabular-nums @[250px]/card:text-3xl">
|
||||
4.5%
|
||||
</CardTitle>
|
||||
<CardAction>
|
||||
<Badge variant="outline">
|
||||
<IconTrendingUp />
|
||||
+4.5%
|
||||
</Badge>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<CardFooter className="flex-col items-start gap-1.5 text-sm">
|
||||
<div className="line-clamp-1 flex gap-2 font-medium">
|
||||
Steady performance increase <IconTrendingUp className="size-4" />
|
||||
</div>
|
||||
<div className="text-muted-foreground">Meets growth projections</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
34
apps/v4/app/(examples)/dashboard/components/site-header.tsx
Normal file
34
apps/v4/app/(examples)/dashboard/components/site-header.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import { Separator } from "@/registry/new-york-v4/ui/separator"
|
||||
import { SidebarTrigger } from "@/registry/new-york-v4/ui/sidebar"
|
||||
import { ModeToggle } from "@/app/(examples)/dashboard/components/mode-toggle"
|
||||
import { ThemeSelector } from "@/app/(examples)/dashboard/components/theme-selector"
|
||||
|
||||
export function SiteHeader() {
|
||||
return (
|
||||
<header className="flex h-(--header-height) shrink-0 items-center gap-2 border-b transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-(--header-height)">
|
||||
<div className="flex w-full items-center gap-1 px-4 lg:gap-2 lg:px-6">
|
||||
<SidebarTrigger className="-ml-1" />
|
||||
<Separator
|
||||
orientation="vertical"
|
||||
className="mx-2 data-[orientation=vertical]:h-4"
|
||||
/>
|
||||
<h1 className="text-base font-medium">Documents</h1>
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
<Button variant="ghost" asChild size="sm" className="hidden sm:flex">
|
||||
<a
|
||||
href="https://github.com/shadcn-ui/ui/tree/main/apps/v4/app/(examples)/dashboard"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
className="dark:text-foreground"
|
||||
>
|
||||
GitHub
|
||||
</a>
|
||||
</Button>
|
||||
<ThemeSelector />
|
||||
<ModeToggle />
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
103
apps/v4/app/(examples)/dashboard/components/theme-selector.tsx
Normal file
103
apps/v4/app/(examples)/dashboard/components/theme-selector.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
"use client"
|
||||
|
||||
import { useThemeConfig } from "@/components/active-theme"
|
||||
import { Label } from "@/registry/new-york-v4/ui/label"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectSeparator,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/registry/new-york-v4/ui/select"
|
||||
|
||||
const DEFAULT_THEMES = [
|
||||
{
|
||||
name: "Default",
|
||||
value: "default",
|
||||
},
|
||||
{
|
||||
name: "Blue",
|
||||
value: "blue",
|
||||
},
|
||||
{
|
||||
name: "Green",
|
||||
value: "green",
|
||||
},
|
||||
{
|
||||
name: "Amber",
|
||||
value: "amber",
|
||||
},
|
||||
]
|
||||
|
||||
const SCALED_THEMES = [
|
||||
{
|
||||
name: "Default",
|
||||
value: "default-scaled",
|
||||
},
|
||||
{
|
||||
name: "Blue",
|
||||
value: "blue-scaled",
|
||||
},
|
||||
]
|
||||
|
||||
const MONO_THEMES = [
|
||||
{
|
||||
name: "Mono",
|
||||
value: "mono-scaled",
|
||||
},
|
||||
]
|
||||
|
||||
export function ThemeSelector() {
|
||||
const { activeTheme, setActiveTheme } = useThemeConfig()
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<Label htmlFor="theme-selector" className="sr-only">
|
||||
Theme
|
||||
</Label>
|
||||
<Select value={activeTheme} onValueChange={setActiveTheme}>
|
||||
<SelectTrigger
|
||||
id="theme-selector"
|
||||
size="sm"
|
||||
className="justify-start *:data-[slot=select-value]:w-12"
|
||||
>
|
||||
<span className="text-muted-foreground hidden sm:block">
|
||||
Select a theme:
|
||||
</span>
|
||||
<span className="text-muted-foreground block sm:hidden">Theme</span>
|
||||
<SelectValue placeholder="Select a theme" />
|
||||
</SelectTrigger>
|
||||
<SelectContent align="end">
|
||||
<SelectGroup>
|
||||
<SelectLabel>Default</SelectLabel>
|
||||
{DEFAULT_THEMES.map((theme) => (
|
||||
<SelectItem key={theme.name} value={theme.value}>
|
||||
{theme.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
<SelectSeparator />
|
||||
<SelectGroup>
|
||||
<SelectLabel>Scaled</SelectLabel>
|
||||
{SCALED_THEMES.map((theme) => (
|
||||
<SelectItem key={theme.name} value={theme.value}>
|
||||
{theme.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
<SelectGroup>
|
||||
<SelectLabel>Monospaced</SelectLabel>
|
||||
{MONO_THEMES.map((theme) => (
|
||||
<SelectItem key={theme.name} value={theme.value}>
|
||||
{theme.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
614
apps/v4/app/(examples)/dashboard/data.json
Normal file
614
apps/v4/app/(examples)/dashboard/data.json
Normal file
@@ -0,0 +1,614 @@
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"header": "Cover page",
|
||||
"type": "Cover page",
|
||||
"status": "In Process",
|
||||
"target": "18",
|
||||
"limit": "5",
|
||||
"reviewer": "Eddie Lake"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"header": "Table of contents",
|
||||
"type": "Table of contents",
|
||||
"status": "Done",
|
||||
"target": "29",
|
||||
"limit": "24",
|
||||
"reviewer": "Eddie Lake"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"header": "Executive summary",
|
||||
"type": "Narrative",
|
||||
"status": "Done",
|
||||
"target": "10",
|
||||
"limit": "13",
|
||||
"reviewer": "Eddie Lake"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"header": "Technical approach",
|
||||
"type": "Narrative",
|
||||
"status": "Done",
|
||||
"target": "27",
|
||||
"limit": "23",
|
||||
"reviewer": "Jamik Tashpulatov"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"header": "Design",
|
||||
"type": "Narrative",
|
||||
"status": "In Process",
|
||||
"target": "2",
|
||||
"limit": "16",
|
||||
"reviewer": "Jamik Tashpulatov"
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"header": "Capabilities",
|
||||
"type": "Narrative",
|
||||
"status": "In Process",
|
||||
"target": "20",
|
||||
"limit": "8",
|
||||
"reviewer": "Jamik Tashpulatov"
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"header": "Integration with existing systems",
|
||||
"type": "Narrative",
|
||||
"status": "In Process",
|
||||
"target": "19",
|
||||
"limit": "21",
|
||||
"reviewer": "Jamik Tashpulatov"
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"header": "Innovation and Advantages",
|
||||
"type": "Narrative",
|
||||
"status": "Done",
|
||||
"target": "25",
|
||||
"limit": "26",
|
||||
"reviewer": "Assign reviewer"
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"header": "Overview of EMR's Innovative Solutions",
|
||||
"type": "Technical content",
|
||||
"status": "Done",
|
||||
"target": "7",
|
||||
"limit": "23",
|
||||
"reviewer": "Assign reviewer"
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"header": "Advanced Algorithms and Machine Learning",
|
||||
"type": "Narrative",
|
||||
"status": "Done",
|
||||
"target": "30",
|
||||
"limit": "28",
|
||||
"reviewer": "Assign reviewer"
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"header": "Adaptive Communication Protocols",
|
||||
"type": "Narrative",
|
||||
"status": "Done",
|
||||
"target": "9",
|
||||
"limit": "31",
|
||||
"reviewer": "Assign reviewer"
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"header": "Advantages Over Current Technologies",
|
||||
"type": "Narrative",
|
||||
"status": "Done",
|
||||
"target": "12",
|
||||
"limit": "0",
|
||||
"reviewer": "Assign reviewer"
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"header": "Past Performance",
|
||||
"type": "Narrative",
|
||||
"status": "Done",
|
||||
"target": "22",
|
||||
"limit": "33",
|
||||
"reviewer": "Assign reviewer"
|
||||
},
|
||||
{
|
||||
"id": 14,
|
||||
"header": "Customer Feedback and Satisfaction Levels",
|
||||
"type": "Narrative",
|
||||
"status": "Done",
|
||||
"target": "15",
|
||||
"limit": "34",
|
||||
"reviewer": "Assign reviewer"
|
||||
},
|
||||
{
|
||||
"id": 15,
|
||||
"header": "Implementation Challenges and Solutions",
|
||||
"type": "Narrative",
|
||||
"status": "Done",
|
||||
"target": "3",
|
||||
"limit": "35",
|
||||
"reviewer": "Assign reviewer"
|
||||
},
|
||||
{
|
||||
"id": 16,
|
||||
"header": "Security Measures and Data Protection Policies",
|
||||
"type": "Narrative",
|
||||
"status": "In Process",
|
||||
"target": "6",
|
||||
"limit": "36",
|
||||
"reviewer": "Assign reviewer"
|
||||
},
|
||||
{
|
||||
"id": 17,
|
||||
"header": "Scalability and Future Proofing",
|
||||
"type": "Narrative",
|
||||
"status": "Done",
|
||||
"target": "4",
|
||||
"limit": "37",
|
||||
"reviewer": "Assign reviewer"
|
||||
},
|
||||
{
|
||||
"id": 18,
|
||||
"header": "Cost-Benefit Analysis",
|
||||
"type": "Plain language",
|
||||
"status": "Done",
|
||||
"target": "14",
|
||||
"limit": "38",
|
||||
"reviewer": "Assign reviewer"
|
||||
},
|
||||
{
|
||||
"id": 19,
|
||||
"header": "User Training and Onboarding Experience",
|
||||
"type": "Narrative",
|
||||
"status": "Done",
|
||||
"target": "17",
|
||||
"limit": "39",
|
||||
"reviewer": "Assign reviewer"
|
||||
},
|
||||
{
|
||||
"id": 20,
|
||||
"header": "Future Development Roadmap",
|
||||
"type": "Narrative",
|
||||
"status": "Done",
|
||||
"target": "11",
|
||||
"limit": "40",
|
||||
"reviewer": "Assign reviewer"
|
||||
},
|
||||
{
|
||||
"id": 21,
|
||||
"header": "System Architecture Overview",
|
||||
"type": "Technical content",
|
||||
"status": "In Process",
|
||||
"target": "24",
|
||||
"limit": "18",
|
||||
"reviewer": "Maya Johnson"
|
||||
},
|
||||
{
|
||||
"id": 22,
|
||||
"header": "Risk Management Plan",
|
||||
"type": "Narrative",
|
||||
"status": "Done",
|
||||
"target": "15",
|
||||
"limit": "22",
|
||||
"reviewer": "Carlos Rodriguez"
|
||||
},
|
||||
{
|
||||
"id": 23,
|
||||
"header": "Compliance Documentation",
|
||||
"type": "Legal",
|
||||
"status": "In Process",
|
||||
"target": "31",
|
||||
"limit": "27",
|
||||
"reviewer": "Sarah Chen"
|
||||
},
|
||||
{
|
||||
"id": 24,
|
||||
"header": "API Documentation",
|
||||
"type": "Technical content",
|
||||
"status": "Done",
|
||||
"target": "8",
|
||||
"limit": "12",
|
||||
"reviewer": "Raj Patel"
|
||||
},
|
||||
{
|
||||
"id": 25,
|
||||
"header": "User Interface Mockups",
|
||||
"type": "Visual",
|
||||
"status": "In Process",
|
||||
"target": "19",
|
||||
"limit": "25",
|
||||
"reviewer": "Leila Ahmadi"
|
||||
},
|
||||
{
|
||||
"id": 26,
|
||||
"header": "Database Schema",
|
||||
"type": "Technical content",
|
||||
"status": "Done",
|
||||
"target": "22",
|
||||
"limit": "20",
|
||||
"reviewer": "Thomas Wilson"
|
||||
},
|
||||
{
|
||||
"id": 27,
|
||||
"header": "Testing Methodology",
|
||||
"type": "Technical content",
|
||||
"status": "In Process",
|
||||
"target": "17",
|
||||
"limit": "14",
|
||||
"reviewer": "Assign reviewer"
|
||||
},
|
||||
{
|
||||
"id": 28,
|
||||
"header": "Deployment Strategy",
|
||||
"type": "Narrative",
|
||||
"status": "Done",
|
||||
"target": "26",
|
||||
"limit": "30",
|
||||
"reviewer": "Eddie Lake"
|
||||
},
|
||||
{
|
||||
"id": 29,
|
||||
"header": "Budget Breakdown",
|
||||
"type": "Financial",
|
||||
"status": "In Process",
|
||||
"target": "13",
|
||||
"limit": "16",
|
||||
"reviewer": "Jamik Tashpulatov"
|
||||
},
|
||||
{
|
||||
"id": 30,
|
||||
"header": "Market Analysis",
|
||||
"type": "Research",
|
||||
"status": "Done",
|
||||
"target": "29",
|
||||
"limit": "32",
|
||||
"reviewer": "Sophia Martinez"
|
||||
},
|
||||
{
|
||||
"id": 31,
|
||||
"header": "Competitor Comparison",
|
||||
"type": "Research",
|
||||
"status": "In Process",
|
||||
"target": "21",
|
||||
"limit": "19",
|
||||
"reviewer": "Assign reviewer"
|
||||
},
|
||||
{
|
||||
"id": 32,
|
||||
"header": "Maintenance Plan",
|
||||
"type": "Technical content",
|
||||
"status": "Done",
|
||||
"target": "16",
|
||||
"limit": "23",
|
||||
"reviewer": "Alex Thompson"
|
||||
},
|
||||
{
|
||||
"id": 33,
|
||||
"header": "User Personas",
|
||||
"type": "Research",
|
||||
"status": "In Process",
|
||||
"target": "27",
|
||||
"limit": "24",
|
||||
"reviewer": "Nina Patel"
|
||||
},
|
||||
{
|
||||
"id": 34,
|
||||
"header": "Accessibility Compliance",
|
||||
"type": "Legal",
|
||||
"status": "Done",
|
||||
"target": "18",
|
||||
"limit": "21",
|
||||
"reviewer": "Assign reviewer"
|
||||
},
|
||||
{
|
||||
"id": 35,
|
||||
"header": "Performance Metrics",
|
||||
"type": "Technical content",
|
||||
"status": "In Process",
|
||||
"target": "23",
|
||||
"limit": "26",
|
||||
"reviewer": "David Kim"
|
||||
},
|
||||
{
|
||||
"id": 36,
|
||||
"header": "Disaster Recovery Plan",
|
||||
"type": "Technical content",
|
||||
"status": "Done",
|
||||
"target": "14",
|
||||
"limit": "17",
|
||||
"reviewer": "Jamik Tashpulatov"
|
||||
},
|
||||
{
|
||||
"id": 37,
|
||||
"header": "Third-party Integrations",
|
||||
"type": "Technical content",
|
||||
"status": "In Process",
|
||||
"target": "25",
|
||||
"limit": "28",
|
||||
"reviewer": "Eddie Lake"
|
||||
},
|
||||
{
|
||||
"id": 38,
|
||||
"header": "User Feedback Summary",
|
||||
"type": "Research",
|
||||
"status": "Done",
|
||||
"target": "20",
|
||||
"limit": "15",
|
||||
"reviewer": "Assign reviewer"
|
||||
},
|
||||
{
|
||||
"id": 39,
|
||||
"header": "Localization Strategy",
|
||||
"type": "Narrative",
|
||||
"status": "In Process",
|
||||
"target": "12",
|
||||
"limit": "19",
|
||||
"reviewer": "Maria Garcia"
|
||||
},
|
||||
{
|
||||
"id": 40,
|
||||
"header": "Mobile Compatibility",
|
||||
"type": "Technical content",
|
||||
"status": "Done",
|
||||
"target": "28",
|
||||
"limit": "31",
|
||||
"reviewer": "James Wilson"
|
||||
},
|
||||
{
|
||||
"id": 41,
|
||||
"header": "Data Migration Plan",
|
||||
"type": "Technical content",
|
||||
"status": "In Process",
|
||||
"target": "19",
|
||||
"limit": "22",
|
||||
"reviewer": "Assign reviewer"
|
||||
},
|
||||
{
|
||||
"id": 42,
|
||||
"header": "Quality Assurance Protocols",
|
||||
"type": "Technical content",
|
||||
"status": "Done",
|
||||
"target": "30",
|
||||
"limit": "33",
|
||||
"reviewer": "Priya Singh"
|
||||
},
|
||||
{
|
||||
"id": 43,
|
||||
"header": "Stakeholder Analysis",
|
||||
"type": "Research",
|
||||
"status": "In Process",
|
||||
"target": "11",
|
||||
"limit": "14",
|
||||
"reviewer": "Eddie Lake"
|
||||
},
|
||||
{
|
||||
"id": 44,
|
||||
"header": "Environmental Impact Assessment",
|
||||
"type": "Research",
|
||||
"status": "Done",
|
||||
"target": "24",
|
||||
"limit": "27",
|
||||
"reviewer": "Assign reviewer"
|
||||
},
|
||||
{
|
||||
"id": 45,
|
||||
"header": "Intellectual Property Rights",
|
||||
"type": "Legal",
|
||||
"status": "In Process",
|
||||
"target": "17",
|
||||
"limit": "20",
|
||||
"reviewer": "Sarah Johnson"
|
||||
},
|
||||
{
|
||||
"id": 46,
|
||||
"header": "Customer Support Framework",
|
||||
"type": "Narrative",
|
||||
"status": "Done",
|
||||
"target": "22",
|
||||
"limit": "25",
|
||||
"reviewer": "Jamik Tashpulatov"
|
||||
},
|
||||
{
|
||||
"id": 47,
|
||||
"header": "Version Control Strategy",
|
||||
"type": "Technical content",
|
||||
"status": "In Process",
|
||||
"target": "15",
|
||||
"limit": "18",
|
||||
"reviewer": "Assign reviewer"
|
||||
},
|
||||
{
|
||||
"id": 48,
|
||||
"header": "Continuous Integration Pipeline",
|
||||
"type": "Technical content",
|
||||
"status": "Done",
|
||||
"target": "26",
|
||||
"limit": "29",
|
||||
"reviewer": "Michael Chen"
|
||||
},
|
||||
{
|
||||
"id": 49,
|
||||
"header": "Regulatory Compliance",
|
||||
"type": "Legal",
|
||||
"status": "In Process",
|
||||
"target": "13",
|
||||
"limit": "16",
|
||||
"reviewer": "Assign reviewer"
|
||||
},
|
||||
{
|
||||
"id": 50,
|
||||
"header": "User Authentication System",
|
||||
"type": "Technical content",
|
||||
"status": "Done",
|
||||
"target": "28",
|
||||
"limit": "31",
|
||||
"reviewer": "Eddie Lake"
|
||||
},
|
||||
{
|
||||
"id": 51,
|
||||
"header": "Data Analytics Framework",
|
||||
"type": "Technical content",
|
||||
"status": "In Process",
|
||||
"target": "21",
|
||||
"limit": "24",
|
||||
"reviewer": "Jamik Tashpulatov"
|
||||
},
|
||||
{
|
||||
"id": 52,
|
||||
"header": "Cloud Infrastructure",
|
||||
"type": "Technical content",
|
||||
"status": "Done",
|
||||
"target": "16",
|
||||
"limit": "19",
|
||||
"reviewer": "Assign reviewer"
|
||||
},
|
||||
{
|
||||
"id": 53,
|
||||
"header": "Network Security Measures",
|
||||
"type": "Technical content",
|
||||
"status": "In Process",
|
||||
"target": "29",
|
||||
"limit": "32",
|
||||
"reviewer": "Lisa Wong"
|
||||
},
|
||||
{
|
||||
"id": 54,
|
||||
"header": "Project Timeline",
|
||||
"type": "Planning",
|
||||
"status": "Done",
|
||||
"target": "14",
|
||||
"limit": "17",
|
||||
"reviewer": "Eddie Lake"
|
||||
},
|
||||
{
|
||||
"id": 55,
|
||||
"header": "Resource Allocation",
|
||||
"type": "Planning",
|
||||
"status": "In Process",
|
||||
"target": "27",
|
||||
"limit": "30",
|
||||
"reviewer": "Assign reviewer"
|
||||
},
|
||||
{
|
||||
"id": 56,
|
||||
"header": "Team Structure and Roles",
|
||||
"type": "Planning",
|
||||
"status": "Done",
|
||||
"target": "20",
|
||||
"limit": "23",
|
||||
"reviewer": "Jamik Tashpulatov"
|
||||
},
|
||||
{
|
||||
"id": 57,
|
||||
"header": "Communication Protocols",
|
||||
"type": "Planning",
|
||||
"status": "In Process",
|
||||
"target": "15",
|
||||
"limit": "18",
|
||||
"reviewer": "Assign reviewer"
|
||||
},
|
||||
{
|
||||
"id": 58,
|
||||
"header": "Success Metrics",
|
||||
"type": "Planning",
|
||||
"status": "Done",
|
||||
"target": "30",
|
||||
"limit": "33",
|
||||
"reviewer": "Eddie Lake"
|
||||
},
|
||||
{
|
||||
"id": 59,
|
||||
"header": "Internationalization Support",
|
||||
"type": "Technical content",
|
||||
"status": "In Process",
|
||||
"target": "23",
|
||||
"limit": "26",
|
||||
"reviewer": "Jamik Tashpulatov"
|
||||
},
|
||||
{
|
||||
"id": 60,
|
||||
"header": "Backup and Recovery Procedures",
|
||||
"type": "Technical content",
|
||||
"status": "Done",
|
||||
"target": "18",
|
||||
"limit": "21",
|
||||
"reviewer": "Assign reviewer"
|
||||
},
|
||||
{
|
||||
"id": 61,
|
||||
"header": "Monitoring and Alerting System",
|
||||
"type": "Technical content",
|
||||
"status": "In Process",
|
||||
"target": "25",
|
||||
"limit": "28",
|
||||
"reviewer": "Daniel Park"
|
||||
},
|
||||
{
|
||||
"id": 62,
|
||||
"header": "Code Review Guidelines",
|
||||
"type": "Technical content",
|
||||
"status": "Done",
|
||||
"target": "12",
|
||||
"limit": "15",
|
||||
"reviewer": "Eddie Lake"
|
||||
},
|
||||
{
|
||||
"id": 63,
|
||||
"header": "Documentation Standards",
|
||||
"type": "Technical content",
|
||||
"status": "In Process",
|
||||
"target": "27",
|
||||
"limit": "30",
|
||||
"reviewer": "Jamik Tashpulatov"
|
||||
},
|
||||
{
|
||||
"id": 64,
|
||||
"header": "Release Management Process",
|
||||
"type": "Planning",
|
||||
"status": "Done",
|
||||
"target": "22",
|
||||
"limit": "25",
|
||||
"reviewer": "Assign reviewer"
|
||||
},
|
||||
{
|
||||
"id": 65,
|
||||
"header": "Feature Prioritization Matrix",
|
||||
"type": "Planning",
|
||||
"status": "In Process",
|
||||
"target": "19",
|
||||
"limit": "22",
|
||||
"reviewer": "Emma Davis"
|
||||
},
|
||||
{
|
||||
"id": 66,
|
||||
"header": "Technical Debt Assessment",
|
||||
"type": "Technical content",
|
||||
"status": "Done",
|
||||
"target": "24",
|
||||
"limit": "27",
|
||||
"reviewer": "Eddie Lake"
|
||||
},
|
||||
{
|
||||
"id": 67,
|
||||
"header": "Capacity Planning",
|
||||
"type": "Planning",
|
||||
"status": "In Process",
|
||||
"target": "21",
|
||||
"limit": "24",
|
||||
"reviewer": "Jamik Tashpulatov"
|
||||
},
|
||||
{
|
||||
"id": 68,
|
||||
"header": "Service Level Agreements",
|
||||
"type": "Legal",
|
||||
"status": "Done",
|
||||
"target": "26",
|
||||
"limit": "29",
|
||||
"reviewer": "Assign reviewer"
|
||||
}
|
||||
]
|
||||
36
apps/v4/app/(examples)/dashboard/layout.tsx
Normal file
36
apps/v4/app/(examples)/dashboard/layout.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import { cookies } from "next/headers"
|
||||
|
||||
import {
|
||||
SidebarInset,
|
||||
SidebarProvider,
|
||||
} from "@/registry/new-york-v4/ui/sidebar"
|
||||
import { AppSidebar } from "@/app/(examples)/dashboard/components/app-sidebar"
|
||||
import { SiteHeader } from "@/app/(examples)/dashboard/components/site-header"
|
||||
|
||||
import "@/app/(examples)/dashboard/theme.css"
|
||||
|
||||
export default async function DashboardLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
const cookieStore = await cookies()
|
||||
const defaultOpen = cookieStore.get("sidebar_state")?.value === "true"
|
||||
|
||||
return (
|
||||
<SidebarProvider
|
||||
defaultOpen={defaultOpen}
|
||||
style={
|
||||
{
|
||||
"--sidebar-width": "calc(var(--spacing) * 72)",
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
<AppSidebar variant="inset" />
|
||||
<SidebarInset>
|
||||
<SiteHeader />
|
||||
<div className="flex flex-1 flex-col">{children}</div>
|
||||
</SidebarInset>
|
||||
</SidebarProvider>
|
||||
)
|
||||
}
|
||||
18
apps/v4/app/(examples)/dashboard/page.tsx
Normal file
18
apps/v4/app/(examples)/dashboard/page.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import { ChartAreaInteractive } from "@/app/(examples)/dashboard/components/chart-area-interactive"
|
||||
import { DataTable } from "@/app/(examples)/dashboard/components/data-table"
|
||||
import { SectionCards } from "@/app/(examples)/dashboard/components/section-cards"
|
||||
import data from "@/app/(examples)/dashboard/data.json"
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<div className="@container/main flex flex-1 flex-col gap-2">
|
||||
<div className="flex flex-col gap-4 py-4 md:gap-6 md:py-6">
|
||||
<SectionCards />
|
||||
<div className="px-4 lg:px-6">
|
||||
<ChartAreaInteractive />
|
||||
</div>
|
||||
<DataTable data={data} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
105
apps/v4/app/(examples)/dashboard/theme.css
Normal file
105
apps/v4/app/(examples)/dashboard/theme.css
Normal file
@@ -0,0 +1,105 @@
|
||||
body {
|
||||
@apply overscroll-none bg-transparent;
|
||||
}
|
||||
|
||||
:root {
|
||||
--font-sans: var(--font-inter);
|
||||
--header-height: calc(var(--spacing) * 12 + 1px);
|
||||
}
|
||||
|
||||
.theme-scaled {
|
||||
@media (min-width: 1024px) {
|
||||
--radius: 0.6rem;
|
||||
--text-lg: 1.05rem;
|
||||
--text-base: 0.85rem;
|
||||
--text-sm: 0.8rem;
|
||||
--spacing: 0.222222rem;
|
||||
}
|
||||
|
||||
[data-slot="card"] {
|
||||
--spacing: 0.16rem;
|
||||
}
|
||||
|
||||
[data-slot="select-trigger"],
|
||||
[data-slot="toggle-group-item"] {
|
||||
--spacing: 0.222222rem;
|
||||
}
|
||||
}
|
||||
|
||||
.theme-default,
|
||||
.theme-default-scaled {
|
||||
--primary: var(--color-neutral-600);
|
||||
--primary-foreground: var(--color-neutral-50);
|
||||
|
||||
@variant dark {
|
||||
--primary: var(--color-neutral-500);
|
||||
--primary-foreground: var(--color-neutral-50);
|
||||
}
|
||||
}
|
||||
|
||||
.theme-blue,
|
||||
.theme-blue-scaled {
|
||||
--primary: var(--color-blue-600);
|
||||
--primary-foreground: var(--color-blue-50);
|
||||
|
||||
@variant dark {
|
||||
--primary: var(--color-blue-500);
|
||||
--primary-foreground: var(--color-blue-50);
|
||||
}
|
||||
}
|
||||
|
||||
.theme-green,
|
||||
.theme-green-scaled {
|
||||
--primary: var(--color-lime-600);
|
||||
--primary-foreground: var(--color-lime-50);
|
||||
|
||||
@variant dark {
|
||||
--primary: var(--color-lime-600);
|
||||
--primary-foreground: var(--color-lime-50);
|
||||
}
|
||||
}
|
||||
|
||||
.theme-amber,
|
||||
.theme-amber-scaled {
|
||||
--primary: var(--color-amber-600);
|
||||
--primary-foreground: var(--color-amber-50);
|
||||
|
||||
@variant dark {
|
||||
--primary: var(--color-amber-500);
|
||||
--primary-foreground: var(--color-amber-50);
|
||||
}
|
||||
}
|
||||
|
||||
.theme-mono,
|
||||
.theme-mono-scaled {
|
||||
--font-sans: var(--font-mono);
|
||||
--primary: var(--color-neutral-600);
|
||||
--primary-foreground: var(--color-neutral-50);
|
||||
|
||||
@variant dark {
|
||||
--primary: var(--color-neutral-500);
|
||||
--primary-foreground: var(--color-neutral-50);
|
||||
}
|
||||
|
||||
.rounded-xs,
|
||||
.rounded-sm,
|
||||
.rounded-md,
|
||||
.rounded-lg,
|
||||
.rounded-xl {
|
||||
@apply !rounded-none;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.shadow-xs,
|
||||
.shadow-sm,
|
||||
.shadow-md,
|
||||
.shadow-lg,
|
||||
.shadow-xl {
|
||||
@apply !shadow-none;
|
||||
}
|
||||
|
||||
[data-slot="toggle-group"],
|
||||
[data-slot="toggle-group-item"] {
|
||||
@apply !rounded-none !shadow-none;
|
||||
}
|
||||
}
|
||||
96
apps/v4/app/(view)/view/[name]/page.tsx
Normal file
96
apps/v4/app/(view)/view/[name]/page.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
import * as React from "react"
|
||||
import { Metadata } from "next"
|
||||
import { notFound } from "next/navigation"
|
||||
import { registryItemSchema } from "shadcn/registry"
|
||||
import { z } from "zod"
|
||||
|
||||
import { getRegistryComponent, getRegistryItem } from "@/lib/registry"
|
||||
import { absoluteUrl, cn } from "@/lib/utils"
|
||||
import { siteConfig } from "@/www/config/site"
|
||||
|
||||
const getCachedRegistryItem = React.cache(async (name: string) => {
|
||||
return await getRegistryItem(name)
|
||||
})
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{
|
||||
name: string
|
||||
}>
|
||||
}): Promise<Metadata> {
|
||||
const { name } = await params
|
||||
const item = await getCachedRegistryItem(name)
|
||||
|
||||
if (!item) {
|
||||
return {}
|
||||
}
|
||||
|
||||
const title = item.name
|
||||
const description = item.description
|
||||
|
||||
return {
|
||||
title: `${item.name}${item.description ? ` - ${item.description}` : ""}`,
|
||||
description,
|
||||
openGraph: {
|
||||
title,
|
||||
description,
|
||||
type: "article",
|
||||
url: absoluteUrl(`/blocks/${item.name}`),
|
||||
images: [
|
||||
{
|
||||
url: siteConfig.ogImage,
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: siteConfig.name,
|
||||
},
|
||||
],
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title,
|
||||
description,
|
||||
images: [siteConfig.ogImage],
|
||||
creator: "@shadcn",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export const dynamicParams = false
|
||||
|
||||
export async function generateStaticParams() {
|
||||
const { Index } = await import("@/__registry__")
|
||||
const index = z.record(registryItemSchema).parse(Index)
|
||||
|
||||
return Object.values(index)
|
||||
.filter((block) =>
|
||||
["registry:block", "registry:component"].includes(block.type)
|
||||
)
|
||||
.map((block) => ({
|
||||
name: block.name,
|
||||
}))
|
||||
}
|
||||
|
||||
export default async function BlockPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{
|
||||
name: string
|
||||
}>
|
||||
}) {
|
||||
const { name } = await params
|
||||
const item = await getCachedRegistryItem(name)
|
||||
const Component = getRegistryComponent(name)
|
||||
|
||||
if (!item || !Component) {
|
||||
return notFound()
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={cn("themes-wrapper bg-background", item.meta?.container)}>
|
||||
<Component />
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
123
apps/v4/app/globals.css
Normal file
123
apps/v4/app/globals.css
Normal file
@@ -0,0 +1,123 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@import "tw-animate-css";
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
@import "./themes.css";
|
||||
|
||||
:root {
|
||||
--radius: 0.625rem;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.145 0 0);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.145 0 0);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.145 0 0);
|
||||
--primary: oklch(0.205 0 0);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.97 0 0);
|
||||
--secondary-foreground: oklch(0.205 0 0);
|
||||
--muted: oklch(0.97 0 0);
|
||||
--muted-foreground: oklch(0.556 0 0);
|
||||
--accent: oklch(0.97 0 0);
|
||||
--accent-foreground: oklch(0.205 0 0);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.922 0 0);
|
||||
--input: oklch(0.922 0 0);
|
||||
--ring: oklch(0.708 0 0);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.145 0 0);
|
||||
--sidebar-primary: oklch(0.205 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.97 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||
--sidebar-border: oklch(0.922 0 0);
|
||||
--sidebar-ring: oklch(0.708 0 0);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.145 0 0);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.205 0 0);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.269 0 0);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.922 0 0);
|
||||
--primary-foreground: oklch(0.205 0 0);
|
||||
--secondary: oklch(0.269 0 0);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.269 0 0);
|
||||
--muted-foreground: oklch(0.708 0 0);
|
||||
--accent: oklch(0.371 0 0);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.556 0 0);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: oklch(0.205 0 0);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.269 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.439 0 0);
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
124
apps/v4/app/layout.tsx
Normal file
124
apps/v4/app/layout.tsx
Normal file
@@ -0,0 +1,124 @@
|
||||
import type { Metadata, Viewport } from "next"
|
||||
import { cookies } from "next/headers"
|
||||
|
||||
import { fontVariables } from "@/lib/fonts"
|
||||
import { Analytics } from "@/components/analytics"
|
||||
import { ThemeProvider } from "@/components/theme-provider"
|
||||
import { Toaster } from "@/registry/new-york-v4/ui/sonner"
|
||||
import { siteConfig } from "@/www/config/site"
|
||||
|
||||
import "./globals.css"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { ActiveThemeProvider } from "@/components/active-theme"
|
||||
|
||||
const META_THEME_COLORS = {
|
||||
light: "#ffffff",
|
||||
dark: "#09090b",
|
||||
}
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: {
|
||||
default: siteConfig.name,
|
||||
template: `%s - ${siteConfig.name}`,
|
||||
},
|
||||
metadataBase: new URL("https://v4.shadcn.com"),
|
||||
description: siteConfig.description,
|
||||
keywords: [
|
||||
"Next.js",
|
||||
"React",
|
||||
"Tailwind CSS",
|
||||
"Server Components",
|
||||
"Radix UI",
|
||||
],
|
||||
authors: [
|
||||
{
|
||||
name: "shadcn",
|
||||
url: "https://shadcn.com",
|
||||
},
|
||||
],
|
||||
creator: "shadcn",
|
||||
openGraph: {
|
||||
type: "website",
|
||||
locale: "en_US",
|
||||
url: "https://v4.shadcn.com",
|
||||
title: siteConfig.name,
|
||||
description: siteConfig.description,
|
||||
siteName: siteConfig.name,
|
||||
images: [
|
||||
{
|
||||
url: "https://v4.shadcn.com/opengraph-image.png",
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: siteConfig.name,
|
||||
},
|
||||
],
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title: siteConfig.name,
|
||||
description: siteConfig.description,
|
||||
images: ["https://v4.shadcn.com/opengraph-image.png"],
|
||||
creator: "@shadcn",
|
||||
},
|
||||
icons: {
|
||||
icon: "/favicon.ico",
|
||||
shortcut: "/favicon-16x16.png",
|
||||
apple: "/apple-touch-icon.png",
|
||||
},
|
||||
manifest: `${siteConfig.url}/site.webmanifest`,
|
||||
}
|
||||
|
||||
export const viewport: Viewport = {
|
||||
themeColor: META_THEME_COLORS.light,
|
||||
}
|
||||
|
||||
export default async function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode
|
||||
}>) {
|
||||
const cookieStore = await cookies()
|
||||
const activeThemeValue = cookieStore.get("active_theme")?.value
|
||||
const isScaled = activeThemeValue?.endsWith("-scaled")
|
||||
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<head>
|
||||
<script
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
try {
|
||||
if (localStorage.theme === 'dark' || ((!('theme' in localStorage) || localStorage.theme === 'system') && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
||||
document.querySelector('meta[name="theme-color"]').setAttribute('content', '${META_THEME_COLORS.dark}')
|
||||
}
|
||||
} catch (_) {}
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
</head>
|
||||
<body
|
||||
className={cn(
|
||||
"bg-background overscroll-none font-sans antialiased",
|
||||
activeThemeValue ? `theme-${activeThemeValue}` : "",
|
||||
isScaled ? "theme-scaled" : "",
|
||||
fontVariables
|
||||
)}
|
||||
>
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="system"
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
enableColorScheme
|
||||
>
|
||||
<ActiveThemeProvider initialTheme={activeThemeValue}>
|
||||
{children}
|
||||
<Toaster />
|
||||
<Analytics />
|
||||
</ActiveThemeProvider>
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
364
apps/v4/app/themes.css
Normal file
364
apps/v4/app/themes.css
Normal file
@@ -0,0 +1,364 @@
|
||||
.theme-stone {
|
||||
--radius: 0.625rem;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.147 0.004 49.25);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.147 0.004 49.25);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.147 0.004 49.25);
|
||||
--primary: oklch(0.216 0.006 56.043);
|
||||
--primary-foreground: oklch(0.985 0.001 106.423);
|
||||
--secondary: oklch(0.97 0.001 106.424);
|
||||
--secondary-foreground: oklch(0.216 0.006 56.043);
|
||||
--muted: oklch(0.97 0.001 106.424);
|
||||
--muted-foreground: oklch(0.553 0.013 58.071);
|
||||
--accent: oklch(0.97 0.001 106.424);
|
||||
--accent-foreground: oklch(0.216 0.006 56.043);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.923 0.003 48.717);
|
||||
--input: oklch(0.923 0.003 48.717);
|
||||
--ring: oklch(0.709 0.01 56.259);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--sidebar: oklch(0.985 0.001 106.423);
|
||||
--sidebar-foreground: oklch(0.147 0.004 49.25);
|
||||
--sidebar-primary: oklch(0.216 0.006 56.043);
|
||||
--sidebar-primary-foreground: oklch(0.985 0.001 106.423);
|
||||
--sidebar-accent: oklch(0.97 0.001 106.424);
|
||||
--sidebar-accent-foreground: oklch(0.216 0.006 56.043);
|
||||
--sidebar-border: oklch(0.923 0.003 48.717);
|
||||
--sidebar-ring: oklch(0.709 0.01 56.259);
|
||||
|
||||
@variant dark {
|
||||
--background: oklch(0.147 0.004 49.25);
|
||||
--foreground: oklch(0.985 0.001 106.423);
|
||||
--card: oklch(0.216 0.006 56.043);
|
||||
--card-foreground: oklch(0.985 0.001 106.423);
|
||||
--popover: oklch(0.216 0.006 56.043);
|
||||
--popover-foreground: oklch(0.985 0.001 106.423);
|
||||
--primary: oklch(0.923 0.003 48.717);
|
||||
--primary-foreground: oklch(0.216 0.006 56.043);
|
||||
--secondary: oklch(0.268 0.007 34.298);
|
||||
--secondary-foreground: oklch(0.985 0.001 106.423);
|
||||
--muted: oklch(0.268 0.007 34.298);
|
||||
--muted-foreground: oklch(0.709 0.01 56.259);
|
||||
--accent: oklch(0.268 0.007 34.298);
|
||||
--accent-foreground: oklch(0.985 0.001 106.423);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.553 0.013 58.071);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: oklch(0.216 0.006 56.043);
|
||||
--sidebar-foreground: oklch(0.985 0.001 106.423);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.985 0.001 106.423);
|
||||
--sidebar-accent: oklch(0.268 0.007 34.298);
|
||||
--sidebar-accent-foreground: oklch(0.985 0.001 106.423);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.553 0.013 58.071);
|
||||
}
|
||||
}
|
||||
|
||||
.theme-zinc {
|
||||
--radius: 0.625rem;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.141 0.005 285.823);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.141 0.005 285.823);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.141 0.005 285.823);
|
||||
--primary: oklch(0.21 0.006 285.885);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.967 0.001 286.375);
|
||||
--secondary-foreground: oklch(0.21 0.006 285.885);
|
||||
--muted: oklch(0.967 0.001 286.375);
|
||||
--muted-foreground: oklch(0.552 0.016 285.938);
|
||||
--accent: oklch(0.967 0.001 286.375);
|
||||
--accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.92 0.004 286.32);
|
||||
--input: oklch(0.92 0.004 286.32);
|
||||
--ring: oklch(0.705 0.015 286.067);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.141 0.005 285.823);
|
||||
--sidebar-primary: oklch(0.21 0.006 285.885);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.967 0.001 286.375);
|
||||
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--sidebar-border: oklch(0.92 0.004 286.32);
|
||||
--sidebar-ring: oklch(0.705 0.015 286.067);
|
||||
|
||||
@variant dark {
|
||||
--background: oklch(0.141 0.005 285.823);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.21 0.006 285.885);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.21 0.006 285.885);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.92 0.004 286.32);
|
||||
--primary-foreground: oklch(0.21 0.006 285.885);
|
||||
--secondary: oklch(0.274 0.006 286.033);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.274 0.006 286.033);
|
||||
--muted-foreground: oklch(0.705 0.015 286.067);
|
||||
--accent: oklch(0.274 0.006 286.033);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.552 0.016 285.938);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: oklch(0.21 0.006 285.885);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.274 0.006 286.033);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.552 0.016 285.938);
|
||||
}
|
||||
}
|
||||
|
||||
.theme-neutral {
|
||||
--radius: 0.625rem;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.145 0 0);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.145 0 0);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.145 0 0);
|
||||
--primary: oklch(0.205 0 0);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.97 0 0);
|
||||
--secondary-foreground: oklch(0.205 0 0);
|
||||
--muted: oklch(0.97 0 0);
|
||||
--muted-foreground: oklch(0.556 0 0);
|
||||
--accent: oklch(0.97 0 0);
|
||||
--accent-foreground: oklch(0.205 0 0);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.922 0 0);
|
||||
--input: oklch(0.922 0 0);
|
||||
--ring: oklch(0.708 0 0);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.145 0 0);
|
||||
--sidebar-primary: oklch(0.205 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.97 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||
--sidebar-border: oklch(0.922 0 0);
|
||||
--sidebar-ring: oklch(0.708 0 0);
|
||||
|
||||
@variant dark {
|
||||
--background: oklch(0.145 0 0);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.205 0 0);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.205 0 0);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.922 0 0);
|
||||
--primary-foreground: oklch(0.205 0 0);
|
||||
--secondary: oklch(0.269 0 0);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.269 0 0);
|
||||
--muted-foreground: oklch(0.708 0 0);
|
||||
--accent: oklch(0.269 0 0);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.556 0 0);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: oklch(0.205 0 0);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.269 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.556 0 0);
|
||||
}
|
||||
}
|
||||
|
||||
.theme-gray {
|
||||
--radius: 0.625rem;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.13 0.028 261.692);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.13 0.028 261.692);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.13 0.028 261.692);
|
||||
--primary: oklch(0.21 0.034 264.665);
|
||||
--primary-foreground: oklch(0.985 0.002 247.839);
|
||||
--secondary: oklch(0.967 0.003 264.542);
|
||||
--secondary-foreground: oklch(0.21 0.034 264.665);
|
||||
--muted: oklch(0.967 0.003 264.542);
|
||||
--muted-foreground: oklch(0.551 0.027 264.364);
|
||||
--accent: oklch(0.967 0.003 264.542);
|
||||
--accent-foreground: oklch(0.21 0.034 264.665);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.928 0.006 264.531);
|
||||
--input: oklch(0.928 0.006 264.531);
|
||||
--ring: oklch(0.707 0.022 261.325);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--sidebar: oklch(0.985 0.002 247.839);
|
||||
--sidebar-foreground: oklch(0.13 0.028 261.692);
|
||||
--sidebar-primary: oklch(0.21 0.034 264.665);
|
||||
--sidebar-primary-foreground: oklch(0.985 0.002 247.839);
|
||||
--sidebar-accent: oklch(0.967 0.003 264.542);
|
||||
--sidebar-accent-foreground: oklch(0.21 0.034 264.665);
|
||||
--sidebar-border: oklch(0.928 0.006 264.531);
|
||||
--sidebar-ring: oklch(0.707 0.022 261.325);
|
||||
|
||||
@variant dark {
|
||||
--background: oklch(0.13 0.028 261.692);
|
||||
--foreground: oklch(0.985 0.002 247.839);
|
||||
--card: oklch(0.21 0.034 264.665);
|
||||
--card-foreground: oklch(0.985 0.002 247.839);
|
||||
--popover: oklch(0.21 0.034 264.665);
|
||||
--popover-foreground: oklch(0.985 0.002 247.839);
|
||||
--primary: oklch(0.928 0.006 264.531);
|
||||
--primary-foreground: oklch(0.21 0.034 264.665);
|
||||
--secondary: oklch(0.278 0.033 256.848);
|
||||
--secondary-foreground: oklch(0.985 0.002 247.839);
|
||||
--muted: oklch(0.278 0.033 256.848);
|
||||
--muted-foreground: oklch(0.707 0.022 261.325);
|
||||
--accent: oklch(0.278 0.033 256.848);
|
||||
--accent-foreground: oklch(0.985 0.002 247.839);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.551 0.027 264.364);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: oklch(0.21 0.034 264.665);
|
||||
--sidebar-foreground: oklch(0.985 0.002 247.839);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.985 0.002 247.839);
|
||||
--sidebar-accent: oklch(0.278 0.033 256.848);
|
||||
--sidebar-accent-foreground: oklch(0.985 0.002 247.839);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.551 0.027 264.364);
|
||||
}
|
||||
}
|
||||
|
||||
.theme-slate {
|
||||
--radius: 0.625rem;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.129 0.042 264.695);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.129 0.042 264.695);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.129 0.042 264.695);
|
||||
--primary: oklch(0.208 0.042 265.755);
|
||||
--primary-foreground: oklch(0.984 0.003 247.858);
|
||||
--secondary: oklch(0.968 0.007 247.896);
|
||||
--secondary-foreground: oklch(0.208 0.042 265.755);
|
||||
--muted: oklch(0.968 0.007 247.896);
|
||||
--muted-foreground: oklch(0.554 0.046 257.417);
|
||||
--accent: oklch(0.968 0.007 247.896);
|
||||
--accent-foreground: oklch(0.208 0.042 265.755);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.929 0.013 255.508);
|
||||
--input: oklch(0.929 0.013 255.508);
|
||||
--ring: oklch(0.704 0.04 256.788);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--sidebar: oklch(0.984 0.003 247.858);
|
||||
--sidebar-foreground: oklch(0.129 0.042 264.695);
|
||||
--sidebar-primary: oklch(0.208 0.042 265.755);
|
||||
--sidebar-primary-foreground: oklch(0.984 0.003 247.858);
|
||||
--sidebar-accent: oklch(0.968 0.007 247.896);
|
||||
--sidebar-accent-foreground: oklch(0.208 0.042 265.755);
|
||||
--sidebar-border: oklch(0.929 0.013 255.508);
|
||||
--sidebar-ring: oklch(0.704 0.04 256.788);
|
||||
|
||||
@variant dark {
|
||||
--background: oklch(0.129 0.042 264.695);
|
||||
--foreground: oklch(0.984 0.003 247.858);
|
||||
--card: oklch(0.208 0.042 265.755);
|
||||
--card-foreground: oklch(0.984 0.003 247.858);
|
||||
--popover: oklch(0.208 0.042 265.755);
|
||||
--popover-foreground: oklch(0.984 0.003 247.858);
|
||||
--primary: oklch(0.929 0.013 255.508);
|
||||
--primary-foreground: oklch(0.208 0.042 265.755);
|
||||
--secondary: oklch(0.279 0.041 260.031);
|
||||
--secondary-foreground: oklch(0.984 0.003 247.858);
|
||||
--muted: oklch(0.279 0.041 260.031);
|
||||
--muted-foreground: oklch(0.704 0.04 256.788);
|
||||
--accent: oklch(0.279 0.041 260.031);
|
||||
--accent-foreground: oklch(0.984 0.003 247.858);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.551 0.027 264.364);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: oklch(0.208 0.042 265.755);
|
||||
--sidebar-foreground: oklch(0.984 0.003 247.858);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.984 0.003 247.858);
|
||||
--sidebar-accent: oklch(0.279 0.041 260.031);
|
||||
--sidebar-accent-foreground: oklch(0.984 0.003 247.858);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.551 0.027 264.364);
|
||||
}
|
||||
}
|
||||
|
||||
.theme-scaled {
|
||||
@media (min-width: 1024px) {
|
||||
--radius: 0.6rem;
|
||||
--text-lg: 1.05rem;
|
||||
--text-base: 0.85rem;
|
||||
--text-sm: 0.8rem;
|
||||
--spacing: 0.222222rem;
|
||||
}
|
||||
|
||||
[data-slot="card"] {
|
||||
--spacing: 0.16rem;
|
||||
}
|
||||
|
||||
[data-slot="card-header"] *,
|
||||
[data-slot="card-content"] *,
|
||||
[data-slot="card-footer"] * {
|
||||
--spacing: 0.222222rem;
|
||||
}
|
||||
}
|
||||
21
apps/v4/components.json
Normal file
21
apps/v4/components.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "new-york",
|
||||
"rsc": true,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"css": "app/globals.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/registry/new-york-v4/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
},
|
||||
"iconLibrary": "lucide"
|
||||
}
|
||||
72
apps/v4/components/accordion-demo.tsx
Normal file
72
apps/v4/components/accordion-demo.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "@/registry/new-york-v4/ui/accordion"
|
||||
|
||||
export function AccordionDemo() {
|
||||
return (
|
||||
<div className="grid w-full max-w-xl gap-4">
|
||||
<Accordion type="single" collapsible className="w-full">
|
||||
<AccordionItem value="item-1">
|
||||
<AccordionTrigger>Is it accessible?</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
Yes. It adheres to the WAI-ARIA design pattern.
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
<AccordionItem value="item-2">
|
||||
<AccordionTrigger>Is it styled?</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
Yes. It comes with default styles that matches the other
|
||||
components' aesthetic.
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
<AccordionItem value="item-3">
|
||||
<AccordionTrigger>Is it animated?</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
Yes. It's animated by default, but you can disable it if you
|
||||
prefer.
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
<Accordion type="single" collapsible className="w-full">
|
||||
<AccordionItem value="item-1">
|
||||
<AccordionTrigger>
|
||||
What are the key considerations when implementing a comprehensive
|
||||
enterprise-level authentication system?
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
Implementing a robust enterprise authentication system requires
|
||||
careful consideration of multiple factors. This includes secure
|
||||
password hashing and storage, multi-factor authentication (MFA)
|
||||
implementation, session management, OAuth2 and SSO integration,
|
||||
regular security audits, rate limiting to prevent brute force
|
||||
attacks, and maintaining detailed audit logs. Additionally,
|
||||
you'll need to consider scalability, performance impact, and
|
||||
compliance with relevant data protection regulations such as GDPR or
|
||||
HIPAA.
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
<AccordionItem value="item-2">
|
||||
<AccordionTrigger>
|
||||
How does modern distributed system architecture handle eventual
|
||||
consistency and data synchronization across multiple regions?
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
Modern distributed systems employ various strategies to maintain
|
||||
data consistency across regions. This often involves using
|
||||
techniques like CRDT (Conflict-Free Replicated Data Types), vector
|
||||
clocks, and gossip protocols. Systems might implement event sourcing
|
||||
patterns, utilize message queues for asynchronous updates, and
|
||||
employ sophisticated conflict resolution strategies. Popular
|
||||
solutions like Amazon's DynamoDB and Google's Spanner
|
||||
demonstrate different approaches to solving these challenges,
|
||||
balancing between consistency, availability, and partition tolerance
|
||||
as described in the CAP theorem.
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
65
apps/v4/components/active-theme.tsx
Normal file
65
apps/v4/components/active-theme.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
"use client"
|
||||
|
||||
import {
|
||||
createContext,
|
||||
ReactNode,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react"
|
||||
|
||||
const COOKIE_NAME = "active_theme"
|
||||
const DEFAULT_THEME = "default"
|
||||
|
||||
function setThemeCookie(theme: string) {
|
||||
if (typeof window === "undefined") return
|
||||
|
||||
document.cookie = `${COOKIE_NAME}=${theme}; path=/; max-age=31536000; SameSite=Lax; ${window.location.protocol === "https:" ? "Secure;" : ""}`
|
||||
}
|
||||
|
||||
type ThemeContextType = {
|
||||
activeTheme: string
|
||||
setActiveTheme: (theme: string) => void
|
||||
}
|
||||
|
||||
const ThemeContext = createContext<ThemeContextType | undefined>(undefined)
|
||||
|
||||
export function ActiveThemeProvider({
|
||||
children,
|
||||
initialTheme,
|
||||
}: {
|
||||
children: ReactNode
|
||||
initialTheme?: string
|
||||
}) {
|
||||
const [activeTheme, setActiveTheme] = useState<string>(
|
||||
() => initialTheme || DEFAULT_THEME
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
setThemeCookie(activeTheme)
|
||||
|
||||
Array.from(document.body.classList)
|
||||
.filter((className) => className.startsWith("theme-"))
|
||||
.forEach((className) => {
|
||||
document.body.classList.remove(className)
|
||||
})
|
||||
document.body.classList.add(`theme-${activeTheme}`)
|
||||
if (activeTheme.endsWith("-scaled")) {
|
||||
document.body.classList.add("theme-scaled")
|
||||
}
|
||||
}, [activeTheme])
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={{ activeTheme, setActiveTheme }}>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function useThemeConfig() {
|
||||
const context = useContext(ThemeContext)
|
||||
if (context === undefined) {
|
||||
throw new Error("useThemeConfig must be used within an ActiveThemeProvider")
|
||||
}
|
||||
return context
|
||||
}
|
||||
115
apps/v4/components/alert-demo.tsx
Normal file
115
apps/v4/components/alert-demo.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
import {
|
||||
AlertCircleIcon,
|
||||
BookmarkCheckIcon,
|
||||
CheckCircle2Icon,
|
||||
GiftIcon,
|
||||
PopcornIcon,
|
||||
ShieldAlertIcon,
|
||||
} from "lucide-react"
|
||||
|
||||
import {
|
||||
Alert,
|
||||
AlertDescription,
|
||||
AlertTitle,
|
||||
} from "@/registry/new-york-v4/ui/alert"
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
|
||||
export function AlertDemo() {
|
||||
return (
|
||||
<div className="grid max-w-xl items-start gap-4">
|
||||
<Alert>
|
||||
<CheckCircle2Icon />
|
||||
<AlertTitle>Success! Your changes have been saved</AlertTitle>
|
||||
<AlertDescription>
|
||||
This is an alert with icon, title and description.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
<Alert>
|
||||
<BookmarkCheckIcon>Heads up!</BookmarkCheckIcon>
|
||||
<AlertDescription>
|
||||
This one has an icon and a description only. No title.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
<Alert>
|
||||
<AlertDescription>
|
||||
This one has a description only. No title. No icon.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
<Alert>
|
||||
<PopcornIcon />
|
||||
<AlertTitle>Let's try one with icon and title.</AlertTitle>
|
||||
</Alert>
|
||||
<Alert>
|
||||
<ShieldAlertIcon />
|
||||
<AlertTitle>
|
||||
This is a very long alert title that demonstrates how the component
|
||||
handles extended text content and potentially wraps across multiple
|
||||
lines
|
||||
</AlertTitle>
|
||||
</Alert>
|
||||
<Alert>
|
||||
<GiftIcon />
|
||||
<AlertDescription>
|
||||
This is a very long alert description that demonstrates how the
|
||||
component handles extended text content and potentially wraps across
|
||||
multiple lines
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
<Alert>
|
||||
<AlertCircleIcon />
|
||||
<AlertTitle>
|
||||
This is an extremely long alert title that spans multiple lines to
|
||||
demonstrate how the component handles very lengthy headings while
|
||||
maintaining readability and proper text wrapping behavior
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
This is an equally long description that contains detailed information
|
||||
about the alert. It shows how the component can accommodate extensive
|
||||
content while preserving proper spacing, alignment, and readability
|
||||
across different screen sizes and viewport widths. This helps ensure
|
||||
the user experience remains consistent regardless of the content
|
||||
length.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
<Alert variant="destructive">
|
||||
<AlertCircleIcon />
|
||||
<AlertTitle>Something went wrong!</AlertTitle>
|
||||
<AlertDescription>
|
||||
Your session has expired. Please log in again.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
<Alert variant="destructive">
|
||||
<AlertCircleIcon />
|
||||
<AlertTitle>Unable to process your payment.</AlertTitle>
|
||||
<AlertDescription>
|
||||
<p>Please verify your billing information and try again.</p>
|
||||
<ul className="list-inside list-disc text-sm">
|
||||
<li>Check your card details</li>
|
||||
<li>Ensure sufficient funds</li>
|
||||
<li>Verify billing address</li>
|
||||
</ul>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
<Alert>
|
||||
<CheckCircle2Icon />
|
||||
<AlertTitle className="max-w-[calc(100%-4rem)] overflow-ellipsis">
|
||||
The selected emails have been marked as spam.
|
||||
</AlertTitle>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="absolute top-2.5 right-3 h-6 shadow-none"
|
||||
>
|
||||
Undo
|
||||
</Button>
|
||||
</Alert>
|
||||
<Alert className="border-amber-50 bg-amber-50 text-amber-900 dark:border-amber-950 dark:bg-amber-950 dark:text-amber-100">
|
||||
<CheckCircle2Icon />
|
||||
<AlertTitle>Plot Twist: This Alert is Actually Amber!</AlertTitle>
|
||||
<AlertDescription>
|
||||
This one has custom colors for light and dark mode.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
35
apps/v4/components/alert-dialog-demo.tsx
Normal file
35
apps/v4/components/alert-dialog-demo.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from "@/registry/new-york-v4/ui/alert-dialog"
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
|
||||
export function AlertDialogDemo() {
|
||||
return (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="outline">Show Dialog</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This action cannot be undone. This will permanently delete your
|
||||
account and remove your data from our servers.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction>Continue</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
)
|
||||
}
|
||||
7
apps/v4/components/analytics.tsx
Normal file
7
apps/v4/components/analytics.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import { Analytics as VercelAnalytics } from "@vercel/analytics/react"
|
||||
|
||||
export function Analytics() {
|
||||
return <VercelAnalytics />
|
||||
}
|
||||
248
apps/v4/components/app-sidebar.tsx
Normal file
248
apps/v4/components/app-sidebar.tsx
Normal file
@@ -0,0 +1,248 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Index } from "@/__registry__"
|
||||
import {
|
||||
AudioWaveform,
|
||||
BookOpen,
|
||||
Bot,
|
||||
ChevronRightIcon,
|
||||
Command,
|
||||
GalleryVerticalEnd,
|
||||
Search,
|
||||
Settings2,
|
||||
SquareTerminal,
|
||||
} from "lucide-react"
|
||||
|
||||
import { NavUser } from "@/registry/new-york-v4/blocks/sidebar-07/components/nav-user"
|
||||
import { TeamSwitcher } from "@/registry/new-york-v4/blocks/sidebar-07/components/team-switcher"
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from "@/registry/new-york-v4/ui/collapsible"
|
||||
import { Label } from "@/registry/new-york-v4/ui/label"
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarFooter,
|
||||
SidebarGroup,
|
||||
SidebarGroupContent,
|
||||
SidebarGroupLabel,
|
||||
SidebarHeader,
|
||||
SidebarInput,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
SidebarMenuSub,
|
||||
SidebarMenuSubButton,
|
||||
SidebarMenuSubItem,
|
||||
SidebarRail,
|
||||
} from "@/registry/new-york-v4/ui/sidebar"
|
||||
|
||||
// This is sample data.
|
||||
const data = {
|
||||
user: {
|
||||
name: "shadcn",
|
||||
email: "m@example.com",
|
||||
avatar: "/avatars/shadcn.jpg",
|
||||
},
|
||||
teams: [
|
||||
{
|
||||
name: "Acme Inc",
|
||||
logo: GalleryVerticalEnd,
|
||||
plan: "Enterprise",
|
||||
},
|
||||
{
|
||||
name: "Acme Corp.",
|
||||
logo: AudioWaveform,
|
||||
plan: "Startup",
|
||||
},
|
||||
{
|
||||
name: "Evil Corp.",
|
||||
logo: Command,
|
||||
plan: "Free",
|
||||
},
|
||||
],
|
||||
navMain: [
|
||||
{
|
||||
title: "Playground",
|
||||
url: "#",
|
||||
icon: SquareTerminal,
|
||||
isActive: true,
|
||||
items: [
|
||||
{
|
||||
title: "History",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "Starred",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "Settings",
|
||||
url: "#",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Models",
|
||||
url: "#",
|
||||
icon: Bot,
|
||||
items: [
|
||||
{
|
||||
title: "Genesis",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "Explorer",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "Quantum",
|
||||
url: "#",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Documentation",
|
||||
url: "#",
|
||||
icon: BookOpen,
|
||||
items: [
|
||||
{
|
||||
title: "Introduction",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "Get Started",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "Tutorials",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "Changelog",
|
||||
url: "#",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Settings",
|
||||
url: "#",
|
||||
icon: Settings2,
|
||||
items: [
|
||||
{
|
||||
title: "General",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "Team",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "Billing",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "Limits",
|
||||
url: "#",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
components: Object.values(Index)
|
||||
.filter((item) => item.type === "registry:ui")
|
||||
.concat([
|
||||
{
|
||||
name: "combobox",
|
||||
},
|
||||
])
|
||||
.sort((a, b) => a.name.localeCompare(b.name)),
|
||||
}
|
||||
|
||||
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
return (
|
||||
<Sidebar collapsible="icon" {...props}>
|
||||
<SidebarHeader>
|
||||
<TeamSwitcher teams={data.teams} />
|
||||
<SidebarGroup className="py-0 group-data-[collapsible=icon]:hidden">
|
||||
<SidebarGroupContent>
|
||||
<form className="relative">
|
||||
<Label htmlFor="search" className="sr-only">
|
||||
Search
|
||||
</Label>
|
||||
<SidebarInput
|
||||
id="search"
|
||||
placeholder="Search the docs..."
|
||||
className="pl-8"
|
||||
/>
|
||||
<Search className="pointer-events-none absolute top-1/2 left-2 size-4 -translate-y-1/2 opacity-50 select-none" />
|
||||
</form>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
</SidebarHeader>
|
||||
<SidebarContent>
|
||||
<SidebarGroup>
|
||||
<SidebarGroupLabel>Platform</SidebarGroupLabel>
|
||||
<SidebarMenu>
|
||||
{data.navMain.map((item) => (
|
||||
<Collapsible
|
||||
key={item.title}
|
||||
asChild
|
||||
defaultOpen={item.isActive}
|
||||
className="group/collapsible"
|
||||
>
|
||||
<SidebarMenuItem>
|
||||
<CollapsibleTrigger asChild>
|
||||
<SidebarMenuButton tooltip={item.title}>
|
||||
{item.icon && <item.icon />}
|
||||
<span>{item.title}</span>
|
||||
<ChevronRightIcon className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
|
||||
</SidebarMenuButton>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<SidebarMenuSub>
|
||||
{item.items?.map((subItem) => (
|
||||
<SidebarMenuSubItem key={subItem.title}>
|
||||
<SidebarMenuSubButton asChild>
|
||||
<a href={subItem.url}>
|
||||
<span>{subItem.title}</span>
|
||||
</a>
|
||||
</SidebarMenuSubButton>
|
||||
</SidebarMenuSubItem>
|
||||
))}
|
||||
</SidebarMenuSub>
|
||||
</CollapsibleContent>
|
||||
</SidebarMenuItem>
|
||||
</Collapsible>
|
||||
))}
|
||||
</SidebarMenu>
|
||||
</SidebarGroup>
|
||||
<SidebarGroup className="group-data-[collapsible=icon]:hidden">
|
||||
<SidebarGroupLabel>Components</SidebarGroupLabel>
|
||||
<SidebarMenu>
|
||||
{data.components.map((item) => (
|
||||
<SidebarMenuItem key={item.name}>
|
||||
<SidebarMenuButton asChild>
|
||||
<a href={`/#${item.name}`}>
|
||||
<span>{getComponentName(item.name)}</span>
|
||||
</a>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
))}
|
||||
</SidebarMenu>
|
||||
</SidebarGroup>
|
||||
</SidebarContent>
|
||||
<SidebarFooter>
|
||||
<NavUser user={data.user} />
|
||||
</SidebarFooter>
|
||||
<SidebarRail />
|
||||
</Sidebar>
|
||||
)
|
||||
}
|
||||
|
||||
function getComponentName(name: string) {
|
||||
// convert kebab-case to title case
|
||||
return name.replace(/-/g, " ").replace(/\b\w/g, (char) => char.toUpperCase())
|
||||
}
|
||||
26
apps/v4/components/aspect-ratio-demo.tsx
Normal file
26
apps/v4/components/aspect-ratio-demo.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import Image from "next/image"
|
||||
|
||||
import { AspectRatio } from "@/registry/new-york-v4/ui/aspect-ratio"
|
||||
|
||||
export function AspectRatioDemo() {
|
||||
return (
|
||||
<div className="grid w-full max-w-sm items-start gap-4">
|
||||
<AspectRatio ratio={16 / 9} className="bg-muted rounded-lg">
|
||||
<Image
|
||||
src="https://images.unsplash.com/photo-1588345921523-c2dcdb7f1dcd?w=800&dpr=2&q=80"
|
||||
alt="Photo by Drew Beamer"
|
||||
fill
|
||||
className="h-full w-full rounded-lg object-cover dark:brightness-[0.2] dark:grayscale"
|
||||
/>
|
||||
</AspectRatio>
|
||||
<AspectRatio ratio={1 / 1} className="bg-muted rounded-lg">
|
||||
<Image
|
||||
src="https://images.unsplash.com/photo-1588345921523-c2dcdb7f1dcd?w=800&dpr=2&q=80"
|
||||
alt="Photo by Drew Beamer"
|
||||
fill
|
||||
className="h-full w-full rounded-lg object-cover dark:brightness-[0.2] dark:grayscale"
|
||||
/>
|
||||
</AspectRatio>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
81
apps/v4/components/avatar-demo.tsx
Normal file
81
apps/v4/components/avatar-demo.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
import {
|
||||
Avatar,
|
||||
AvatarFallback,
|
||||
AvatarImage,
|
||||
} from "@/registry/new-york-v4/ui/avatar"
|
||||
|
||||
export function AvatarDemo() {
|
||||
return (
|
||||
<div className="flex flex-row flex-wrap items-center gap-4">
|
||||
<Avatar>
|
||||
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
|
||||
<AvatarFallback>CN</AvatarFallback>
|
||||
</Avatar>
|
||||
<Avatar>
|
||||
<AvatarFallback>CN</AvatarFallback>
|
||||
</Avatar>
|
||||
<Avatar className="size-12">
|
||||
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
|
||||
<AvatarFallback>CN</AvatarFallback>
|
||||
</Avatar>
|
||||
<Avatar className="rounded-lg">
|
||||
<AvatarImage
|
||||
src="https://github.com/evilrabbit.png"
|
||||
alt="@evilrabbit"
|
||||
/>
|
||||
<AvatarFallback>ER</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="*:data-[slot=avatar]:ring-background flex -space-x-2 *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:grayscale">
|
||||
<Avatar>
|
||||
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
|
||||
<AvatarFallback>CN</AvatarFallback>
|
||||
</Avatar>
|
||||
<Avatar>
|
||||
<AvatarImage src="https://github.com/leerob.png" alt="@leerob" />
|
||||
<AvatarFallback>LR</AvatarFallback>
|
||||
</Avatar>
|
||||
<Avatar>
|
||||
<AvatarImage
|
||||
src="https://github.com/evilrabbit.png"
|
||||
alt="@evilrabbit"
|
||||
/>
|
||||
<AvatarFallback>ER</AvatarFallback>
|
||||
</Avatar>
|
||||
</div>
|
||||
<div className="*:data-[slot=avatar]:ring-background flex -space-x-2 *:data-[slot=avatar]:size-12 *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:grayscale">
|
||||
<Avatar>
|
||||
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
|
||||
<AvatarFallback>CN</AvatarFallback>
|
||||
</Avatar>
|
||||
<Avatar>
|
||||
<AvatarImage src="https://github.com/leerob.png" alt="@leerob" />
|
||||
<AvatarFallback>LR</AvatarFallback>
|
||||
</Avatar>
|
||||
<Avatar>
|
||||
<AvatarImage
|
||||
src="https://github.com/evilrabbit.png"
|
||||
alt="@evilrabbit"
|
||||
/>
|
||||
<AvatarFallback>ER</AvatarFallback>
|
||||
</Avatar>
|
||||
</div>
|
||||
<div className="*:data-[slot=avatar]:ring-background flex -space-x-2 hover:space-x-1 *:data-[slot=avatar]:size-12 *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:grayscale *:data-[slot=avatar]:transition-all *:data-[slot=avatar]:duration-300 *:data-[slot=avatar]:ease-in-out">
|
||||
<Avatar>
|
||||
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
|
||||
<AvatarFallback>CN</AvatarFallback>
|
||||
</Avatar>
|
||||
<Avatar>
|
||||
<AvatarImage src="https://github.com/leerob.png" alt="@leerob" />
|
||||
<AvatarFallback>LR</AvatarFallback>
|
||||
</Avatar>
|
||||
<Avatar>
|
||||
<AvatarImage
|
||||
src="https://github.com/evilrabbit.png"
|
||||
alt="@evilrabbit"
|
||||
/>
|
||||
<AvatarFallback>ER</AvatarFallback>
|
||||
</Avatar>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
61
apps/v4/components/badge-demo.tsx
Normal file
61
apps/v4/components/badge-demo.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import { AlertCircleIcon, ArrowRightIcon, CheckIcon } from "lucide-react"
|
||||
|
||||
import { Badge } from "@/registry/new-york-v4/ui/badge"
|
||||
|
||||
export function BadgeDemo() {
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<div className="flex w-full flex-wrap gap-2">
|
||||
<Badge>Badge</Badge>
|
||||
<Badge variant="secondary">Secondary</Badge>
|
||||
<Badge variant="destructive">Destructive</Badge>
|
||||
<Badge variant="outline">Outline</Badge>
|
||||
<Badge variant="outline">
|
||||
<CheckIcon />
|
||||
Badge
|
||||
</Badge>
|
||||
<Badge variant="destructive">
|
||||
<AlertCircleIcon />
|
||||
Alert
|
||||
</Badge>
|
||||
<Badge className="h-5 min-w-5 rounded-full px-1 font-mono tabular-nums">
|
||||
8
|
||||
</Badge>
|
||||
<Badge
|
||||
className="h-5 min-w-5 rounded-full px-1 font-mono tabular-nums"
|
||||
variant="destructive"
|
||||
>
|
||||
99
|
||||
</Badge>
|
||||
<Badge
|
||||
className="h-5 min-w-5 rounded-full px-1 font-mono tabular-nums"
|
||||
variant="outline"
|
||||
>
|
||||
20+
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex w-full flex-wrap gap-2">
|
||||
<Badge asChild>
|
||||
<a href="#">
|
||||
Link <ArrowRightIcon />
|
||||
</a>
|
||||
</Badge>
|
||||
<Badge asChild variant="secondary">
|
||||
<a href="#">
|
||||
Link <ArrowRightIcon />
|
||||
</a>
|
||||
</Badge>
|
||||
<Badge asChild variant="destructive">
|
||||
<a href="#">
|
||||
Link <ArrowRightIcon />
|
||||
</a>
|
||||
</Badge>
|
||||
<Badge asChild variant="outline">
|
||||
<a href="#">
|
||||
Link <ArrowRightIcon />
|
||||
</a>
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
49
apps/v4/components/breadcrumb-demo.tsx
Normal file
49
apps/v4/components/breadcrumb-demo.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbEllipsis,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbList,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
} from "@/registry/new-york-v4/ui/breadcrumb"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/registry/new-york-v4/ui/dropdown-menu"
|
||||
|
||||
export function BreadcrumbDemo() {
|
||||
return (
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink href="/">Home</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger className="flex items-center gap-1">
|
||||
<BreadcrumbEllipsis className="h-4 w-4" />
|
||||
<span className="sr-only">Toggle menu</span>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start">
|
||||
<DropdownMenuItem>Documentation</DropdownMenuItem>
|
||||
<DropdownMenuItem>Themes</DropdownMenuItem>
|
||||
<DropdownMenuItem>GitHub</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink href="/docs/components">Components</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbPage>Breadcrumb</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
)
|
||||
}
|
||||
84
apps/v4/components/button-demo.tsx
Normal file
84
apps/v4/components/button-demo.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
import { ArrowRightIcon, Loader2Icon, SendIcon } from "lucide-react"
|
||||
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
|
||||
export function ButtonDemo() {
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex flex-wrap items-center gap-2 md:flex-row">
|
||||
<Button>Button</Button>
|
||||
<Button variant="outline">Outline</Button>
|
||||
<Button variant="ghost">Ghost</Button>
|
||||
<Button variant="destructive">Destructive</Button>
|
||||
<Button variant="secondary">Secondary</Button>
|
||||
<Button variant="link">Link</Button>
|
||||
<Button variant="outline">
|
||||
<SendIcon /> Send
|
||||
</Button>
|
||||
<Button variant="outline">
|
||||
Learn More <ArrowRightIcon />
|
||||
</Button>
|
||||
<Button disabled variant="outline">
|
||||
<Loader2Icon className="animate-spin" />
|
||||
Please wait
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2 md:flex-row">
|
||||
<Button size="sm">Small</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
Outline
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm">
|
||||
Ghost
|
||||
</Button>
|
||||
<Button variant="destructive" size="sm">
|
||||
Destructive
|
||||
</Button>
|
||||
<Button variant="secondary" size="sm">
|
||||
Secondary
|
||||
</Button>
|
||||
<Button variant="link" size="sm">
|
||||
Link
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
<SendIcon /> Send
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
Learn More <ArrowRightIcon />
|
||||
</Button>
|
||||
<Button disabled size="sm" variant="outline">
|
||||
<Loader2Icon className="animate-spin" />
|
||||
Please wait
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2 md:flex-row">
|
||||
<Button size="lg">Large</Button>
|
||||
<Button variant="outline" size="lg">
|
||||
Outline
|
||||
</Button>
|
||||
<Button variant="ghost" size="lg">
|
||||
Ghost
|
||||
</Button>
|
||||
<Button variant="destructive" size="lg">
|
||||
Destructive
|
||||
</Button>
|
||||
<Button variant="secondary" size="lg">
|
||||
Secondary
|
||||
</Button>
|
||||
<Button variant="link" size="lg">
|
||||
Link
|
||||
</Button>
|
||||
<Button variant="outline" size="lg">
|
||||
<SendIcon /> Send
|
||||
</Button>
|
||||
<Button variant="outline" size="lg">
|
||||
Learn More <ArrowRightIcon />
|
||||
</Button>
|
||||
<Button disabled size="lg" variant="outline">
|
||||
<Loader2Icon className="animate-spin" />
|
||||
Please wait
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
47
apps/v4/components/calendar-demo.tsx
Normal file
47
apps/v4/components/calendar-demo.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { addDays } from "date-fns"
|
||||
import { type DateRange } from "react-day-picker"
|
||||
|
||||
import { Calendar } from "@/registry/new-york-v4/ui/calendar"
|
||||
|
||||
export function CalendarDemo() {
|
||||
const [date, setDate] = React.useState<Date | undefined>(new Date())
|
||||
const [dateRange, setDateRange] = React.useState<DateRange | undefined>({
|
||||
from: new Date(new Date().getFullYear(), 0, 12),
|
||||
to: addDays(new Date(new Date().getFullYear(), 0, 12), 30),
|
||||
})
|
||||
const [range, setRange] = React.useState<DateRange | undefined>({
|
||||
from: new Date(new Date().getFullYear(), 0, 12),
|
||||
to: addDays(new Date(new Date().getFullYear(), 0, 12), 50),
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="flex flex-col flex-wrap items-start gap-2 @md:flex-row">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={date}
|
||||
onSelect={setDate}
|
||||
className="rounded-md border shadow-sm"
|
||||
/>
|
||||
<Calendar
|
||||
mode="range"
|
||||
defaultMonth={dateRange?.from}
|
||||
selected={dateRange}
|
||||
onSelect={setDateRange}
|
||||
numberOfMonths={2}
|
||||
disabled={(date) => date > new Date() || date < new Date("1900-01-01")}
|
||||
className="rounded-md border shadow-sm"
|
||||
/>
|
||||
<Calendar
|
||||
mode="range"
|
||||
defaultMonth={range?.from}
|
||||
selected={range}
|
||||
onSelect={setRange}
|
||||
numberOfMonths={3}
|
||||
className="hidden rounded-md border shadow-sm @4xl:flex [&>div]:gap-5"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
191
apps/v4/components/card-demo.tsx
Normal file
191
apps/v4/components/card-demo.tsx
Normal file
@@ -0,0 +1,191 @@
|
||||
import Image from "next/image"
|
||||
import { BathIcon, BedIcon, LandPlotIcon } from "lucide-react"
|
||||
|
||||
import {
|
||||
Avatar,
|
||||
AvatarFallback,
|
||||
AvatarImage,
|
||||
} from "@/registry/new-york-v4/ui/avatar"
|
||||
import { Badge } from "@/registry/new-york-v4/ui/badge"
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/new-york-v4/ui/card"
|
||||
import { Input } from "@/registry/new-york-v4/ui/input"
|
||||
import { Label } from "@/registry/new-york-v4/ui/label"
|
||||
|
||||
export function CardDemo() {
|
||||
return (
|
||||
<div className="flex flex-col items-start gap-4">
|
||||
<Card className="w-full max-w-sm">
|
||||
<CardHeader>
|
||||
<CardTitle>Login to your account</CardTitle>
|
||||
<CardDescription>
|
||||
Enter your email below to login to your account
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form>
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="m@example.com"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<div className="flex items-center">
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<a
|
||||
href="#"
|
||||
className="ml-auto inline-block text-sm underline-offset-4 hover:underline"
|
||||
>
|
||||
Forgot your password?
|
||||
</a>
|
||||
</div>
|
||||
<Input id="password" type="password" required />
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</CardContent>
|
||||
<CardFooter className="flex-col gap-2">
|
||||
<Button type="submit" className="w-full">
|
||||
Login
|
||||
</Button>
|
||||
<Button variant="outline" className="w-full">
|
||||
Login with Google
|
||||
</Button>
|
||||
<div className="mt-4 text-center text-sm">
|
||||
Don't have an account?{" "}
|
||||
<a href="#" className="underline underline-offset-4">
|
||||
Sign up
|
||||
</a>
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Meeting Notes</CardTitle>
|
||||
<CardDescription>
|
||||
Transcript from the meeting with the client.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="text-sm">
|
||||
<p>
|
||||
Client requested dashboard redesign with focus on mobile
|
||||
responsiveness.
|
||||
</p>
|
||||
<ol className="mt-4 flex list-decimal flex-col gap-2 pl-6">
|
||||
<li>New analytics widgets for daily/weekly metrics</li>
|
||||
<li>Simplified navigation menu</li>
|
||||
<li>Dark mode support</li>
|
||||
<li>Timeline: 6 weeks</li>
|
||||
<li>Follow-up meeting scheduled for next Tuesday</li>
|
||||
</ol>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<div className="*:data-[slot=avatar]:ring-background flex -space-x-2 *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:grayscale">
|
||||
<Avatar>
|
||||
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
|
||||
<AvatarFallback>CN</AvatarFallback>
|
||||
</Avatar>
|
||||
<Avatar>
|
||||
<AvatarImage src="https://github.com/leerob.png" alt="@leerob" />
|
||||
<AvatarFallback>LR</AvatarFallback>
|
||||
</Avatar>
|
||||
<Avatar>
|
||||
<AvatarImage
|
||||
src="https://github.com/evilrabbit.png"
|
||||
alt="@evilrabbit"
|
||||
/>
|
||||
<AvatarFallback>ER</AvatarFallback>
|
||||
</Avatar>
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Is this an image?</CardTitle>
|
||||
<CardDescription>This is a card with an image.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="px-0">
|
||||
<Image
|
||||
src="https://images.unsplash.com/photo-1588345921523-c2dcdb7f1dcd?w=800&dpr=2&q=80"
|
||||
alt="Photo by Drew Beamer"
|
||||
className="aspect-video object-cover"
|
||||
width={500}
|
||||
height={500}
|
||||
/>
|
||||
</CardContent>
|
||||
<CardFooter className="flex items-center gap-2">
|
||||
<Badge variant="outline">
|
||||
<BedIcon /> 4
|
||||
</Badge>
|
||||
<Badge variant="outline">
|
||||
<BathIcon /> 2
|
||||
</Badge>
|
||||
<Badge variant="outline">
|
||||
<LandPlotIcon /> 350m²
|
||||
</Badge>
|
||||
<div className="ml-auto font-medium tabular-nums">$135,000</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
<div className="flex w-full flex-wrap items-start gap-8 md:*:data-[slot=card]:basis-1/4">
|
||||
<Card>
|
||||
<CardContent className="text-sm">Content Only</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Header Only</CardTitle>
|
||||
<CardDescription>
|
||||
This is a card with a header and a description.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Header and Content</CardTitle>
|
||||
<CardDescription>
|
||||
This is a card with a header and a content.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="text-sm">Content</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardFooter className="text-sm">Footer Only</CardFooter>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Header + Footer</CardTitle>
|
||||
<CardDescription>
|
||||
This is a card with a header and a footer.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardFooter className="text-sm">Footer</CardFooter>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardContent className="text-sm">Content</CardContent>
|
||||
<CardFooter className="text-sm">Footer</CardFooter>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Header + Footer</CardTitle>
|
||||
<CardDescription>
|
||||
This is a card with a header and a footer.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="text-sm">Content</CardContent>
|
||||
<CardFooter className="text-sm">Footer</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
73
apps/v4/components/carousel-demo.tsx
Normal file
73
apps/v4/components/carousel-demo.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { Card, CardContent } from "@/registry/new-york-v4/ui/card"
|
||||
import {
|
||||
Carousel,
|
||||
CarouselContent,
|
||||
CarouselItem,
|
||||
CarouselNext,
|
||||
CarouselPrevious,
|
||||
} from "@/registry/new-york-v4/ui/carousel"
|
||||
|
||||
export function CarouselDemo() {
|
||||
return (
|
||||
<div className="hidden w-full flex-col items-center gap-4 @4xl:flex">
|
||||
<Carousel className="max-w-sm *:data-[slot=carousel-next]:hidden *:data-[slot=carousel-previous]:hidden *:data-[slot=carousel-next]:md:inline-flex *:data-[slot=carousel-previous]:md:inline-flex">
|
||||
<CarouselContent>
|
||||
{Array.from({ length: 5 }).map((_, index) => (
|
||||
<CarouselItem key={index}>
|
||||
<div className="p-1">
|
||||
<Card>
|
||||
<CardContent className="flex aspect-square items-center justify-center p-6">
|
||||
<span className="text-4xl font-semibold">{index + 1}</span>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</CarouselItem>
|
||||
))}
|
||||
</CarouselContent>
|
||||
<CarouselPrevious />
|
||||
<CarouselNext />
|
||||
</Carousel>
|
||||
<Carousel
|
||||
className="max-w-sm *:data-[slot=carousel-next]:hidden *:data-[slot=carousel-previous]:hidden *:data-[slot=carousel-next]:md:inline-flex *:data-[slot=carousel-previous]:md:inline-flex"
|
||||
opts={{
|
||||
align: "start",
|
||||
}}
|
||||
>
|
||||
<CarouselContent>
|
||||
{Array.from({ length: 5 }).map((_, index) => (
|
||||
<CarouselItem key={index} className="md:basis-1/2 lg:basis-1/3">
|
||||
<div className="p-1">
|
||||
<Card>
|
||||
<CardContent className="flex aspect-square items-center justify-center p-6">
|
||||
<span className="text-3xl font-semibold">{index + 1}</span>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</CarouselItem>
|
||||
))}
|
||||
</CarouselContent>
|
||||
<CarouselPrevious />
|
||||
<CarouselNext />
|
||||
</Carousel>
|
||||
<Carousel className="max-w-sm *:data-[slot=carousel-next]:hidden *:data-[slot=carousel-previous]:hidden *:data-[slot=carousel-next]:md:inline-flex *:data-[slot=carousel-previous]:md:inline-flex">
|
||||
<CarouselContent className="-ml-1">
|
||||
{Array.from({ length: 5 }).map((_, index) => (
|
||||
<CarouselItem key={index} className="pl-1 md:basis-1/2">
|
||||
<div className="p-1">
|
||||
<Card>
|
||||
<CardContent className="flex aspect-square items-center justify-center p-6">
|
||||
<span className="text-2xl font-semibold">{index + 1}</span>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</CarouselItem>
|
||||
))}
|
||||
</CarouselContent>
|
||||
<CarouselPrevious />
|
||||
<CarouselNext />
|
||||
</Carousel>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
94
apps/v4/components/chart-area-demo.tsx
Normal file
94
apps/v4/components/chart-area-demo.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
"use client"
|
||||
|
||||
import { TrendingUp } from "lucide-react"
|
||||
import { Area, AreaChart, CartesianGrid, XAxis } from "recharts"
|
||||
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/new-york-v4/ui/card"
|
||||
import {
|
||||
ChartConfig,
|
||||
ChartContainer,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
} from "@/registry/new-york-v4/ui/chart"
|
||||
|
||||
export const description = "A simple area chart"
|
||||
|
||||
const chartData = [
|
||||
{ month: "January", desktop: 186 },
|
||||
{ month: "February", desktop: 305 },
|
||||
{ month: "March", desktop: 237 },
|
||||
{ month: "April", desktop: 73 },
|
||||
{ month: "May", desktop: 209 },
|
||||
{ month: "June", desktop: 214 },
|
||||
]
|
||||
|
||||
const chartConfig = {
|
||||
desktop: {
|
||||
label: "Desktop",
|
||||
color: "var(--chart-1)",
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
|
||||
export function ChartAreaDemo() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Area Chart</CardTitle>
|
||||
<CardDescription>
|
||||
Showing total visitors for the last 6 months
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ChartContainer config={chartConfig}>
|
||||
<AreaChart
|
||||
accessibilityLayer
|
||||
data={chartData}
|
||||
margin={{
|
||||
left: 12,
|
||||
right: 12,
|
||||
}}
|
||||
>
|
||||
<CartesianGrid vertical={false} />
|
||||
<XAxis
|
||||
dataKey="month"
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={8}
|
||||
tickFormatter={(value) => value.slice(0, 3)}
|
||||
/>
|
||||
<ChartTooltip
|
||||
cursor={false}
|
||||
content={<ChartTooltipContent indicator="line" />}
|
||||
/>
|
||||
<Area
|
||||
dataKey="desktop"
|
||||
type="natural"
|
||||
fill="var(--color-desktop)"
|
||||
fillOpacity={0.4}
|
||||
stroke="var(--color-desktop)"
|
||||
/>
|
||||
</AreaChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<div className="flex w-full items-start gap-2 text-sm">
|
||||
<div className="grid gap-2">
|
||||
<div className="flex items-center gap-2 leading-none font-medium">
|
||||
Trending up by 5.2% this month <TrendingUp className="h-4 w-4" />
|
||||
</div>
|
||||
<div className="text-muted-foreground flex items-center gap-2 leading-none">
|
||||
January - June 2024
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
80
apps/v4/components/chart-bar-demo.tsx
Normal file
80
apps/v4/components/chart-bar-demo.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
"use client"
|
||||
|
||||
import { TrendingUp } from "lucide-react"
|
||||
import { Bar, BarChart, CartesianGrid, XAxis } from "recharts"
|
||||
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/new-york-v4/ui/card"
|
||||
import {
|
||||
ChartConfig,
|
||||
ChartContainer,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
} from "@/registry/new-york-v4/ui/chart"
|
||||
|
||||
export const description = "A multiple bar chart"
|
||||
|
||||
const chartData = [
|
||||
{ month: "January", desktop: 186, mobile: 80 },
|
||||
{ month: "February", desktop: 305, mobile: 200 },
|
||||
{ month: "March", desktop: 237, mobile: 120 },
|
||||
{ month: "April", desktop: 73, mobile: 190 },
|
||||
{ month: "May", desktop: 209, mobile: 130 },
|
||||
{ month: "June", desktop: 214, mobile: 140 },
|
||||
]
|
||||
|
||||
const chartConfig = {
|
||||
desktop: {
|
||||
label: "Desktop",
|
||||
color: "var(--chart-1)",
|
||||
},
|
||||
mobile: {
|
||||
label: "Mobile",
|
||||
color: "var(--chart-2)",
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
|
||||
export function ChartBarDemo() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Bar Chart - Multiple</CardTitle>
|
||||
<CardDescription>January - June 2024</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ChartContainer config={chartConfig}>
|
||||
<BarChart accessibilityLayer data={chartData}>
|
||||
<CartesianGrid vertical={false} />
|
||||
<XAxis
|
||||
dataKey="month"
|
||||
tickLine={false}
|
||||
tickMargin={10}
|
||||
axisLine={false}
|
||||
tickFormatter={(value) => value.slice(0, 3)}
|
||||
/>
|
||||
<ChartTooltip
|
||||
cursor={false}
|
||||
content={<ChartTooltipContent indicator="dashed" />}
|
||||
/>
|
||||
<Bar dataKey="desktop" fill="var(--color-desktop)" radius={4} />
|
||||
<Bar dataKey="mobile" fill="var(--color-mobile)" radius={4} />
|
||||
</BarChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
<CardFooter className="flex-col items-start gap-2 text-sm">
|
||||
<div className="flex gap-2 leading-none font-medium">
|
||||
Trending up by 5.2% this month <TrendingUp className="h-4 w-4" />
|
||||
</div>
|
||||
<div className="text-muted-foreground leading-none">
|
||||
Showing total visitors for the last 6 months
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
103
apps/v4/components/chart-bar-mixed.tsx
Normal file
103
apps/v4/components/chart-bar-mixed.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
"use client"
|
||||
|
||||
import { TrendingUp } from "lucide-react"
|
||||
import { Bar, BarChart, XAxis, YAxis } from "recharts"
|
||||
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/new-york-v4/ui/card"
|
||||
import {
|
||||
ChartConfig,
|
||||
ChartContainer,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
} from "@/registry/new-york-v4/ui/chart"
|
||||
|
||||
export const description = "A mixed bar chart"
|
||||
|
||||
const chartData = [
|
||||
{ browser: "chrome", visitors: 275, fill: "var(--color-chrome)" },
|
||||
{ browser: "safari", visitors: 200, fill: "var(--color-safari)" },
|
||||
{ browser: "firefox", visitors: 187, fill: "var(--color-firefox)" },
|
||||
{ browser: "edge", visitors: 173, fill: "var(--color-edge)" },
|
||||
{ browser: "other", visitors: 90, fill: "var(--color-other)" },
|
||||
]
|
||||
|
||||
const chartConfig = {
|
||||
visitors: {
|
||||
label: "Visitors",
|
||||
},
|
||||
chrome: {
|
||||
label: "Chrome",
|
||||
color: "var(--chart-1)",
|
||||
},
|
||||
safari: {
|
||||
label: "Safari",
|
||||
color: "var(--chart-2)",
|
||||
},
|
||||
firefox: {
|
||||
label: "Firefox",
|
||||
color: "var(--chart-3)",
|
||||
},
|
||||
edge: {
|
||||
label: "Edge",
|
||||
color: "var(--chart-4)",
|
||||
},
|
||||
other: {
|
||||
label: "Other",
|
||||
color: "var(--chart-5)",
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
|
||||
export function ChartBarMixed() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Bar Chart - Mixed</CardTitle>
|
||||
<CardDescription>January - June 2024</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ChartContainer config={chartConfig}>
|
||||
<BarChart
|
||||
accessibilityLayer
|
||||
data={chartData}
|
||||
layout="vertical"
|
||||
margin={{
|
||||
left: 0,
|
||||
}}
|
||||
>
|
||||
<YAxis
|
||||
dataKey="browser"
|
||||
type="category"
|
||||
tickLine={false}
|
||||
tickMargin={10}
|
||||
axisLine={false}
|
||||
tickFormatter={(value) =>
|
||||
chartConfig[value as keyof typeof chartConfig]?.label
|
||||
}
|
||||
/>
|
||||
<XAxis dataKey="visitors" type="number" hide />
|
||||
<ChartTooltip
|
||||
cursor={false}
|
||||
content={<ChartTooltipContent hideLabel />}
|
||||
/>
|
||||
<Bar dataKey="visitors" layout="vertical" radius={5} />
|
||||
</BarChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
<CardFooter className="flex-col items-start gap-2 text-sm">
|
||||
<div className="flex gap-2 leading-none font-medium">
|
||||
Trending up by 5.2% this month <TrendingUp className="h-4 w-4" />
|
||||
</div>
|
||||
<div className="text-muted-foreground leading-none">
|
||||
Showing total visitors for the last 6 months
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
17
apps/v4/components/chart-demo.tsx
Normal file
17
apps/v4/components/chart-demo.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import { ChartAreaDemo } from "@/components/chart-area-demo"
|
||||
import { ChartBarDemo } from "@/components/chart-bar-demo"
|
||||
import { ChartLineDemo } from "@/components/chart-line-demo"
|
||||
import { ChartBarMixed } from "@/registry/new-york-v4/charts/chart-bar-mixed"
|
||||
|
||||
export function ChartDemo() {
|
||||
return (
|
||||
<div className="grid w-full max-w-screen-2xl gap-4 *:data-[slot=card]:flex-1 @2xl:grid-cols-2 @6xl:grid-cols-3">
|
||||
<ChartAreaDemo />
|
||||
<ChartBarDemo />
|
||||
<ChartBarMixed />
|
||||
<div className="@6xl:hidden">
|
||||
<ChartLineDemo />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
100
apps/v4/components/chart-line-demo.tsx
Normal file
100
apps/v4/components/chart-line-demo.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
"use client"
|
||||
|
||||
import { TrendingUp } from "lucide-react"
|
||||
import { CartesianGrid, Line, LineChart, XAxis } from "recharts"
|
||||
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/new-york-v4/ui/card"
|
||||
import {
|
||||
ChartConfig,
|
||||
ChartContainer,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
} from "@/registry/new-york-v4/ui/chart"
|
||||
|
||||
export const description = "A multiple line chart"
|
||||
|
||||
const chartData = [
|
||||
{ month: "January", desktop: 186, mobile: 80 },
|
||||
{ month: "February", desktop: 305, mobile: 200 },
|
||||
{ month: "March", desktop: 237, mobile: 120 },
|
||||
{ month: "April", desktop: 73, mobile: 190 },
|
||||
{ month: "May", desktop: 209, mobile: 130 },
|
||||
{ month: "June", desktop: 214, mobile: 140 },
|
||||
]
|
||||
|
||||
const chartConfig = {
|
||||
desktop: {
|
||||
label: "Desktop",
|
||||
color: "var(--chart-1)",
|
||||
},
|
||||
mobile: {
|
||||
label: "Mobile",
|
||||
color: "var(--chart-2)",
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
|
||||
export function ChartLineDemo() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Line Chart - Multiple</CardTitle>
|
||||
<CardDescription>January - June 2024</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ChartContainer config={chartConfig}>
|
||||
<LineChart
|
||||
accessibilityLayer
|
||||
data={chartData}
|
||||
margin={{
|
||||
left: 12,
|
||||
right: 12,
|
||||
}}
|
||||
>
|
||||
<CartesianGrid vertical={false} />
|
||||
<XAxis
|
||||
dataKey="month"
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={8}
|
||||
tickFormatter={(value) => value.slice(0, 3)}
|
||||
/>
|
||||
<ChartTooltip cursor={false} content={<ChartTooltipContent />} />
|
||||
<Line
|
||||
dataKey="desktop"
|
||||
type="monotone"
|
||||
stroke="var(--color-desktop)"
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
/>
|
||||
<Line
|
||||
dataKey="mobile"
|
||||
type="monotone"
|
||||
stroke="var(--color-mobile)"
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
/>
|
||||
</LineChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<div className="flex w-full items-start gap-2 text-sm">
|
||||
<div className="grid gap-2">
|
||||
<div className="flex items-center gap-2 leading-none font-medium">
|
||||
Trending up by 5.2% this month <TrendingUp className="h-4 w-4" />
|
||||
</div>
|
||||
<div className="text-muted-foreground flex items-center gap-2 leading-none">
|
||||
Showing total visitors for the last 6 months
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
43
apps/v4/components/checkbox-demo.tsx
Normal file
43
apps/v4/components/checkbox-demo.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
"use client"
|
||||
|
||||
import { Checkbox } from "@/registry/new-york-v4/ui/checkbox"
|
||||
import { Label } from "@/registry/new-york-v4/ui/label"
|
||||
|
||||
export function CheckboxDemo() {
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<Checkbox id="terms" />
|
||||
<Label htmlFor="terms">Accept terms and conditions</Label>
|
||||
</div>
|
||||
<div className="flex items-start gap-3">
|
||||
<Checkbox id="terms-2" defaultChecked />
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="terms-2">Accept terms and conditions</Label>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
By clicking this checkbox, you agree to the terms and conditions.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-3">
|
||||
<Checkbox id="toggle" disabled />
|
||||
<Label htmlFor="toggle">Enable notifications</Label>
|
||||
</div>
|
||||
<Label className="hover:bg-accent/50 flex items-start gap-3 rounded-lg border p-3 has-[[aria-checked=true]]:border-blue-600 has-[[aria-checked=true]]:bg-blue-50 dark:has-[[aria-checked=true]]:border-blue-900 dark:has-[[aria-checked=true]]:bg-blue-950">
|
||||
<Checkbox
|
||||
id="toggle-2"
|
||||
defaultChecked
|
||||
className="data-[state=checked]:border-blue-600 data-[state=checked]:bg-blue-600 data-[state=checked]:text-white dark:data-[state=checked]:border-blue-700 dark:data-[state=checked]:bg-blue-700"
|
||||
/>
|
||||
<div className="grid gap-1.5 font-normal">
|
||||
<p className="text-sm leading-none font-medium">
|
||||
Enable notifications
|
||||
</p>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
You can enable or disable notifications at any time.
|
||||
</p>
|
||||
</div>
|
||||
</Label>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
46
apps/v4/components/collapsible-demo.tsx
Normal file
46
apps/v4/components/collapsible-demo.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { ChevronsUpDown } from "lucide-react"
|
||||
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from "@/registry/new-york-v4/ui/collapsible"
|
||||
|
||||
export function CollapsibleDemo() {
|
||||
const [isOpen, setIsOpen] = React.useState(false)
|
||||
|
||||
return (
|
||||
<Collapsible
|
||||
open={isOpen}
|
||||
onOpenChange={setIsOpen}
|
||||
className="flex w-full flex-col gap-2 md:w-[350px]"
|
||||
>
|
||||
<div className="flex items-center justify-between gap-4 px-4">
|
||||
<h4 className="line-clamp-1 text-sm font-semibold">
|
||||
@peduarte starred 3 repositories
|
||||
</h4>
|
||||
<CollapsibleTrigger asChild>
|
||||
<Button variant="ghost" size="sm">
|
||||
<ChevronsUpDown className="h-4 w-4" />
|
||||
<span className="sr-only">Toggle</span>
|
||||
</Button>
|
||||
</CollapsibleTrigger>
|
||||
</div>
|
||||
<div className="rounded-md border px-4 py-2 font-mono text-sm shadow-xs">
|
||||
@radix-ui/primitives
|
||||
</div>
|
||||
<CollapsibleContent className="flex flex-col gap-2">
|
||||
<div className="rounded-md border px-4 py-2 font-mono text-sm shadow-xs">
|
||||
@radix-ui/colors
|
||||
</div>
|
||||
<div className="rounded-md border px-4 py-2 font-mono text-sm shadow-xs">
|
||||
@stitches/react
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
)
|
||||
}
|
||||
405
apps/v4/components/combobox-demo.tsx
Normal file
405
apps/v4/components/combobox-demo.tsx
Normal file
@@ -0,0 +1,405 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import {
|
||||
CheckIcon,
|
||||
ChevronDownIcon,
|
||||
ChevronsUpDown,
|
||||
PlusCircleIcon,
|
||||
} from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import {
|
||||
Avatar,
|
||||
AvatarFallback,
|
||||
AvatarImage,
|
||||
} from "@/registry/new-york-v4/ui/avatar"
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
CommandSeparator,
|
||||
} from "@/registry/new-york-v4/ui/command"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/registry/new-york-v4/ui/popover"
|
||||
|
||||
const frameworks = [
|
||||
{
|
||||
value: "next.js",
|
||||
label: "Next.js",
|
||||
},
|
||||
{
|
||||
value: "sveltekit",
|
||||
label: "SvelteKit",
|
||||
},
|
||||
{
|
||||
value: "nuxt.js",
|
||||
label: "Nuxt.js",
|
||||
},
|
||||
{
|
||||
value: "remix",
|
||||
label: "Remix",
|
||||
},
|
||||
{
|
||||
value: "astro",
|
||||
label: "Astro",
|
||||
},
|
||||
]
|
||||
|
||||
type Framework = (typeof frameworks)[number]
|
||||
|
||||
const users = [
|
||||
{
|
||||
id: "1",
|
||||
username: "shadcn",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
username: "leerob",
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
username: "evilrabbit",
|
||||
},
|
||||
] as const
|
||||
|
||||
type User = (typeof users)[number]
|
||||
|
||||
const timezones = [
|
||||
{
|
||||
label: "Americas",
|
||||
timezones: [
|
||||
{ value: "America/New_York", label: "(GMT-5) New York" },
|
||||
{ value: "America/Los_Angeles", label: "(GMT-8) Los Angeles" },
|
||||
{ value: "America/Chicago", label: "(GMT-6) Chicago" },
|
||||
{ value: "America/Toronto", label: "(GMT-5) Toronto" },
|
||||
{ value: "America/Vancouver", label: "(GMT-8) Vancouver" },
|
||||
{ value: "America/Sao_Paulo", label: "(GMT-3) São Paulo" },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Europe",
|
||||
timezones: [
|
||||
{ value: "Europe/London", label: "(GMT+0) London" },
|
||||
{ value: "Europe/Paris", label: "(GMT+1) Paris" },
|
||||
{ value: "Europe/Berlin", label: "(GMT+1) Berlin" },
|
||||
{ value: "Europe/Rome", label: "(GMT+1) Rome" },
|
||||
{ value: "Europe/Madrid", label: "(GMT+1) Madrid" },
|
||||
{ value: "Europe/Amsterdam", label: "(GMT+1) Amsterdam" },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Asia/Pacific",
|
||||
timezones: [
|
||||
{ value: "Asia/Tokyo", label: "(GMT+9) Tokyo" },
|
||||
{ value: "Asia/Shanghai", label: "(GMT+8) Shanghai" },
|
||||
{ value: "Asia/Singapore", label: "(GMT+8) Singapore" },
|
||||
{ value: "Asia/Dubai", label: "(GMT+4) Dubai" },
|
||||
{ value: "Australia/Sydney", label: "(GMT+11) Sydney" },
|
||||
{ value: "Asia/Seoul", label: "(GMT+9) Seoul" },
|
||||
],
|
||||
},
|
||||
] as const
|
||||
|
||||
type Timezone = (typeof timezones)[number]
|
||||
|
||||
export function ComboboxDemo() {
|
||||
return (
|
||||
<div className="flex w-full flex-wrap items-start gap-4">
|
||||
<FrameworkCombobox frameworks={[...frameworks]} />
|
||||
<UserCombobox users={[...users]} selectedUserId={users[0].id} />
|
||||
<TimezoneCombobox
|
||||
timezones={[...timezones]}
|
||||
selectedTimezone={timezones[0].timezones[0]}
|
||||
/>
|
||||
<ComboboxWithCheckbox frameworks={[...frameworks]} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function FrameworkCombobox({ frameworks }: { frameworks: Framework[] }) {
|
||||
const [open, setOpen] = React.useState(false)
|
||||
const [value, setValue] = React.useState("")
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={open}
|
||||
className="w-full justify-between md:max-w-[200px]"
|
||||
>
|
||||
{value
|
||||
? frameworks.find((framework) => framework.value === value)?.label
|
||||
: "Select framework..."}
|
||||
<ChevronsUpDown className="text-muted-foreground" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-(--radix-popover-trigger-width) p-0">
|
||||
<Command>
|
||||
<CommandInput placeholder="Search framework..." />
|
||||
<CommandList>
|
||||
<CommandEmpty>No framework found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{frameworks.map((framework) => (
|
||||
<CommandItem
|
||||
key={framework.value}
|
||||
value={framework.value}
|
||||
onSelect={(currentValue) => {
|
||||
setValue(currentValue === value ? "" : currentValue)
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
{framework.label}
|
||||
<CheckIcon
|
||||
className={cn(
|
||||
"ml-auto",
|
||||
value === framework.value ? "opacity-100" : "opacity-0"
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
function UserCombobox({
|
||||
users,
|
||||
selectedUserId,
|
||||
}: {
|
||||
users: User[]
|
||||
selectedUserId: string
|
||||
}) {
|
||||
const [open, setOpen] = React.useState(false)
|
||||
const [value, setValue] = React.useState(selectedUserId)
|
||||
|
||||
const selectedUser = React.useMemo(
|
||||
() => users.find((user) => user.id === value),
|
||||
[value, users]
|
||||
)
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={open}
|
||||
className="w-full justify-between px-2 md:max-w-[200px]"
|
||||
>
|
||||
{selectedUser ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<Avatar className="size-5">
|
||||
<AvatarImage
|
||||
src={`https://github.com/${selectedUser.username}.png`}
|
||||
/>
|
||||
<AvatarFallback>{selectedUser.username[0]}</AvatarFallback>
|
||||
</Avatar>
|
||||
{selectedUser.username}
|
||||
</div>
|
||||
) : (
|
||||
"Select user..."
|
||||
)}
|
||||
<ChevronsUpDown className="text-muted-foreground" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-(--radix-popover-trigger-width) p-0">
|
||||
<Command>
|
||||
<CommandInput placeholder="Search user..." />
|
||||
<CommandList>
|
||||
<CommandEmpty>No user found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{users.map((user) => (
|
||||
<CommandItem
|
||||
key={user.id}
|
||||
value={user.id}
|
||||
onSelect={(currentValue) => {
|
||||
setValue(currentValue === value ? "" : currentValue)
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
<Avatar className="size-5">
|
||||
<AvatarImage
|
||||
src={`https://github.com/${user.username}.png`}
|
||||
/>
|
||||
<AvatarFallback>{user.username[0]}</AvatarFallback>
|
||||
</Avatar>
|
||||
{user.username}
|
||||
<CheckIcon
|
||||
className={cn(
|
||||
"ml-auto",
|
||||
value === user.id ? "opacity-100" : "opacity-0"
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
<CommandSeparator />
|
||||
<CommandGroup>
|
||||
<CommandItem>
|
||||
<PlusCircleIcon />
|
||||
Create user
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
function TimezoneCombobox({
|
||||
timezones,
|
||||
selectedTimezone,
|
||||
}: {
|
||||
timezones: Timezone[]
|
||||
selectedTimezone: Timezone["timezones"][number]
|
||||
}) {
|
||||
const [open, setOpen] = React.useState(false)
|
||||
const [value, setValue] = React.useState(selectedTimezone.value)
|
||||
|
||||
const selectedGroup = React.useMemo(
|
||||
() =>
|
||||
timezones.find((group) =>
|
||||
group.timezones.find((tz) => tz.value === value)
|
||||
),
|
||||
[value, timezones]
|
||||
)
|
||||
|
||||
const selectedTimezoneLabel = React.useMemo(
|
||||
() => selectedGroup?.timezones.find((tz) => tz.value === value)?.label,
|
||||
[value, selectedGroup]
|
||||
)
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="h-12 w-full justify-between px-2.5 md:max-w-[200px]"
|
||||
>
|
||||
{selectedTimezone ? (
|
||||
<div className="flex flex-col items-start gap-0.5">
|
||||
<span className="text-muted-foreground text-xs font-normal">
|
||||
{selectedGroup?.label}
|
||||
</span>
|
||||
<span>{selectedTimezoneLabel}</span>
|
||||
</div>
|
||||
) : (
|
||||
"Select timezone"
|
||||
)}
|
||||
<ChevronDownIcon className="text-muted-foreground" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0" align="start">
|
||||
<Command>
|
||||
<CommandInput placeholder="Search timezone..." />
|
||||
<CommandList className="scroll-pb-12">
|
||||
<CommandEmpty>No timezone found.</CommandEmpty>
|
||||
{timezones.map((region) => (
|
||||
<CommandGroup key={region.label} heading={region.label}>
|
||||
{region.timezones.map((timezone) => (
|
||||
<CommandItem
|
||||
key={timezone.value}
|
||||
value={timezone.value}
|
||||
onSelect={(currentValue) => {
|
||||
setValue(
|
||||
currentValue as Timezone["timezones"][number]["value"]
|
||||
)
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
{timezone.label}
|
||||
<CheckIcon
|
||||
className="ml-auto opacity-0 data-[selected=true]:opacity-100"
|
||||
data-selected={value === timezone.value}
|
||||
/>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
))}
|
||||
<CommandSeparator className="sticky bottom-10" />
|
||||
<CommandGroup className="bg-popover sticky bottom-0">
|
||||
<CommandItem>
|
||||
<PlusCircleIcon />
|
||||
Create timezone
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
function ComboboxWithCheckbox({ frameworks }: { frameworks: Framework[] }) {
|
||||
const [open, setOpen] = React.useState(false)
|
||||
const [selectedFrameworks, setSelectedFrameworks] = React.useState<
|
||||
Framework[]
|
||||
>([])
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={open}
|
||||
className="w-fit min-w-[280px] justify-between"
|
||||
>
|
||||
{selectedFrameworks.length > 0
|
||||
? selectedFrameworks.map((framework) => framework.label).join(", ")
|
||||
: "Select frameworks (multi-select)..."}
|
||||
<ChevronsUpDown className="text-muted-foreground" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[300px] p-0" align="start">
|
||||
<Command>
|
||||
<CommandInput placeholder="Search framework..." />
|
||||
<CommandList>
|
||||
<CommandEmpty>No framework found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{frameworks.map((framework) => (
|
||||
<CommandItem
|
||||
key={framework.value}
|
||||
value={framework.value}
|
||||
onSelect={(currentValue) => {
|
||||
setSelectedFrameworks(
|
||||
selectedFrameworks.some((f) => f.value === currentValue)
|
||||
? selectedFrameworks.filter(
|
||||
(f) => f.value !== currentValue
|
||||
)
|
||||
: [...selectedFrameworks, framework]
|
||||
)
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="border-input data-[selected=true]:border-primary data-[selected=true]:bg-primary data-[selected=true]:text-primary-foreground pointer-events-none size-4 shrink-0 rounded-[4px] border transition-all select-none *:[svg]:opacity-0 data-[selected=true]:*:[svg]:opacity-100"
|
||||
data-selected={selectedFrameworks.some(
|
||||
(f) => f.value === framework.value
|
||||
)}
|
||||
>
|
||||
<CheckIcon className="size-3.5 text-current" />
|
||||
</div>
|
||||
{framework.label}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
87
apps/v4/components/command-demo.tsx
Normal file
87
apps/v4/components/command-demo.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import {
|
||||
Calculator,
|
||||
Calendar,
|
||||
CreditCard,
|
||||
Settings,
|
||||
Smile,
|
||||
User,
|
||||
} from "lucide-react"
|
||||
|
||||
import {
|
||||
CommandDialog,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
CommandSeparator,
|
||||
CommandShortcut,
|
||||
} from "@/registry/new-york-v4/ui/command"
|
||||
|
||||
export function CommandDemo() {
|
||||
const [open, setOpen] = React.useState(false)
|
||||
|
||||
React.useEffect(() => {
|
||||
const down = (e: KeyboardEvent) => {
|
||||
if (e.key === "j" && (e.metaKey || e.ctrlKey)) {
|
||||
e.preventDefault()
|
||||
setOpen((open) => !open)
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("keydown", down)
|
||||
return () => document.removeEventListener("keydown", down)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Press{" "}
|
||||
<kbd className="bg-muted text-muted-foreground pointer-events-none inline-flex h-5 items-center gap-1 rounded border px-1.5 font-mono text-[10px] font-medium opacity-100 select-none">
|
||||
<span className="text-xs">⌘</span>J
|
||||
</kbd>
|
||||
</p>
|
||||
<CommandDialog open={open} onOpenChange={setOpen}>
|
||||
<CommandInput placeholder="Type a command or search..." />
|
||||
<CommandList>
|
||||
<CommandEmpty>No results found.</CommandEmpty>
|
||||
<CommandGroup heading="Suggestions">
|
||||
<CommandItem>
|
||||
<Calendar />
|
||||
<span>Calendar</span>
|
||||
</CommandItem>
|
||||
<CommandItem>
|
||||
<Smile />
|
||||
<span>Search Emoji</span>
|
||||
</CommandItem>
|
||||
<CommandItem>
|
||||
<Calculator />
|
||||
<span>Calculator</span>
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
<CommandSeparator />
|
||||
<CommandGroup heading="Settings">
|
||||
<CommandItem>
|
||||
<User />
|
||||
<span>Profile</span>
|
||||
<CommandShortcut>⌘P</CommandShortcut>
|
||||
</CommandItem>
|
||||
<CommandItem>
|
||||
<CreditCard />
|
||||
<span>Billing</span>
|
||||
<CommandShortcut>⌘B</CommandShortcut>
|
||||
</CommandItem>
|
||||
<CommandItem>
|
||||
<Settings />
|
||||
<span>Settings</span>
|
||||
<CommandShortcut>⌘S</CommandShortcut>
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</CommandDialog>
|
||||
</>
|
||||
)
|
||||
}
|
||||
66
apps/v4/components/component-wrapper.tsx
Normal file
66
apps/v4/components/component-wrapper.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/registry/new-york-v4/lib/utils"
|
||||
|
||||
export function ComponentWrapper({
|
||||
className,
|
||||
name,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentPropsWithoutRef<"div"> & { name: string }) {
|
||||
return (
|
||||
<ComponentErrorBoundary name={name}>
|
||||
<div
|
||||
id={name}
|
||||
data-name={name.toLowerCase()}
|
||||
className={cn(
|
||||
"flex w-full scroll-mt-16 flex-col rounded-lg border",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="border-b px-4 py-3">
|
||||
<div className="text-sm font-medium">{getComponentName(name)}</div>
|
||||
</div>
|
||||
<div className="flex flex-1 items-center gap-2 p-4">{children}</div>
|
||||
</div>
|
||||
</ComponentErrorBoundary>
|
||||
)
|
||||
}
|
||||
|
||||
class ComponentErrorBoundary extends React.Component<
|
||||
{ children: React.ReactNode; name: string },
|
||||
{ hasError: boolean }
|
||||
> {
|
||||
constructor(props: { children: React.ReactNode; name: string }) {
|
||||
super(props)
|
||||
this.state = { hasError: false }
|
||||
}
|
||||
|
||||
static getDerivedStateFromError() {
|
||||
return { hasError: true }
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
||||
console.error(`Error in component ${this.props.name}:`, error, errorInfo)
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
return (
|
||||
<div className="p-4 text-red-500">
|
||||
Something went wrong in component: {this.props.name}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return this.props.children
|
||||
}
|
||||
}
|
||||
|
||||
function getComponentName(name: string) {
|
||||
// convert kebab-case to title case
|
||||
return name.replace(/-/g, " ").replace(/\b\w/g, (char) => char.toUpperCase())
|
||||
}
|
||||
79
apps/v4/components/context-menu-demo.tsx
Normal file
79
apps/v4/components/context-menu-demo.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
import { Code2Icon, PlusIcon, TrashIcon } from "lucide-react"
|
||||
|
||||
import {
|
||||
ContextMenu,
|
||||
ContextMenuCheckboxItem,
|
||||
ContextMenuContent,
|
||||
ContextMenuItem,
|
||||
ContextMenuLabel,
|
||||
ContextMenuRadioGroup,
|
||||
ContextMenuRadioItem,
|
||||
ContextMenuSeparator,
|
||||
ContextMenuShortcut,
|
||||
ContextMenuSub,
|
||||
ContextMenuSubContent,
|
||||
ContextMenuSubTrigger,
|
||||
ContextMenuTrigger,
|
||||
} from "@/registry/new-york-v4/ui/context-menu"
|
||||
|
||||
export function ContextMenuDemo() {
|
||||
return (
|
||||
<ContextMenu>
|
||||
<ContextMenuTrigger className="flex h-[150px] w-[300px] items-center justify-center rounded-md border border-dashed text-sm">
|
||||
Right click here
|
||||
</ContextMenuTrigger>
|
||||
<ContextMenuContent className="w-64">
|
||||
<ContextMenuItem inset>
|
||||
Back
|
||||
<ContextMenuShortcut>⌘[</ContextMenuShortcut>
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem inset disabled>
|
||||
Forward
|
||||
<ContextMenuShortcut>⌘]</ContextMenuShortcut>
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem inset>
|
||||
Reload
|
||||
<ContextMenuShortcut>⌘R</ContextMenuShortcut>
|
||||
</ContextMenuItem>
|
||||
<ContextMenuSub>
|
||||
<ContextMenuSubTrigger inset>More Tools</ContextMenuSubTrigger>
|
||||
<ContextMenuSubContent className="w-48">
|
||||
<ContextMenuItem inset>
|
||||
Save Page...
|
||||
<ContextMenuShortcut>⇧⌘S</ContextMenuShortcut>
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem>
|
||||
<PlusIcon />
|
||||
Create Shortcut...
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem inset>Name Window...</ContextMenuItem>
|
||||
<ContextMenuSeparator />
|
||||
<ContextMenuItem>
|
||||
<Code2Icon />
|
||||
Developer Tools
|
||||
</ContextMenuItem>
|
||||
<ContextMenuSeparator />
|
||||
<ContextMenuItem variant="destructive">
|
||||
<TrashIcon />
|
||||
Delete
|
||||
</ContextMenuItem>
|
||||
</ContextMenuSubContent>
|
||||
</ContextMenuSub>
|
||||
<ContextMenuSeparator />
|
||||
<ContextMenuCheckboxItem checked>
|
||||
Show Bookmarks Bar
|
||||
<ContextMenuShortcut>⌘⇧B</ContextMenuShortcut>
|
||||
</ContextMenuCheckboxItem>
|
||||
<ContextMenuCheckboxItem>Show Full URLs</ContextMenuCheckboxItem>
|
||||
<ContextMenuSeparator />
|
||||
<ContextMenuRadioGroup value="pedro">
|
||||
<ContextMenuLabel inset>People</ContextMenuLabel>
|
||||
<ContextMenuRadioItem value="pedro">
|
||||
Pedro Duarte
|
||||
</ContextMenuRadioItem>
|
||||
<ContextMenuRadioItem value="colm">Colm Tuite</ContextMenuRadioItem>
|
||||
</ContextMenuRadioGroup>
|
||||
</ContextMenuContent>
|
||||
</ContextMenu>
|
||||
)
|
||||
}
|
||||
99
apps/v4/components/date-picker-demo.tsx
Normal file
99
apps/v4/components/date-picker-demo.tsx
Normal file
@@ -0,0 +1,99 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { addDays, format } from "date-fns"
|
||||
import { CalendarIcon } from "lucide-react"
|
||||
import { DateRange } from "react-day-picker"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import { Calendar } from "@/registry/new-york-v4/ui/calendar"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/registry/new-york-v4/ui/popover"
|
||||
|
||||
export function DatePickerDemo() {
|
||||
return (
|
||||
<div className="flex flex-col items-start gap-4 md:flex-row">
|
||||
<DatePickerSimple />
|
||||
<DatePickerWithRange />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function DatePickerSimple() {
|
||||
const [date, setDate] = React.useState<Date>()
|
||||
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant={"outline"}
|
||||
className={cn(
|
||||
"min-w-[200px] justify-start px-2 font-normal",
|
||||
!date && "text-muted-foreground"
|
||||
)}
|
||||
>
|
||||
<CalendarIcon className="text-muted-foreground" />
|
||||
{date ? format(date, "PPP") : <span>Pick a date</span>}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0" align="start">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={date}
|
||||
onSelect={setDate}
|
||||
initialFocus
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
function DatePickerWithRange() {
|
||||
const [date, setDate] = React.useState<DateRange | undefined>({
|
||||
from: new Date(new Date().getFullYear(), 0, 20),
|
||||
to: addDays(new Date(new Date().getFullYear(), 0, 20), 20),
|
||||
})
|
||||
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
id="date"
|
||||
variant={"outline"}
|
||||
className={cn(
|
||||
"w-fit justify-start px-2 font-normal",
|
||||
!date && "text-muted-foreground"
|
||||
)}
|
||||
>
|
||||
<CalendarIcon className="text-muted-foreground" />
|
||||
{date?.from ? (
|
||||
date.to ? (
|
||||
<>
|
||||
{format(date.from, "LLL dd, y")} -{" "}
|
||||
{format(date.to, "LLL dd, y")}
|
||||
</>
|
||||
) : (
|
||||
format(date.from, "LLL dd, y")
|
||||
)
|
||||
) : (
|
||||
<span>Pick a date</span>
|
||||
)}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0" align="start">
|
||||
<Calendar
|
||||
initialFocus
|
||||
mode="range"
|
||||
defaultMonth={date?.from}
|
||||
selected={date}
|
||||
onSelect={setDate}
|
||||
numberOfMonths={2}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
129
apps/v4/components/dialog-demo.tsx
Normal file
129
apps/v4/components/dialog-demo.tsx
Normal file
@@ -0,0 +1,129 @@
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/registry/new-york-v4/ui/dialog"
|
||||
import { Input } from "@/registry/new-york-v4/ui/input"
|
||||
import { Label } from "@/registry/new-york-v4/ui/label"
|
||||
|
||||
export function DialogDemo() {
|
||||
return (
|
||||
<div className="flex flex-col items-start gap-4 md:flex-row">
|
||||
<DialogWithForm />
|
||||
<DialogScrollableContent />
|
||||
<DialogWithStickyFooter />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogWithForm() {
|
||||
return (
|
||||
<Dialog>
|
||||
<form>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline">Edit Profile</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Edit profile</DialogTitle>
|
||||
<DialogDescription>
|
||||
Make changes to your profile here. Click save when you're
|
||||
done.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4">
|
||||
<div className="grid gap-3">
|
||||
<Label htmlFor="name-1">Name</Label>
|
||||
<Input id="name-1" name="name" defaultValue="Pedro Duarte" />
|
||||
</div>
|
||||
<div className="grid gap-3">
|
||||
<Label htmlFor="username-1">Username</Label>
|
||||
<Input id="username-1" name="username" defaultValue="@peduarte" />
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<DialogClose asChild>
|
||||
<Button variant="outline">Cancel</Button>
|
||||
</DialogClose>
|
||||
<Button type="submit">Save changes</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</form>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogScrollableContent() {
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline">Scrollable Content</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Scrollable Content</DialogTitle>
|
||||
<DialogDescription>
|
||||
This is a dialog with scrollable content.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="-mx-6 max-h-[500px] overflow-y-auto px-6 text-sm">
|
||||
<h4 className="mb-4 text-lg leading-none font-medium">Lorem Ipsum</h4>
|
||||
{Array.from({ length: 10 }).map((_, index) => (
|
||||
<p key={index} className="mb-4 leading-normal">
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do
|
||||
eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
|
||||
enim ad minim veniam, quis nostrud exercitation ullamco laboris
|
||||
nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in
|
||||
reprehenderit in voluptate velit esse cillum dolore eu fugiat
|
||||
nulla pariatur. Excepteur sint occaecat cupidatat non proident,
|
||||
sunt in culpa qui officia deserunt mollit anim id est laborum.
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogWithStickyFooter() {
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline">Sticky Footer</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Scrollable Content</DialogTitle>
|
||||
<DialogDescription>
|
||||
This is a dialog with scrollable content.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="-mx-6 max-h-[500px] overflow-y-auto px-6 text-sm">
|
||||
<h4 className="mb-4 text-lg leading-none font-medium">Lorem Ipsum</h4>
|
||||
{Array.from({ length: 10 }).map((_, index) => (
|
||||
<p key={index} className="mb-4 leading-normal">
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do
|
||||
eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
|
||||
enim ad minim veniam, quis nostrud exercitation ullamco laboris
|
||||
nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in
|
||||
reprehenderit in voluptate velit esse cillum dolore eu fugiat
|
||||
nulla pariatur. Excepteur sint occaecat cupidatat non proident,
|
||||
sunt in culpa qui officia deserunt mollit anim id est laborum.
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<DialogClose asChild>
|
||||
<Button variant="outline">Close</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
228
apps/v4/components/drawer-demo.tsx
Normal file
228
apps/v4/components/drawer-demo.tsx
Normal file
@@ -0,0 +1,228 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Minus, Plus } from "lucide-react"
|
||||
import { Bar, BarChart, ResponsiveContainer } from "recharts"
|
||||
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import {
|
||||
Drawer,
|
||||
DrawerClose,
|
||||
DrawerContent,
|
||||
DrawerDescription,
|
||||
DrawerFooter,
|
||||
DrawerHeader,
|
||||
DrawerTitle,
|
||||
DrawerTrigger,
|
||||
} from "@/registry/new-york-v4/ui/drawer"
|
||||
|
||||
const data = [
|
||||
{
|
||||
goal: 400,
|
||||
},
|
||||
{
|
||||
goal: 300,
|
||||
},
|
||||
{
|
||||
goal: 200,
|
||||
},
|
||||
{
|
||||
goal: 300,
|
||||
},
|
||||
{
|
||||
goal: 200,
|
||||
},
|
||||
{
|
||||
goal: 278,
|
||||
},
|
||||
{
|
||||
goal: 189,
|
||||
},
|
||||
{
|
||||
goal: 239,
|
||||
},
|
||||
{
|
||||
goal: 300,
|
||||
},
|
||||
{
|
||||
goal: 200,
|
||||
},
|
||||
{
|
||||
goal: 278,
|
||||
},
|
||||
{
|
||||
goal: 189,
|
||||
},
|
||||
{
|
||||
goal: 349,
|
||||
},
|
||||
]
|
||||
|
||||
export function DrawerDemo() {
|
||||
return (
|
||||
<div className="flex flex-wrap items-start gap-4">
|
||||
<DrawerBottom />
|
||||
<DrawerScrollableContent />
|
||||
<DrawerDirections />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function DrawerBottom() {
|
||||
const [goal, setGoal] = React.useState(350)
|
||||
|
||||
const onClick = React.useCallback((adjustment: number) => {
|
||||
setGoal((prevGoal) => Math.max(200, Math.min(400, prevGoal + adjustment)))
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Drawer>
|
||||
<DrawerTrigger asChild>
|
||||
<Button variant="outline">Open Drawer</Button>
|
||||
</DrawerTrigger>
|
||||
<DrawerContent>
|
||||
<div className="mx-auto w-full max-w-sm">
|
||||
<DrawerHeader>
|
||||
<DrawerTitle>Move Goal</DrawerTitle>
|
||||
<DrawerDescription>Set your daily activity goal.</DrawerDescription>
|
||||
</DrawerHeader>
|
||||
<div className="p-4 pb-0">
|
||||
<div className="flex items-center justify-center space-x-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="h-8 w-8 shrink-0 rounded-full"
|
||||
onClick={() => onClick(-10)}
|
||||
disabled={goal <= 200}
|
||||
>
|
||||
<Minus />
|
||||
<span className="sr-only">Decrease</span>
|
||||
</Button>
|
||||
<div className="flex-1 text-center">
|
||||
<div className="text-7xl font-bold tracking-tighter">
|
||||
{goal}
|
||||
</div>
|
||||
<div className="text-muted-foreground text-[0.70rem] uppercase">
|
||||
Calories/day
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="h-8 w-8 shrink-0 rounded-full"
|
||||
onClick={() => onClick(10)}
|
||||
disabled={goal >= 400}
|
||||
>
|
||||
<Plus />
|
||||
<span className="sr-only">Increase</span>
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mt-3 h-[120px]">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart data={data}>
|
||||
<Bar
|
||||
dataKey="goal"
|
||||
style={
|
||||
{
|
||||
fill: "var(--primary)",
|
||||
opacity: 0.9,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
/>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</div>
|
||||
<DrawerFooter>
|
||||
<Button>Submit</Button>
|
||||
<DrawerClose asChild>
|
||||
<Button variant="outline">Cancel</Button>
|
||||
</DrawerClose>
|
||||
</DrawerFooter>
|
||||
</div>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
)
|
||||
}
|
||||
|
||||
function DrawerScrollableContent() {
|
||||
return (
|
||||
<Drawer direction="right">
|
||||
<DrawerTrigger asChild>
|
||||
<Button variant="outline">Scrollable Content</Button>
|
||||
</DrawerTrigger>
|
||||
<DrawerContent>
|
||||
<DrawerHeader>
|
||||
<DrawerTitle>Move Goal</DrawerTitle>
|
||||
<DrawerDescription>Set your daily activity goal.</DrawerDescription>
|
||||
</DrawerHeader>
|
||||
<div className="overflow-y-auto px-4 text-sm">
|
||||
<h4 className="mb-4 text-lg leading-none font-medium">Lorem Ipsum</h4>
|
||||
{Array.from({ length: 10 }).map((_, index) => (
|
||||
<p key={index} className="mb-4 leading-normal">
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do
|
||||
eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
|
||||
enim ad minim veniam, quis nostrud exercitation ullamco laboris
|
||||
nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in
|
||||
reprehenderit in voluptate velit esse cillum dolore eu fugiat
|
||||
nulla pariatur. Excepteur sint occaecat cupidatat non proident,
|
||||
sunt in culpa qui officia deserunt mollit anim id est laborum.
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
<DrawerFooter>
|
||||
<Button>Submit</Button>
|
||||
<DrawerClose asChild>
|
||||
<Button variant="outline">Cancel</Button>
|
||||
</DrawerClose>
|
||||
</DrawerFooter>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
)
|
||||
}
|
||||
|
||||
const directions = ["top", "right", "bottom", "left"] as const
|
||||
|
||||
function DrawerDirections() {
|
||||
return (
|
||||
<>
|
||||
{directions.map((direction) => (
|
||||
<Drawer key={direction} direction={direction}>
|
||||
<DrawerTrigger asChild>
|
||||
<Button variant="outline" className="capitalize">
|
||||
{direction}
|
||||
</Button>
|
||||
</DrawerTrigger>
|
||||
<DrawerContent>
|
||||
<DrawerHeader>
|
||||
<DrawerTitle>Move Goal</DrawerTitle>
|
||||
<DrawerDescription>
|
||||
Set your daily activity goal.
|
||||
</DrawerDescription>
|
||||
</DrawerHeader>
|
||||
<div className="overflow-y-auto px-4 text-sm">
|
||||
{Array.from({ length: 10 }).map((_, index) => (
|
||||
<p key={index} className="mb-4 leading-normal">
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed
|
||||
do eiusmod tempor incididunt ut labore et dolore magna aliqua.
|
||||
Ut enim ad minim veniam, quis nostrud exercitation ullamco
|
||||
laboris nisi ut aliquip ex ea commodo consequat. Duis aute
|
||||
irure dolor in reprehenderit in voluptate velit esse cillum
|
||||
dolore eu fugiat nulla pariatur. Excepteur sint occaecat
|
||||
cupidatat non proident, sunt in culpa qui officia deserunt
|
||||
mollit anim id est laborum.
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
<DrawerFooter>
|
||||
<Button>Submit</Button>
|
||||
<DrawerClose asChild>
|
||||
<Button variant="outline">Cancel</Button>
|
||||
</DrawerClose>
|
||||
</DrawerFooter>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
360
apps/v4/components/dropdown-menu-demo.tsx
Normal file
360
apps/v4/components/dropdown-menu-demo.tsx
Normal file
@@ -0,0 +1,360 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import {
|
||||
BadgeCheckIcon,
|
||||
BellIcon,
|
||||
ChevronsUpDownIcon,
|
||||
CreditCardIcon,
|
||||
LogOut,
|
||||
LogOutIcon,
|
||||
MoreHorizontalIcon,
|
||||
PencilIcon,
|
||||
Settings2Icon,
|
||||
ShareIcon,
|
||||
SparklesIcon,
|
||||
TrashIcon,
|
||||
UserIcon,
|
||||
} from "lucide-react"
|
||||
|
||||
import {
|
||||
Avatar,
|
||||
AvatarFallback,
|
||||
AvatarImage,
|
||||
} from "@/registry/new-york-v4/ui/avatar"
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/registry/new-york-v4/ui/dropdown-menu"
|
||||
|
||||
export function DropdownMenuDemo() {
|
||||
return (
|
||||
<div className="flex flex-wrap items-start gap-4">
|
||||
<DropdownMenuSimple />
|
||||
<DropdownMenuCheckboxes />
|
||||
<DropdownMenuRadioGroupDemo />
|
||||
<DropdownMenuWithAvatar />
|
||||
<DropdownMenuAvatarOnly />
|
||||
<DropdownMenuIconColor />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuSimple() {
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline">Open</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="w-56">
|
||||
<DropdownMenuLabel>My Account</DropdownMenuLabel>
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>
|
||||
Profile
|
||||
<DropdownMenuShortcut>⇧⌘P</DropdownMenuShortcut>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
Billing
|
||||
<DropdownMenuShortcut>⌘B</DropdownMenuShortcut>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
Settings
|
||||
<DropdownMenuShortcut>⌘S</DropdownMenuShortcut>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
Keyboard shortcuts
|
||||
<DropdownMenuShortcut>⌘K</DropdownMenuShortcut>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>Team</DropdownMenuItem>
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger>Invite users</DropdownMenuSubTrigger>
|
||||
<DropdownMenuPortal>
|
||||
<DropdownMenuSubContent>
|
||||
<DropdownMenuItem>Email</DropdownMenuItem>
|
||||
<DropdownMenuItem>Message</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>More...</DropdownMenuItem>
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuPortal>
|
||||
</DropdownMenuSub>
|
||||
<DropdownMenuItem>
|
||||
New Team
|
||||
<DropdownMenuShortcut>⌘+T</DropdownMenuShortcut>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>GitHub</DropdownMenuItem>
|
||||
<DropdownMenuItem>Support</DropdownMenuItem>
|
||||
<DropdownMenuItem disabled>API</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>
|
||||
Log out
|
||||
<DropdownMenuShortcut>⇧⌘Q</DropdownMenuShortcut>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuCheckboxes() {
|
||||
const [showStatusBar, setShowStatusBar] = React.useState(true)
|
||||
const [showActivityBar, setShowActivityBar] = React.useState(false)
|
||||
const [showPanel, setShowPanel] = React.useState(false)
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline">Checkboxes</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="w-56">
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuLabel>Account</DropdownMenuLabel>
|
||||
<DropdownMenuItem>
|
||||
<UserIcon /> Profile
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<CreditCardIcon /> Billing
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<Settings2Icon /> Settings
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuLabel>Appearance</DropdownMenuLabel>
|
||||
<DropdownMenuCheckboxItem
|
||||
checked={showStatusBar}
|
||||
onCheckedChange={setShowStatusBar}
|
||||
>
|
||||
Status Bar
|
||||
</DropdownMenuCheckboxItem>
|
||||
<DropdownMenuCheckboxItem
|
||||
checked={showActivityBar}
|
||||
onCheckedChange={setShowActivityBar}
|
||||
disabled
|
||||
>
|
||||
Activity Bar
|
||||
</DropdownMenuCheckboxItem>
|
||||
<DropdownMenuCheckboxItem
|
||||
checked={showPanel}
|
||||
onCheckedChange={setShowPanel}
|
||||
>
|
||||
Panel
|
||||
</DropdownMenuCheckboxItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>
|
||||
<LogOutIcon /> Sign Out
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuRadioGroupDemo() {
|
||||
const [position, setPosition] = React.useState("bottom")
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline">Radio Group</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="w-56">
|
||||
<DropdownMenuLabel inset>Panel Position</DropdownMenuLabel>
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuRadioGroup value={position} onValueChange={setPosition}>
|
||||
<DropdownMenuRadioItem value="top">Top</DropdownMenuRadioItem>
|
||||
<DropdownMenuRadioItem value="bottom">Bottom</DropdownMenuRadioItem>
|
||||
<DropdownMenuRadioItem value="right" disabled>
|
||||
Right
|
||||
</DropdownMenuRadioItem>
|
||||
</DropdownMenuRadioGroup>
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuWithAvatar() {
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="h-12 justify-start px-2 md:max-w-[200px]"
|
||||
>
|
||||
<Avatar>
|
||||
<AvatarImage src="https://github.com/shadcn.png" alt="Shadcn" />
|
||||
<AvatarFallback className="rounded-lg">CN</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||
<span className="truncate font-semibold">shadcn</span>
|
||||
<span className="text-muted-foreground truncate text-xs">
|
||||
shadcn@example.com
|
||||
</span>
|
||||
</div>
|
||||
<ChevronsUpDownIcon className="text-muted-foreground ml-auto" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
className="w-(--radix-dropdown-menu-trigger-width) min-w-56"
|
||||
align="start"
|
||||
>
|
||||
<DropdownMenuLabel className="p-0 font-normal">
|
||||
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
|
||||
<Avatar>
|
||||
<AvatarImage src="https://github.com/shadcn.png" alt="Shadcn" />
|
||||
<AvatarFallback className="rounded-lg">CN</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||
<span className="truncate font-semibold">shadcn</span>
|
||||
<span className="text-muted-foreground truncate text-xs">
|
||||
shadcn@example.com
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>
|
||||
<SparklesIcon />
|
||||
Upgrade to Pro
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>
|
||||
<BadgeCheckIcon />
|
||||
Account
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<CreditCardIcon />
|
||||
Billing
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<BellIcon />
|
||||
Notifications
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>
|
||||
<LogOut />
|
||||
Sign Out
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuAvatarOnly() {
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="size-8 rounded-full border-none p-0"
|
||||
>
|
||||
<Avatar>
|
||||
<AvatarImage src="https://github.com/leerob.png" alt="leerob" />
|
||||
<AvatarFallback className="rounded-lg">LR</AvatarFallback>
|
||||
</Avatar>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
className="w-(--radix-dropdown-menu-trigger-width) min-w-56"
|
||||
align="start"
|
||||
>
|
||||
<DropdownMenuLabel className="p-0 font-normal">
|
||||
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
|
||||
<Avatar>
|
||||
<AvatarImage src="https://github.com/leerob.png" alt="leerob" />
|
||||
<AvatarFallback className="rounded-lg">LR</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||
<span className="truncate font-semibold">leerob</span>
|
||||
<span className="text-muted-foreground truncate text-xs">
|
||||
leerob@example.com
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>
|
||||
<SparklesIcon />
|
||||
Upgrade to Pro
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>
|
||||
<BadgeCheckIcon />
|
||||
Account
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<CreditCardIcon />
|
||||
Billing
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<BellIcon />
|
||||
Notifications
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>
|
||||
<LogOut />
|
||||
Sign Out
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuIconColor() {
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon">
|
||||
<MoreHorizontalIcon />
|
||||
<span className="sr-only">Toggle menu</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start">
|
||||
<DropdownMenuGroup className="*:data-[slot=dropdown-menu-item]:[&>svg]:text-muted-foreground">
|
||||
<DropdownMenuItem>
|
||||
<PencilIcon />
|
||||
Edit
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<ShareIcon />
|
||||
Share
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem variant="destructive">
|
||||
<TrashIcon />
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
||||
399
apps/v4/components/form-demo.tsx
Normal file
399
apps/v4/components/form-demo.tsx
Normal file
@@ -0,0 +1,399 @@
|
||||
"use client"
|
||||
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { format } from "date-fns"
|
||||
import { CalendarIcon } from "lucide-react"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { toast } from "sonner"
|
||||
import { z } from "zod"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import { Calendar } from "@/registry/new-york-v4/ui/calendar"
|
||||
import { Checkbox } from "@/registry/new-york-v4/ui/checkbox"
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/registry/new-york-v4/ui/form"
|
||||
import { Input } from "@/registry/new-york-v4/ui/input"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/registry/new-york-v4/ui/popover"
|
||||
import {
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
} from "@/registry/new-york-v4/ui/radio-group"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/registry/new-york-v4/ui/select"
|
||||
import { Switch } from "@/registry/new-york-v4/ui/switch"
|
||||
import { Textarea } from "@/registry/new-york-v4/ui/textarea"
|
||||
|
||||
const items = [
|
||||
{
|
||||
id: "recents",
|
||||
label: "Recents",
|
||||
},
|
||||
{
|
||||
id: "home",
|
||||
label: "Home",
|
||||
},
|
||||
{
|
||||
id: "applications",
|
||||
label: "Applications",
|
||||
},
|
||||
{
|
||||
id: "desktop",
|
||||
label: "Desktop",
|
||||
},
|
||||
{
|
||||
id: "downloads",
|
||||
label: "Downloads",
|
||||
},
|
||||
{
|
||||
id: "documents",
|
||||
label: "Documents",
|
||||
},
|
||||
] as const
|
||||
|
||||
const FormSchema = z.object({
|
||||
username: z.string().min(2, {
|
||||
message: "Username must be at least 2 characters.",
|
||||
}),
|
||||
bio: z
|
||||
.string()
|
||||
.min(10, {
|
||||
message: "Bio must be at least 10 characters.",
|
||||
})
|
||||
.max(160, {
|
||||
message: "Bio must not be longer than 30 characters.",
|
||||
}),
|
||||
email: z
|
||||
.string({
|
||||
required_error: "Please select an email to display.",
|
||||
})
|
||||
.email(),
|
||||
type: z.enum(["all", "mentions", "none"], {
|
||||
required_error: "You need to select a notification type.",
|
||||
}),
|
||||
mobile: z.boolean().default(false).optional(),
|
||||
items: z.array(z.string()).refine((value) => value.some((item) => item), {
|
||||
message: "You have to select at least one item.",
|
||||
}),
|
||||
dob: z.date({
|
||||
required_error: "A date of birth is required.",
|
||||
}),
|
||||
marketing_emails: z.boolean().default(false).optional(),
|
||||
security_emails: z.boolean(),
|
||||
})
|
||||
|
||||
export function FormDemo() {
|
||||
const form = useForm<z.infer<typeof FormSchema>>({
|
||||
resolver: zodResolver(FormSchema),
|
||||
defaultValues: {
|
||||
username: "",
|
||||
items: ["recents", "home"],
|
||||
},
|
||||
})
|
||||
|
||||
function onSubmit(data: z.infer<typeof FormSchema>) {
|
||||
toast("You submitted the following values:", {
|
||||
description: (
|
||||
<pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4">
|
||||
<code className="text-white">{JSON.stringify(data, null, 2)}</code>
|
||||
</pre>
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="grid w-full max-w-sm gap-6"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="username"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Username</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="shadcn" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
This is your public display name.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a verified email to display" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="m@example.com">m@example.com</SelectItem>
|
||||
<SelectItem value="m@google.com">m@google.com</SelectItem>
|
||||
<SelectItem value="m@support.com">m@support.com</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormDescription>
|
||||
You can manage email addresses in your email settings.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="bio"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Bio</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
placeholder="Tell us a little bit about yourself"
|
||||
className="resize-none"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
You can <span>@mention</span> other users and organizations.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="type"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col gap-3">
|
||||
<FormLabel>Notify me about...</FormLabel>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value}
|
||||
className="flex flex-col gap-3"
|
||||
>
|
||||
<FormItem className="flex items-center gap-2">
|
||||
<FormControl>
|
||||
<RadioGroupItem value="all" />
|
||||
</FormControl>
|
||||
<FormLabel className="font-normal">
|
||||
All new messages
|
||||
</FormLabel>
|
||||
</FormItem>
|
||||
<FormItem className="flex items-center gap-2">
|
||||
<FormControl>
|
||||
<RadioGroupItem value="mentions" />
|
||||
</FormControl>
|
||||
<FormLabel className="font-normal">
|
||||
Direct messages and mentions
|
||||
</FormLabel>
|
||||
</FormItem>
|
||||
<FormItem className="flex items-center gap-2">
|
||||
<FormControl>
|
||||
<RadioGroupItem value="none" />
|
||||
</FormControl>
|
||||
<FormLabel className="font-normal">Nothing</FormLabel>
|
||||
</FormItem>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="mobile"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-start gap-3 rounded-md border p-4 shadow-xs">
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<div className="flex flex-col gap-1">
|
||||
<FormLabel className="leading-snug">
|
||||
Use different settings for my mobile devices
|
||||
</FormLabel>
|
||||
<FormDescription className="leading-snug">
|
||||
You can manage your mobile notifications in the mobile
|
||||
settings page.
|
||||
</FormDescription>
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="items"
|
||||
render={() => (
|
||||
<FormItem className="flex flex-col gap-4">
|
||||
<div>
|
||||
<FormLabel className="text-base">Sidebar</FormLabel>
|
||||
<FormDescription>
|
||||
Select the items you want to display in the sidebar.
|
||||
</FormDescription>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
{items.map((item) => (
|
||||
<FormField
|
||||
key={item.id}
|
||||
control={form.control}
|
||||
name="items"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem
|
||||
key={item.id}
|
||||
className="flex items-start gap-3"
|
||||
>
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
checked={field.value?.includes(item.id)}
|
||||
onCheckedChange={(checked) => {
|
||||
return checked
|
||||
? field.onChange([...field.value, item.id])
|
||||
: field.onChange(
|
||||
field.value?.filter(
|
||||
(value) => value !== item.id
|
||||
)
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormLabel className="text-sm leading-tight font-normal">
|
||||
{item.label}
|
||||
</FormLabel>
|
||||
</FormItem>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="dob"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col">
|
||||
<FormLabel>Date of birth</FormLabel>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<FormControl>
|
||||
<Button
|
||||
variant={"outline"}
|
||||
className={cn(
|
||||
"w-[240px] pl-3 text-left font-normal",
|
||||
!field.value && "text-muted-foreground"
|
||||
)}
|
||||
>
|
||||
{field.value ? (
|
||||
format(field.value, "PPP")
|
||||
) : (
|
||||
<span>Pick a date</span>
|
||||
)}
|
||||
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
|
||||
</Button>
|
||||
</FormControl>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0" align="start">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={field.value}
|
||||
onSelect={field.onChange}
|
||||
disabled={(date) =>
|
||||
date > new Date() || date < new Date("1900-01-01")
|
||||
}
|
||||
initialFocus
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<FormDescription>
|
||||
Your date of birth is used to calculate your age.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div>
|
||||
<h3 className="mb-4 text-lg font-medium">Email Notifications</h3>
|
||||
<div className="flex flex-col gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="marketing_emails"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-start justify-between rounded-lg border p-4 shadow-xs">
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<FormLabel className="leading-normal">
|
||||
Marketing emails
|
||||
</FormLabel>
|
||||
<FormDescription className="leading-snug">
|
||||
Receive emails about new products, features, and more.
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="security_emails"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-start justify-between rounded-lg border p-4 shadow-xs">
|
||||
<div className="flex flex-col gap-0.5 opacity-60">
|
||||
<FormLabel className="leading-normal">
|
||||
Security emails
|
||||
</FormLabel>
|
||||
<FormDescription className="leading-snug">
|
||||
Receive emails about your account security.
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
disabled
|
||||
aria-readonly
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Button type="submit">Submit</Button>
|
||||
</form>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
234
apps/v4/components/forms-demo.tsx
Normal file
234
apps/v4/components/forms-demo.tsx
Normal file
@@ -0,0 +1,234 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { useTheme } from "next-themes"
|
||||
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/new-york-v4/ui/card"
|
||||
import { Checkbox } from "@/registry/new-york-v4/ui/checkbox"
|
||||
import { Input } from "@/registry/new-york-v4/ui/input"
|
||||
import { Label } from "@/registry/new-york-v4/ui/label"
|
||||
import {
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
} from "@/registry/new-york-v4/ui/radio-group"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/registry/new-york-v4/ui/select"
|
||||
import { Textarea } from "@/registry/new-york-v4/ui/textarea"
|
||||
|
||||
const plans = [
|
||||
{
|
||||
id: "starter",
|
||||
name: "Starter Plan",
|
||||
description: "Perfect for small businesses.",
|
||||
price: "$10",
|
||||
},
|
||||
{
|
||||
id: "pro",
|
||||
name: "Pro Plan",
|
||||
description: "Advanced features with more storage.",
|
||||
price: "$20",
|
||||
},
|
||||
] as const
|
||||
|
||||
const themes = {
|
||||
neutral: {
|
||||
light: {
|
||||
"--primary": "oklch(0.205 0 0)",
|
||||
"--primary-foreground": "oklch(0.985 0 0)",
|
||||
"--ring": "oklch(0.708 0 0)",
|
||||
},
|
||||
dark: {
|
||||
"--primary": "oklch(0.922 0 0)",
|
||||
"--primary-foreground": "oklch(0.205 0 0)",
|
||||
"--ring": "oklch(0.556 0 0)",
|
||||
},
|
||||
},
|
||||
blue: {
|
||||
light: {
|
||||
"--primary": "oklch(0.546 0.245 262.881)",
|
||||
"--primary-foreground": "oklch(0.985 0.001 106.423)",
|
||||
"--ring": "oklch(0.546 0.245 262.881)",
|
||||
},
|
||||
dark: {
|
||||
"--primary": "oklch(0.623 0.214 259.815)",
|
||||
"--primary-foreground": "oklch(0.985 0.001 106.423)",
|
||||
"--ring": "oklch(0.623 0.214 259.815)",
|
||||
},
|
||||
},
|
||||
amber: {
|
||||
light: {
|
||||
"--primary": "oklch(0.769 0.188 70.08)",
|
||||
"--primary-foreground": "oklch(0.985 0.001 106.423)",
|
||||
"--ring": "oklch(0.82 0.13 92.25)",
|
||||
},
|
||||
dark: {
|
||||
"--primary": "oklch(0.769 0.188 70.08)",
|
||||
"--primary-foreground": "oklch(0.216 0.006 56.043)",
|
||||
"--ring": "oklch(0.666 0.179 58.318)",
|
||||
},
|
||||
},
|
||||
teal: {
|
||||
light: {
|
||||
"--primary": "oklch(0.627 0.194 149.214)",
|
||||
"--primary-foreground": "oklch(0.985 0.001 106.423)",
|
||||
"--ring": "oklch(0.79 0.19 153.13)",
|
||||
},
|
||||
dark: {
|
||||
"--primary": "oklch(0.704 0.14 182.503)",
|
||||
"--primary-foreground": "oklch(0.216 0.006 56.043)",
|
||||
"--ring": "oklch(0.704 0.14 182.503)",
|
||||
},
|
||||
},
|
||||
} as const
|
||||
|
||||
export function FormsDemo() {
|
||||
const { theme: mode = "light" } = useTheme()
|
||||
const [theme, setTheme] = React.useState<keyof typeof themes | undefined>(
|
||||
undefined
|
||||
)
|
||||
|
||||
const themeStyles = React.useMemo(() => {
|
||||
if (!theme) return undefined
|
||||
return themes[theme][mode as keyof (typeof themes)[typeof theme]]
|
||||
}, [theme, mode])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex max-w-md flex-col gap-4">
|
||||
<Card style={themeStyles as React.CSSProperties}>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">Upgrade your subscription</CardTitle>
|
||||
<CardDescription className="text-balance">
|
||||
You are currently on the free plan. Upgrade to the pro plan to get
|
||||
access to all features.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex flex-col gap-3 md:flex-row">
|
||||
<div className="flex flex-1 flex-col gap-2">
|
||||
<Label htmlFor="name">Name</Label>
|
||||
<Input id="name" placeholder="Evil Rabbit" />
|
||||
</div>
|
||||
<div className="flex flex-1 flex-col gap-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input id="email" placeholder="example@acme.com" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="card-number">Card Number</Label>
|
||||
<div className="grid grid-cols-2 gap-3 md:grid-cols-[1fr_80px_60px]">
|
||||
<Input
|
||||
id="card-number"
|
||||
placeholder="1234 1234 1234 1234"
|
||||
className="col-span-2 md:col-span-1"
|
||||
/>
|
||||
<Input id="card-number-expiry" placeholder="MM/YY" />
|
||||
<Input id="card-number-cvc" placeholder="CVC" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="color">Color</Label>
|
||||
<Select
|
||||
onValueChange={(value) =>
|
||||
setTheme(value as keyof typeof themes)
|
||||
}
|
||||
>
|
||||
<SelectTrigger id="color" className="w-full capitalize">
|
||||
<SelectValue placeholder="Select a color" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{Object.keys(themes).map((theme) => (
|
||||
<SelectItem
|
||||
key={theme}
|
||||
value={theme}
|
||||
className="capitalize"
|
||||
>
|
||||
<div
|
||||
className="size-3.5 rounded-full border"
|
||||
style={{
|
||||
backgroundColor:
|
||||
themes[theme as keyof typeof themes][
|
||||
mode as keyof (typeof themes)[keyof typeof themes]
|
||||
]?.["--primary"],
|
||||
}}
|
||||
/>
|
||||
{theme}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<fieldset className="flex flex-col gap-3">
|
||||
<legend className="text-sm font-medium">Plan</legend>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Select the plan that best fits your needs.
|
||||
</p>
|
||||
<RadioGroup
|
||||
defaultValue="starter"
|
||||
className="grid gap-3 md:grid-cols-2"
|
||||
>
|
||||
{plans.map((plan) => (
|
||||
<Label
|
||||
className="has-[[data-state=checked]]:border-ring has-[[data-state=checked]]:bg-input/30 flex items-start gap-3 rounded-lg border p-3"
|
||||
key={plan.id}
|
||||
>
|
||||
<RadioGroupItem
|
||||
value={plan.id}
|
||||
id={plan.name}
|
||||
className="data-[state=checked]:border-primary"
|
||||
/>
|
||||
<div className="grid gap-1 font-normal">
|
||||
<div className="font-medium">{plan.name}</div>
|
||||
<div className="text-muted-foreground pr-2 text-xs leading-snug text-balance">
|
||||
{plan.description}
|
||||
</div>
|
||||
</div>
|
||||
</Label>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</fieldset>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="notes">Notes</Label>
|
||||
<Textarea id="notes" placeholder="Enter notes" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox id="terms" />
|
||||
<Label htmlFor="terms" className="font-normal">
|
||||
I agree to the terms and conditions
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox id="newsletter" defaultChecked />
|
||||
<Label htmlFor="newsletter" className="font-normal">
|
||||
Allow us to send you emails
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter className="flex justify-between">
|
||||
<Button variant="outline" size="sm">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button size="sm">Upgrade Plan</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
43
apps/v4/components/hover-card-demo.tsx
Normal file
43
apps/v4/components/hover-card-demo.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import { CalendarIcon } from "lucide-react"
|
||||
|
||||
import {
|
||||
Avatar,
|
||||
AvatarFallback,
|
||||
AvatarImage,
|
||||
} from "@/registry/new-york-v4/ui/avatar"
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import {
|
||||
HoverCard,
|
||||
HoverCardContent,
|
||||
HoverCardTrigger,
|
||||
} from "@/registry/new-york-v4/ui/hover-card"
|
||||
|
||||
export function HoverCardDemo() {
|
||||
return (
|
||||
<HoverCard>
|
||||
<HoverCardTrigger asChild>
|
||||
<Button variant="link">@nextjs</Button>
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent className="w-80" side="right">
|
||||
<div className="flex justify-between gap-4">
|
||||
<Avatar>
|
||||
<AvatarImage src="https://github.com/vercel.png" />
|
||||
<AvatarFallback>VC</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="flex flex-col gap-1">
|
||||
<h4 className="text-sm font-semibold">@nextjs</h4>
|
||||
<p className="text-sm">
|
||||
The React Framework – created and maintained by @vercel.
|
||||
</p>
|
||||
<div className="mt-1 flex items-center gap-2">
|
||||
<CalendarIcon className="text-muted-foreground size-4" />{" "}
|
||||
<span className="text-muted-foreground text-xs">
|
||||
Joined December 2021
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</HoverCardContent>
|
||||
</HoverCard>
|
||||
)
|
||||
}
|
||||
23
apps/v4/components/input-demo.tsx
Normal file
23
apps/v4/components/input-demo.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { Input } from "@/registry/new-york-v4/ui/input"
|
||||
|
||||
export function InputDemo() {
|
||||
return (
|
||||
<div className="flex flex-col flex-wrap gap-4 md:flex-row">
|
||||
<Input type="email" placeholder="Email" />
|
||||
<Input type="text" placeholder="Error" aria-invalid="true" />
|
||||
<Input type="password" placeholder="Password" />
|
||||
<Input type="number" placeholder="Number" />
|
||||
<Input type="file" placeholder="File" />
|
||||
<Input type="tel" placeholder="Tel" />
|
||||
<Input type="text" placeholder="Text" />
|
||||
<Input type="url" placeholder="URL" />
|
||||
<Input type="search" placeholder="Search" />
|
||||
<Input type="date" placeholder="Date" />
|
||||
<Input type="datetime-local" placeholder="Datetime Local" />
|
||||
<Input type="month" placeholder="Month" />
|
||||
<Input type="time" placeholder="Time" />
|
||||
<Input type="week" placeholder="Week" />
|
||||
<Input disabled placeholder="Disabled" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
109
apps/v4/components/input-otp-demo.tsx
Normal file
109
apps/v4/components/input-otp-demo.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { REGEXP_ONLY_DIGITS } from "input-otp"
|
||||
|
||||
import {
|
||||
InputOTP,
|
||||
InputOTPGroup,
|
||||
InputOTPSeparator,
|
||||
InputOTPSlot,
|
||||
} from "@/registry/new-york-v4/ui/input-otp"
|
||||
import { Label } from "@/registry/new-york-v4/ui/label"
|
||||
|
||||
export function InputOTPDemo() {
|
||||
return (
|
||||
<div className="flex flex-col flex-wrap gap-6 md:flex-row">
|
||||
<InputOTPSimple />
|
||||
<InputOTPPattern />
|
||||
<InputOTPWithSeparator />
|
||||
<InputOTPWithSpacing />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function InputOTPSimple() {
|
||||
return (
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="simple">Simple</Label>
|
||||
<InputOTP id="simple" maxLength={6}>
|
||||
<InputOTPGroup>
|
||||
<InputOTPSlot index={0} />
|
||||
<InputOTPSlot index={1} />
|
||||
<InputOTPSlot index={2} />
|
||||
</InputOTPGroup>
|
||||
<InputOTPSeparator />
|
||||
<InputOTPGroup>
|
||||
<InputOTPSlot index={3} />
|
||||
<InputOTPSlot index={4} />
|
||||
<InputOTPSlot index={5} />
|
||||
</InputOTPGroup>
|
||||
</InputOTP>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function InputOTPPattern() {
|
||||
return (
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="digits-only">Digits Only</Label>
|
||||
<InputOTP id="digits-only" maxLength={6} pattern={REGEXP_ONLY_DIGITS}>
|
||||
<InputOTPGroup>
|
||||
<InputOTPSlot index={0} />
|
||||
<InputOTPSlot index={1} />
|
||||
<InputOTPSlot index={2} />
|
||||
<InputOTPSlot index={3} />
|
||||
<InputOTPSlot index={4} />
|
||||
<InputOTPSlot index={5} />
|
||||
</InputOTPGroup>
|
||||
</InputOTP>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function InputOTPWithSeparator() {
|
||||
const [value, setValue] = React.useState("123456")
|
||||
|
||||
return (
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="with-separator">With Separator</Label>
|
||||
<InputOTP
|
||||
id="with-separator"
|
||||
maxLength={6}
|
||||
value={value}
|
||||
onChange={setValue}
|
||||
>
|
||||
<InputOTPGroup>
|
||||
<InputOTPSlot index={0} />
|
||||
<InputOTPSlot index={1} />
|
||||
</InputOTPGroup>
|
||||
<InputOTPSeparator />
|
||||
<InputOTPGroup>
|
||||
<InputOTPSlot index={2} />
|
||||
<InputOTPSlot index={3} />
|
||||
</InputOTPGroup>
|
||||
<InputOTPSeparator />
|
||||
<InputOTPGroup>
|
||||
<InputOTPSlot index={4} />
|
||||
<InputOTPSlot index={5} />
|
||||
</InputOTPGroup>
|
||||
</InputOTP>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function InputOTPWithSpacing() {
|
||||
return (
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="with-spacing">With Spacing</Label>
|
||||
<InputOTP id="with-spacing" maxLength={6}>
|
||||
<InputOTPGroup className="gap-2 *:data-[slot=input-otp-slot]:rounded-md *:data-[slot=input-otp-slot]:border">
|
||||
<InputOTPSlot index={0} aria-invalid="true" />
|
||||
<InputOTPSlot index={1} aria-invalid="true" />
|
||||
<InputOTPSlot index={2} aria-invalid="true" />
|
||||
<InputOTPSlot index={3} aria-invalid="true" />
|
||||
</InputOTPGroup>
|
||||
</InputOTP>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
27
apps/v4/components/label-demo.tsx
Normal file
27
apps/v4/components/label-demo.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import { Checkbox } from "@/registry/new-york-v4/ui/checkbox"
|
||||
import { Input } from "@/registry/new-york-v4/ui/input"
|
||||
import { Label } from "@/registry/new-york-v4/ui/label"
|
||||
import { Textarea } from "@/registry/new-york-v4/ui/textarea"
|
||||
|
||||
export function LabelDemo() {
|
||||
return (
|
||||
<div className="grid w-full max-w-sm gap-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<Checkbox id="label-demo-terms" />
|
||||
<Label htmlFor="label-demo-terms">Accept terms and conditions</Label>
|
||||
</div>
|
||||
<div className="grid gap-3">
|
||||
<Label htmlFor="label-demo-username">Username</Label>
|
||||
<Input id="label-demo-username" placeholder="Username" />
|
||||
</div>
|
||||
<div className="group grid gap-3" data-disabled={true}>
|
||||
<Label htmlFor="label-demo-disabled">Disabled</Label>
|
||||
<Input id="label-demo-disabled" placeholder="Disabled" disabled />
|
||||
</div>
|
||||
<div className="grid gap-3">
|
||||
<Label htmlFor="label-demo-message">Message</Label>
|
||||
<Textarea id="label-demo-message" placeholder="Message" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
112
apps/v4/components/login-form.tsx
Normal file
112
apps/v4/components/login-form.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
import Image from "next/image"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import { Card, CardContent } from "@/registry/new-york-v4/ui/card"
|
||||
import { Input } from "@/registry/new-york-v4/ui/input"
|
||||
import { Label } from "@/registry/new-york-v4/ui/label"
|
||||
|
||||
export function LoginForm({
|
||||
className,
|
||||
imageUrl,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & {
|
||||
imageUrl?: string
|
||||
}) {
|
||||
return (
|
||||
<div className={cn("flex flex-col gap-6", className)} {...props}>
|
||||
<Card className="overflow-hidden p-0">
|
||||
<CardContent className="grid p-0 md:grid-cols-2">
|
||||
<form className="p-6 md:p-8">
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex flex-col items-center text-center">
|
||||
<h1 className="text-2xl font-bold">Welcome back</h1>
|
||||
<p className="text-muted-foreground text-balance">
|
||||
Login to your Acme Inc account
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid gap-3">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="m@example.com"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-3">
|
||||
<div className="flex items-center">
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<a
|
||||
href="#"
|
||||
className="ml-auto text-sm underline-offset-2 hover:underline"
|
||||
>
|
||||
Forgot your password?
|
||||
</a>
|
||||
</div>
|
||||
<Input id="password" type="password" required />
|
||||
</div>
|
||||
<Button type="submit" className="w-full">
|
||||
Login
|
||||
</Button>
|
||||
<div className="after:border-border relative text-center text-sm after:absolute after:inset-0 after:top-1/2 after:z-0 after:flex after:items-center after:border-t">
|
||||
<span className="bg-background text-muted-foreground relative z-10 px-2">
|
||||
Or continue with
|
||||
</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<Button variant="outline" type="button" className="w-full">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M12.152 6.896c-.948 0-2.415-1.078-3.96-1.04-2.04.027-3.91 1.183-4.961 3.014-2.117 3.675-.546 9.103 1.519 12.09 1.013 1.454 2.208 3.09 3.792 3.039 1.52-.065 2.09-.987 3.935-.987 1.831 0 2.35.987 3.96.948 1.637-.026 2.676-1.48 3.676-2.948 1.156-1.688 1.636-3.325 1.662-3.415-.039-.013-3.182-1.221-3.22-4.857-.026-3.04 2.48-4.494 2.597-4.559-1.429-2.09-3.623-2.324-4.39-2.376-2-.156-3.675 1.09-4.61 1.09zM15.53 3.83c.843-1.012 1.4-2.427 1.245-3.83-1.207.052-2.662.805-3.532 1.818-.78.896-1.454 2.338-1.273 3.714 1.338.104 2.715-.688 3.559-1.701"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
<span className="sr-only">Login with Apple</span>
|
||||
</Button>
|
||||
<Button variant="outline" type="button" className="w-full">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
<span className="sr-only">Login with Google</span>
|
||||
</Button>
|
||||
<Button variant="outline" type="button" className="w-full">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M6.915 4.03c-1.968 0-3.683 1.28-4.871 3.113C.704 9.208 0 11.883 0 14.449c0 .706.07 1.369.21 1.973a6.624 6.624 0 0 0 .265.86 5.297 5.297 0 0 0 .371.761c.696 1.159 1.818 1.927 3.593 1.927 1.497 0 2.633-.671 3.965-2.444.76-1.012 1.144-1.626 2.663-4.32l.756-1.339.186-.325c.061.1.121.196.183.3l2.152 3.595c.724 1.21 1.665 2.556 2.47 3.314 1.046.987 1.992 1.22 3.06 1.22 1.075 0 1.876-.355 2.455-.843a3.743 3.743 0 0 0 .81-.973c.542-.939.861-2.127.861-3.745 0-2.72-.681-5.357-2.084-7.45-1.282-1.912-2.957-2.93-4.716-2.93-1.047 0-2.088.467-3.053 1.308-.652.57-1.257 1.29-1.82 2.05-.69-.875-1.335-1.547-1.958-2.056-1.182-.966-2.315-1.303-3.454-1.303zm10.16 2.053c1.147 0 2.188.758 2.992 1.999 1.132 1.748 1.647 4.195 1.647 6.4 0 1.548-.368 2.9-1.839 2.9-.58 0-1.027-.23-1.664-1.004-.496-.601-1.343-1.878-2.832-4.358l-.617-1.028a44.908 44.908 0 0 0-1.255-1.98c.07-.109.141-.224.211-.327 1.12-1.667 2.118-2.602 3.358-2.602zm-10.201.553c1.265 0 2.058.791 2.675 1.446.307.327.737.871 1.234 1.579l-1.02 1.566c-.757 1.163-1.882 3.017-2.837 4.338-1.191 1.649-1.81 1.817-2.486 1.817-.524 0-1.038-.237-1.383-.794-.263-.426-.464-1.13-.464-2.046 0-2.221.63-4.535 1.66-6.088.454-.687.964-1.226 1.533-1.533a2.264 2.264 0 0 1 1.088-.285z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
<span className="sr-only">Login with Meta</span>
|
||||
</Button>
|
||||
</div>
|
||||
<div className="text-center text-sm">
|
||||
Don't have an account?{" "}
|
||||
<a href="#" className="underline underline-offset-4">
|
||||
Sign up
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div className="bg-primary/50 relative hidden md:block">
|
||||
{imageUrl && (
|
||||
<Image
|
||||
fill
|
||||
src={imageUrl}
|
||||
alt="Image"
|
||||
className="absolute inset-0 h-full w-full object-cover"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div className="text-muted-foreground *:[a]:hover:text-primary text-center text-xs text-balance *:[a]:underline *:[a]:underline-offset-4">
|
||||
By clicking continue, you agree to our <a href="#">Terms of Service</a>{" "}
|
||||
and <a href="#">Privacy Policy</a>.
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
130
apps/v4/components/menubar-demo.tsx
Normal file
130
apps/v4/components/menubar-demo.tsx
Normal file
@@ -0,0 +1,130 @@
|
||||
import { HelpCircleIcon, SettingsIcon, Trash2Icon } from "lucide-react"
|
||||
|
||||
import {
|
||||
Menubar,
|
||||
MenubarCheckboxItem,
|
||||
MenubarContent,
|
||||
MenubarGroup,
|
||||
MenubarItem,
|
||||
MenubarMenu,
|
||||
MenubarRadioGroup,
|
||||
MenubarRadioItem,
|
||||
MenubarSeparator,
|
||||
MenubarShortcut,
|
||||
MenubarSub,
|
||||
MenubarSubContent,
|
||||
MenubarSubTrigger,
|
||||
MenubarTrigger,
|
||||
} from "@/registry/new-york-v4/ui/menubar"
|
||||
|
||||
export function MenubarDemo() {
|
||||
return (
|
||||
<Menubar>
|
||||
<MenubarMenu>
|
||||
<MenubarTrigger>File</MenubarTrigger>
|
||||
<MenubarContent>
|
||||
<MenubarItem>
|
||||
New Tab <MenubarShortcut>⌘T</MenubarShortcut>
|
||||
</MenubarItem>
|
||||
<MenubarItem>
|
||||
New Window <MenubarShortcut>⌘N</MenubarShortcut>
|
||||
</MenubarItem>
|
||||
<MenubarItem disabled>New Incognito Window</MenubarItem>
|
||||
<MenubarSeparator />
|
||||
<MenubarSub>
|
||||
<MenubarSubTrigger>Share</MenubarSubTrigger>
|
||||
<MenubarSubContent>
|
||||
<MenubarItem>Email link</MenubarItem>
|
||||
<MenubarItem>Messages</MenubarItem>
|
||||
<MenubarItem>Notes</MenubarItem>
|
||||
</MenubarSubContent>
|
||||
</MenubarSub>
|
||||
<MenubarSeparator />
|
||||
<MenubarItem>
|
||||
Print... <MenubarShortcut>⌘P</MenubarShortcut>
|
||||
</MenubarItem>
|
||||
</MenubarContent>
|
||||
</MenubarMenu>
|
||||
<MenubarMenu>
|
||||
<MenubarTrigger>Edit</MenubarTrigger>
|
||||
<MenubarContent>
|
||||
<MenubarItem>
|
||||
Undo <MenubarShortcut>⌘Z</MenubarShortcut>
|
||||
</MenubarItem>
|
||||
<MenubarItem>
|
||||
Redo <MenubarShortcut>⇧⌘Z</MenubarShortcut>
|
||||
</MenubarItem>
|
||||
<MenubarSeparator />
|
||||
<MenubarSub>
|
||||
<MenubarSubTrigger>Find</MenubarSubTrigger>
|
||||
<MenubarSubContent>
|
||||
<MenubarItem>Search the web</MenubarItem>
|
||||
<MenubarSeparator />
|
||||
<MenubarItem>Find...</MenubarItem>
|
||||
<MenubarItem>Find Next</MenubarItem>
|
||||
<MenubarItem>Find Previous</MenubarItem>
|
||||
</MenubarSubContent>
|
||||
</MenubarSub>
|
||||
<MenubarSeparator />
|
||||
<MenubarItem>Cut</MenubarItem>
|
||||
<MenubarItem>Copy</MenubarItem>
|
||||
<MenubarItem>Paste</MenubarItem>
|
||||
</MenubarContent>
|
||||
</MenubarMenu>
|
||||
<MenubarMenu>
|
||||
<MenubarTrigger>View</MenubarTrigger>
|
||||
<MenubarContent>
|
||||
<MenubarCheckboxItem>Always Show Bookmarks Bar</MenubarCheckboxItem>
|
||||
<MenubarCheckboxItem checked>
|
||||
Always Show Full URLs
|
||||
</MenubarCheckboxItem>
|
||||
<MenubarSeparator />
|
||||
<MenubarItem inset>
|
||||
Reload <MenubarShortcut>⌘R</MenubarShortcut>
|
||||
</MenubarItem>
|
||||
<MenubarItem disabled inset>
|
||||
Force Reload <MenubarShortcut>⇧⌘R</MenubarShortcut>
|
||||
</MenubarItem>
|
||||
<MenubarSeparator />
|
||||
<MenubarItem inset>Toggle Fullscreen</MenubarItem>
|
||||
<MenubarSeparator />
|
||||
<MenubarItem inset>Hide Sidebar</MenubarItem>
|
||||
</MenubarContent>
|
||||
</MenubarMenu>
|
||||
<MenubarMenu>
|
||||
<MenubarTrigger>Profiles</MenubarTrigger>
|
||||
<MenubarContent>
|
||||
<MenubarRadioGroup value="benoit">
|
||||
<MenubarRadioItem value="andy">Andy</MenubarRadioItem>
|
||||
<MenubarRadioItem value="benoit">Benoit</MenubarRadioItem>
|
||||
<MenubarRadioItem value="Luis">Luis</MenubarRadioItem>
|
||||
</MenubarRadioGroup>
|
||||
<MenubarSeparator />
|
||||
<MenubarItem inset>Edit...</MenubarItem>
|
||||
<MenubarSeparator />
|
||||
<MenubarItem inset>Add Profile...</MenubarItem>
|
||||
</MenubarContent>
|
||||
</MenubarMenu>
|
||||
<MenubarMenu>
|
||||
<MenubarTrigger>More</MenubarTrigger>
|
||||
<MenubarContent>
|
||||
<MenubarGroup>
|
||||
<MenubarItem>
|
||||
<SettingsIcon />
|
||||
Settings
|
||||
</MenubarItem>
|
||||
<MenubarItem>
|
||||
<HelpCircleIcon />
|
||||
Help
|
||||
</MenubarItem>
|
||||
<MenubarSeparator />
|
||||
<MenubarItem variant="destructive">
|
||||
<Trash2Icon />
|
||||
Delete
|
||||
</MenubarItem>
|
||||
</MenubarGroup>
|
||||
</MenubarContent>
|
||||
</MenubarMenu>
|
||||
</Menubar>
|
||||
)
|
||||
}
|
||||
35
apps/v4/components/mode-switcher.tsx
Normal file
35
apps/v4/components/mode-switcher.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { MoonIcon, SunIcon } from "lucide-react"
|
||||
import { useTheme } from "next-themes"
|
||||
|
||||
import { META_THEME_COLORS, useMetaColor } from "@/hooks/use-meta-color"
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
|
||||
export function ModeSwitcher() {
|
||||
const { setTheme, resolvedTheme } = useTheme()
|
||||
const { setMetaColor } = useMetaColor()
|
||||
|
||||
const toggleTheme = React.useCallback(() => {
|
||||
setTheme(resolvedTheme === "dark" ? "light" : "dark")
|
||||
setMetaColor(
|
||||
resolvedTheme === "dark"
|
||||
? META_THEME_COLORS.light
|
||||
: META_THEME_COLORS.dark
|
||||
)
|
||||
}, [resolvedTheme, setTheme, setMetaColor])
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="group/toggle size-8"
|
||||
onClick={toggleTheme}
|
||||
>
|
||||
<SunIcon className="hidden [html.dark_&]:block" />
|
||||
<MoonIcon className="hidden [html.light_&]:block" />
|
||||
<span className="sr-only">Toggle theme</span>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user