mirror of
https://github.com/shadcn-ui/ui.git
synced 2026-06-12 10:21:39 +00:00
Compare commits
146 Commits
shadcn@3.3
...
shadcn/req
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
84eb464ae1 | ||
|
|
7c0618bf43 | ||
|
|
854641cea1 | ||
|
|
3a72007f61 | ||
|
|
6b53b238fb | ||
|
|
b398fea304 | ||
|
|
f22174a77f | ||
|
|
c9a39f1007 | ||
|
|
a8ad21f81f | ||
|
|
504503c638 | ||
|
|
f8df5c95cb | ||
|
|
2bfc1c82ba | ||
|
|
84bd724d97 | ||
|
|
39fdf94550 | ||
|
|
08479cc3db | ||
|
|
02d5ce85ec | ||
|
|
c0329c86b9 | ||
|
|
3b1491f908 | ||
|
|
ca4c1c43ec | ||
|
|
1e840eb53c | ||
|
|
96ac92e63f | ||
|
|
e11546e692 | ||
|
|
0b4d62f95c | ||
|
|
dae80dad65 | ||
|
|
abc09809e8 | ||
|
|
8a40fe0ead | ||
|
|
b3ab304a00 | ||
|
|
bb45fd83c3 | ||
|
|
84678ee1c0 | ||
|
|
33ffb0419c | ||
|
|
a2f6c031e2 | ||
|
|
ac098d8cf0 | ||
|
|
8160610410 | ||
|
|
c7901e3a41 | ||
|
|
d73ac361b3 | ||
|
|
ebad2901ce | ||
|
|
4f617d59b8 | ||
|
|
ed0e103bd6 | ||
|
|
9cab0c9b18 | ||
|
|
d80e084814 | ||
|
|
efcf9728c2 | ||
|
|
8835bacc8b | ||
|
|
f2556d2386 | ||
|
|
75a0000075 | ||
|
|
ac306c60f5 | ||
|
|
5e2ef1f8bd | ||
|
|
7d9b8aefff | ||
|
|
58208e3802 | ||
|
|
a16a77446a | ||
|
|
39032bb390 | ||
|
|
d7e0dc3ec8 | ||
|
|
6bddba986d | ||
|
|
b70059b25b | ||
|
|
37bc2eec1f | ||
|
|
bb048fb532 | ||
|
|
9c373dbd27 | ||
|
|
d75b092c61 | ||
|
|
be49662bf5 | ||
|
|
b2b2e3fc98 | ||
|
|
188b746074 | ||
|
|
6f093a0f3f | ||
|
|
f18f1eaff7 | ||
|
|
9ac1b5c0a5 | ||
|
|
f63b70b413 | ||
|
|
54e725d986 | ||
|
|
62dbad36bb | ||
|
|
a707424fa2 | ||
|
|
e2bfa6bd85 | ||
|
|
6292464d90 | ||
|
|
6617167d6f | ||
|
|
ca28857d40 | ||
|
|
343bc941b1 | ||
|
|
c9311f26fa | ||
|
|
4e0871f426 | ||
|
|
cb769b7059 | ||
|
|
93037dca94 | ||
|
|
ed9d5939e6 | ||
|
|
b52ec12f1e | ||
|
|
2ab9bff4bb | ||
|
|
2f6b51fa0a | ||
|
|
8a4764ed91 | ||
|
|
e934d4645b | ||
|
|
08b8e499d8 | ||
|
|
69402b3579 | ||
|
|
679c852254 | ||
|
|
d478412e44 | ||
|
|
d5c8a25150 | ||
|
|
26433a651c | ||
|
|
c3da716e94 | ||
|
|
b2572d0287 | ||
|
|
b83f042416 | ||
|
|
6567897393 | ||
|
|
2675fa3941 | ||
|
|
fbda67c88c | ||
|
|
e8674ee848 | ||
|
|
adb66f4d43 | ||
|
|
3afb46eaf6 | ||
|
|
7cd019ad36 | ||
|
|
bea7d30536 | ||
|
|
40c3ff513a | ||
|
|
89ebfdce47 | ||
|
|
b83023034a | ||
|
|
6a534d7954 | ||
|
|
ef1987ded9 | ||
|
|
77bf7d28b4 | ||
|
|
41f4f7357d | ||
|
|
bc99818e04 | ||
|
|
162ba7b13c | ||
|
|
f12db1e3a2 | ||
|
|
ce3e2b1df8 | ||
|
|
dcfe911b33 | ||
|
|
7210a4919a | ||
|
|
d198908510 | ||
|
|
b0b1cd1f0d | ||
|
|
f3d70724b6 | ||
|
|
407e9c6802 | ||
|
|
c67e630521 | ||
|
|
f494411953 | ||
|
|
a43c1d1342 | ||
|
|
607a6fd127 | ||
|
|
fbcc665b49 | ||
|
|
7ddcf31e43 | ||
|
|
3e39163b08 | ||
|
|
e311fdae04 | ||
|
|
26640d9d88 | ||
|
|
3e20c228da | ||
|
|
0810c0e1a2 | ||
|
|
1205ea5445 | ||
|
|
4430ab8bab | ||
|
|
d6716db9cc | ||
|
|
da8fa6aacd | ||
|
|
e96f9edf02 | ||
|
|
b19e9cadb2 | ||
|
|
3bb47bf914 | ||
|
|
a72fac6fde | ||
|
|
4b3186c46b | ||
|
|
e67e955f2a | ||
|
|
bf047b9824 | ||
|
|
04432835f9 | ||
|
|
77e6f28e81 | ||
|
|
f1e51ec8a1 | ||
|
|
3c525b8305 | ||
|
|
e7e844ff63 | ||
|
|
e14c55ac65 | ||
|
|
043be944ab | ||
|
|
4eb257bc14 |
@@ -7,5 +7,5 @@
|
||||
"access": "public",
|
||||
"baseBranch": "main",
|
||||
"updateInternalDependencies": "patch",
|
||||
"ignore": ["www", "v4", "tests"]
|
||||
"ignore": ["v4", "tests"]
|
||||
}
|
||||
|
||||
63
.github/ISSUE_TEMPLATE/registry_directory.yml
vendored
Normal file
63
.github/ISSUE_TEMPLATE/registry_directory.yml
vendored
Normal file
@@ -0,0 +1,63 @@
|
||||
name: Add registry to directory
|
||||
description: Add your registry to the directory
|
||||
title: "[Registry Directory]: "
|
||||
labels: ["registry", "directory"]
|
||||
assignees: []
|
||||
body:
|
||||
- type: input
|
||||
id: name
|
||||
attributes:
|
||||
label: Name
|
||||
description: The name of your registry. This is also the namespace.
|
||||
placeholder: e.g., "@acme"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: url
|
||||
attributes:
|
||||
label: URL
|
||||
description: The URL to your registry index. Use {name} placeholder.
|
||||
placeholder: https://ui.acme.com/r/{name}.json
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: homepage
|
||||
attributes:
|
||||
label: Homepage
|
||||
description: The URL to your registry homepage. This is where users can browse your registry.
|
||||
placeholder: https://ui.acme.com
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Description
|
||||
description: Briefly describe what is your registry and what type of components or code it distributes.
|
||||
placeholder:
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: logo
|
||||
attributes:
|
||||
label: Logo
|
||||
description: Add your SVG logo here.
|
||||
placeholder:
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
id: requirements
|
||||
attributes:
|
||||
label: Checklist
|
||||
description: Verify that your registry meets the following requirements.
|
||||
options:
|
||||
- label: The registry must be open source and publicly accessible.
|
||||
- label: The registry must be a valid JSON file that conforms to the [registry schema](https://ui.shadcn.com/docs/registry/registry-json) specification.
|
||||
- label: The `files` array, if present on your registry items, must NOT include a `content` property.
|
||||
- label: I've attached a square SVG logo to this issue
|
||||
validations:
|
||||
required: true
|
||||
78
.github/workflows/deprecated.yml
vendored
Normal file
78
.github/workflows/deprecated.yml
vendored
Normal file
@@ -0,0 +1,78 @@
|
||||
name: Deprecated
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, synchronize]
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
deprecated:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout PR
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Get changed files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@v46
|
||||
with:
|
||||
files: |
|
||||
apps/www/**
|
||||
files_ignore: |
|
||||
apps/www/public/r/**
|
||||
base_sha: ${{ github.event.pull_request.base.sha }}
|
||||
sha: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Comment on PR if www files changed
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const changedFiles = `${{ steps.changed-files.outputs.all_changed_files }}`.split(' ');
|
||||
const wwwFiles = changedFiles.filter(file =>
|
||||
file.startsWith('apps/www/') &&
|
||||
!file.startsWith('apps/www/public/r/') &&
|
||||
file !== 'apps/www/package.json'
|
||||
);
|
||||
|
||||
if (wwwFiles.length > 0) {
|
||||
const comment = `Looks like this PR modifies files in \`apps/www\`, which is deprecated.
|
||||
|
||||
Consider applying the change to \`apps/v4\` if relevant.`;
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
body: comment
|
||||
});
|
||||
|
||||
// Add deprecated label
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
labels: ['deprecated']
|
||||
});
|
||||
} else {
|
||||
// Remove deprecated label if no www files are changed
|
||||
try {
|
||||
await github.rest.issues.removeLabel({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
name: 'deprecated'
|
||||
});
|
||||
} catch (error) {
|
||||
// Label doesn't exist, which is fine
|
||||
console.log('Deprecated label not found, skipping removal');
|
||||
}
|
||||
}
|
||||
4
.github/workflows/prerelease.yml
vendored
4
.github/workflows/prerelease.yml
vendored
@@ -27,10 +27,10 @@ jobs:
|
||||
with:
|
||||
version: 9.0.6
|
||||
|
||||
- name: Use Node.js 18
|
||||
- name: Use Node.js 20
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: 20
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install NPM Dependencies
|
||||
|
||||
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@@ -23,11 +23,11 @@ jobs:
|
||||
with:
|
||||
version: 9.0.6
|
||||
|
||||
- name: Use Node.js 18
|
||||
- name: Use Node.js 20
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
version: 9.0.6
|
||||
node-version: 18
|
||||
node-version: 20
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install NPM Dependencies
|
||||
|
||||
131
.github/workflows/request.yml
vendored
Normal file
131
.github/workflows/request.yml
vendored
Normal file
@@ -0,0 +1,131 @@
|
||||
name: "Convert requests to discussions"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
# This runs every hour: https://crontab.guru/#0_*_*_*_*
|
||||
- cron: "0 * * * *"
|
||||
|
||||
jobs:
|
||||
convert:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository_owner == 'shadcn-ui'
|
||||
steps:
|
||||
- name: "Convert issues to discussions"
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
// Fetch issues with "area: request" label (limit 20).
|
||||
const { data: issues } = await github.rest.issues.listForRepo({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
labels: 'area: request',
|
||||
state: 'open',
|
||||
per_page: 20,
|
||||
sort: 'created',
|
||||
direction: 'asc',
|
||||
});
|
||||
|
||||
console.log(`Found ${issues.length} issues with "area: request" label`);
|
||||
|
||||
if (issues.length === 0) {
|
||||
console.log('No issues to convert');
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the repository node ID for GraphQL queries.
|
||||
const repoQuery = `
|
||||
query getRepo($owner: String!, $repo: String!) {
|
||||
repository(owner: $owner, name: $repo) {
|
||||
id
|
||||
discussionCategories(first: 20) {
|
||||
nodes {
|
||||
id
|
||||
name
|
||||
slug
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const repoResult = await github.graphql(repoQuery, {
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
});
|
||||
|
||||
const requestsCategory = repoResult.repository.discussionCategories.nodes.find(
|
||||
cat => cat.name.toLowerCase() === 'requests' || cat.slug.toLowerCase() === 'requests'
|
||||
);
|
||||
|
||||
if (!requestsCategory) {
|
||||
throw new Error('Requests category not found in discussions. Available categories: ' +
|
||||
repoResult.repository.discussionCategories.nodes.map(c => c.name).join(', '));
|
||||
}
|
||||
|
||||
// Convert each issue to a discussion.
|
||||
for (const issue of issues) {
|
||||
try {
|
||||
// Create discussion from issue using GraphQL.
|
||||
const discussionTitle = issue.title;
|
||||
const discussionBody = `**Converted from issue #${issue.number}**
|
||||
|
||||
${issue.body || ''}
|
||||
|
||||
---
|
||||
|
||||
_This discussion was automatically created from [issue #${issue.number}](${issue.html_url})_`;
|
||||
|
||||
const createDiscussionMutation = `
|
||||
mutation createDiscussion($repositoryId: ID!, $categoryId: ID!, $title: String!, $body: String!) {
|
||||
createDiscussion(input: {
|
||||
repositoryId: $repositoryId
|
||||
categoryId: $categoryId
|
||||
title: $title
|
||||
body: $body
|
||||
}) {
|
||||
discussion {
|
||||
id
|
||||
number
|
||||
url
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const discussionResult = await github.graphql(createDiscussionMutation, {
|
||||
repositoryId: repoResult.repository.id,
|
||||
categoryId: requestsCategory.id,
|
||||
title: discussionTitle,
|
||||
body: discussionBody,
|
||||
});
|
||||
|
||||
const discussion = discussionResult.createDiscussion.discussion;
|
||||
console.log(`Created discussion #${discussion.number} from issue #${issue.number}`);
|
||||
|
||||
// Add a comment to the original issue linking to the discussion.
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
body: `This issue has been converted to a discussion: [#${discussion.number}](${discussion.url})`,
|
||||
});
|
||||
|
||||
// Close the original issue.
|
||||
await github.rest.issues.update({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
state: 'closed',
|
||||
});
|
||||
|
||||
console.log(`Closed issue #${issue.number}`);
|
||||
} catch (error) {
|
||||
console.error(`Failed to convert issue #${issue.number}:`, error);
|
||||
// Continue with next issue instead of failing the entire workflow.
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Completed processing ${issues.length} issues`);
|
||||
|
||||
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@@ -19,7 +19,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
|
||||
|
||||
@@ -3,5 +3,5 @@ node_modules
|
||||
.next
|
||||
build
|
||||
.contentlayer
|
||||
apps/www/pages/api/registry.json
|
||||
**/fixtures
|
||||
deprecated
|
||||
|
||||
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
@@ -8,5 +8,8 @@
|
||||
"<node_internals>/**",
|
||||
"**/node_modules/**",
|
||||
"**/fixtures/**"
|
||||
]
|
||||
],
|
||||
"files.exclude": {
|
||||
"deprecated": true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,28 +20,25 @@ This repository is structured as follows:
|
||||
|
||||
```
|
||||
apps
|
||||
└── www
|
||||
└── v4
|
||||
├── app
|
||||
├── components
|
||||
├── content
|
||||
└── registry
|
||||
├── default
|
||||
│ ├── example
|
||||
│ └── ui
|
||||
└── new-york
|
||||
└── new-york-v4
|
||||
├── example
|
||||
└── ui
|
||||
packages
|
||||
└── cli
|
||||
└── shadcn
|
||||
```
|
||||
|
||||
| Path | Description |
|
||||
| --------------------- | ---------------------------------------- |
|
||||
| `apps/www/app` | The Next.js application for the website. |
|
||||
| `apps/www/components` | The React components for the website. |
|
||||
| `apps/www/content` | The content for the website. |
|
||||
| `apps/www/registry` | The registry for the components. |
|
||||
| `packages/cli` | The `shadcn-ui` package. |
|
||||
| Path | Description |
|
||||
| -------------------- | ---------------------------------------- |
|
||||
| `apps/v4/app` | The Next.js application for the website. |
|
||||
| `apps/v4/components` | The React components for the website. |
|
||||
| `apps/v4/content` | The content for the website. |
|
||||
| `apps/v4/registry` | The registry for the components. |
|
||||
| `packages/shadcn` | The `shadcn` package. |
|
||||
|
||||
## Development
|
||||
|
||||
@@ -82,32 +79,26 @@ You can use the `pnpm --filter=[WORKSPACE]` command to start the development pro
|
||||
1. To run the `ui.shadcn.com` website:
|
||||
|
||||
```bash
|
||||
pnpm --filter=www dev
|
||||
pnpm --filter=v4 dev
|
||||
```
|
||||
|
||||
2. To run the `shadcn-ui` package:
|
||||
2. To run the `shadcn` package:
|
||||
|
||||
```bash
|
||||
pnpm --filter=shadcn-ui dev
|
||||
pnpm --filter=shadcn 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:
|
||||
1. Start by running the dev server:
|
||||
|
||||
```bash
|
||||
pnpm v4:dev
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
2. Run the development script for the CLI:
|
||||
|
||||
```bash
|
||||
pnpm shadcn:dev
|
||||
```
|
||||
|
||||
3. In another terminal tab, test the CLI by running:
|
||||
2. In another terminal tab, test the CLI by running:
|
||||
|
||||
```bash
|
||||
pnpm shadcn
|
||||
@@ -119,36 +110,27 @@ To run the CLI locally, you can follow the workflow:
|
||||
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:
|
||||
The documentation for this project is located in the `v4` workspace. You can run the documentation locally by running the following command:
|
||||
|
||||
```bash
|
||||
pnpm --filter=www dev
|
||||
pnpm --filter=v4 dev
|
||||
```
|
||||
|
||||
Documentation is written using [MDX](https://mdxjs.com). You can find the documentation files in the `apps/www/content/docs` directory.
|
||||
Documentation is written using [MDX](https://mdxjs.com). You can find the documentation files in the `apps/v4/content/docs` directory.
|
||||
|
||||
## Components
|
||||
|
||||
We use a registry system for developing components. You can find the source code for the components under `apps/www/registry`. The components are organized by styles.
|
||||
We use a registry system for developing components. You can find the source code for the components under `apps/v4/registry`. The components are organized by styles.
|
||||
|
||||
```bash
|
||||
apps
|
||||
└── www
|
||||
└── v4
|
||||
└── registry
|
||||
├── default
|
||||
│ ├── example
|
||||
│ └── ui
|
||||
└── new-york
|
||||
└── new-york-v4
|
||||
├── example
|
||||
└── ui
|
||||
```
|
||||
@@ -157,7 +139,7 @@ When adding or modifying components, please ensure that:
|
||||
|
||||
1. You make the changes for every style.
|
||||
2. You update the documentation.
|
||||
3. You run `pnpm build:registry` to update the registry.
|
||||
3. You run `pnpm registry:build` to update the registry.
|
||||
|
||||
## Commit Convention
|
||||
|
||||
@@ -196,9 +178,9 @@ If you have a request for a new component, please open a discussion on GitHub. W
|
||||
|
||||
## CLI
|
||||
|
||||
The `shadcn-ui` package is a CLI for adding components to your project. You can find the documentation for the CLI [here](https://ui.shadcn.com/docs/cli).
|
||||
The `shadcn` package is a CLI for adding components to your project. You can find the documentation for the CLI [here](https://ui.shadcn.com/docs/cli).
|
||||
|
||||
Any changes to the CLI should be made in the `packages/cli` directory. If you can, it would be great if you could add tests for your changes.
|
||||
Any changes to the CLI should be made in the `packages/shadcn` directory. If you can, it would be great if you could add tests for your changes.
|
||||
|
||||
## Testing
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# shadcn/ui
|
||||
|
||||
Accessible and customizable components that you can copy and paste into your apps. Free. Open Source. **Use this to build your own component library**.
|
||||
A set of beautifully designed components that you can customize, extend, and build on. Start here then make it your own. Open Source. Open Code. **Use this to build your own component library**.
|
||||
|
||||

|
||||

|
||||
|
||||
## Documentation
|
||||
|
||||
|
||||
@@ -6,4 +6,4 @@ We will investigate all legitimate reports and do our best to quickly fix the pr
|
||||
|
||||
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.
|
||||
To do this, please visit the security tab of the repository and click the [Report a vulnerability](https://github.com/shadcn-ui/ui/security/advisories/new) button.
|
||||
|
||||
138
apps/v4/app/(app)/(root)/components/appearance-settings.tsx
Normal file
138
apps/v4/app/(app)/(root)/components/appearance-settings.tsx
Normal file
@@ -0,0 +1,138 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { IconMinus, IconPlus } from "@tabler/icons-react"
|
||||
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import { ButtonGroup } from "@/registry/new-york-v4/ui/button-group"
|
||||
import {
|
||||
Field,
|
||||
FieldContent,
|
||||
FieldDescription,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldLegend,
|
||||
FieldSeparator,
|
||||
FieldSet,
|
||||
FieldTitle,
|
||||
} from "@/registry/new-york-v4/ui/field"
|
||||
import { Input } from "@/registry/new-york-v4/ui/input"
|
||||
import {
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
} from "@/registry/new-york-v4/ui/radio-group"
|
||||
import { Switch } from "@/registry/new-york-v4/ui/switch"
|
||||
|
||||
export function AppearanceSettings() {
|
||||
const [gpuCount, setGpuCount] = React.useState(8)
|
||||
|
||||
const handleGpuAdjustment = React.useCallback((adjustment: number) => {
|
||||
setGpuCount((prevCount) =>
|
||||
Math.max(1, Math.min(99, prevCount + adjustment))
|
||||
)
|
||||
}, [])
|
||||
|
||||
const handleGpuInputChange = React.useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = parseInt(e.target.value, 10)
|
||||
if (!isNaN(value) && value >= 1 && value <= 99) {
|
||||
setGpuCount(value)
|
||||
}
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
return (
|
||||
<FieldSet>
|
||||
<FieldGroup>
|
||||
<FieldSet>
|
||||
<FieldLegend>Compute Environment</FieldLegend>
|
||||
<FieldDescription>
|
||||
Select the compute environment for your cluster.
|
||||
</FieldDescription>
|
||||
<RadioGroup defaultValue="kubernetes">
|
||||
<FieldLabel htmlFor="kubernetes-r2h">
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldTitle>Kubernetes</FieldTitle>
|
||||
<FieldDescription>
|
||||
Run GPU workloads on a K8s configured cluster. This is the
|
||||
default.
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<RadioGroupItem
|
||||
value="kubernetes"
|
||||
id="kubernetes-r2h"
|
||||
aria-label="Kubernetes"
|
||||
/>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
<FieldLabel htmlFor="vm-z4k">
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldTitle>Virtual Machine</FieldTitle>
|
||||
<FieldDescription>
|
||||
Access a VM configured cluster to run workloads. (Coming
|
||||
soon)
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<RadioGroupItem
|
||||
value="vm"
|
||||
id="vm-z4k"
|
||||
aria-label="Virtual Machine"
|
||||
/>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
</RadioGroup>
|
||||
</FieldSet>
|
||||
<FieldSeparator />
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor="number-of-gpus-f6l">Number of GPUs</FieldLabel>
|
||||
<FieldDescription>You can add more later.</FieldDescription>
|
||||
</FieldContent>
|
||||
<ButtonGroup>
|
||||
<Input
|
||||
id="number-of-gpus-f6l"
|
||||
value={gpuCount}
|
||||
onChange={handleGpuInputChange}
|
||||
size={3}
|
||||
className="h-8 !w-14 font-mono"
|
||||
maxLength={3}
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon-sm"
|
||||
type="button"
|
||||
aria-label="Decrement"
|
||||
onClick={() => handleGpuAdjustment(-1)}
|
||||
disabled={gpuCount <= 1}
|
||||
>
|
||||
<IconMinus />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon-sm"
|
||||
type="button"
|
||||
aria-label="Increment"
|
||||
onClick={() => handleGpuAdjustment(1)}
|
||||
disabled={gpuCount >= 99}
|
||||
>
|
||||
<IconPlus />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</Field>
|
||||
<FieldSeparator />
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor="tinting">Wallpaper Tinting</FieldLabel>
|
||||
<FieldDescription>
|
||||
Allow the wallpaper to be tinted.
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<Switch id="tinting" defaultChecked />
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</FieldSet>
|
||||
)
|
||||
}
|
||||
120
apps/v4/app/(app)/(root)/components/button-group-demo.tsx
Normal file
120
apps/v4/app/(app)/(root)/components/button-group-demo.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import {
|
||||
ArchiveIcon,
|
||||
ArrowLeftIcon,
|
||||
CalendarPlusIcon,
|
||||
ClockIcon,
|
||||
ListFilterPlusIcon,
|
||||
MailCheckIcon,
|
||||
MoreHorizontalIcon,
|
||||
TagIcon,
|
||||
Trash2Icon,
|
||||
} from "lucide-react"
|
||||
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import { ButtonGroup } from "@/registry/new-york-v4/ui/button-group"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/registry/new-york-v4/ui/dropdown-menu"
|
||||
|
||||
export function ButtonGroupDemo() {
|
||||
const [label, setLabel] = React.useState("personal")
|
||||
|
||||
return (
|
||||
<ButtonGroup>
|
||||
<ButtonGroup className="hidden sm:flex">
|
||||
<Button variant="outline" size="icon-sm" aria-label="Go Back">
|
||||
<ArrowLeftIcon />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup>
|
||||
<Button variant="outline" size="sm">
|
||||
Archive
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
Report
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup>
|
||||
<Button variant="outline" size="sm">
|
||||
Snooze
|
||||
</Button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" size="icon-sm" aria-label="More Options">
|
||||
<MoreHorizontalIcon />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-48 [--radius:1rem]">
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>
|
||||
<MailCheckIcon />
|
||||
Mark as Read
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<ArchiveIcon />
|
||||
Archive
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>
|
||||
<ClockIcon />
|
||||
Snooze
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<CalendarPlusIcon />
|
||||
Add to Calendar
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<ListFilterPlusIcon />
|
||||
Add to List
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger>
|
||||
<TagIcon />
|
||||
Label As...
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuSubContent>
|
||||
<DropdownMenuRadioGroup
|
||||
value={label}
|
||||
onValueChange={setLabel}
|
||||
>
|
||||
<DropdownMenuRadioItem value="personal">
|
||||
Personal
|
||||
</DropdownMenuRadioItem>
|
||||
<DropdownMenuRadioItem value="work">
|
||||
Work
|
||||
</DropdownMenuRadioItem>
|
||||
<DropdownMenuRadioItem value="other">
|
||||
Other
|
||||
</DropdownMenuRadioItem>
|
||||
</DropdownMenuRadioGroup>
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuSub>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem variant="destructive">
|
||||
<Trash2Icon />
|
||||
Trash
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</ButtonGroup>
|
||||
</ButtonGroup>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { AudioLinesIcon, PlusIcon } from "lucide-react"
|
||||
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import { ButtonGroup } from "@/registry/new-york-v4/ui/button-group"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupInput,
|
||||
} from "@/registry/new-york-v4/ui/input-group"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/registry/new-york-v4/ui/tooltip"
|
||||
|
||||
export function ButtonGroupInputGroup() {
|
||||
const [voiceEnabled, setVoiceEnabled] = React.useState(false)
|
||||
return (
|
||||
<ButtonGroup className="[--radius:9999rem]">
|
||||
<ButtonGroup>
|
||||
<Button variant="outline" size="icon" aria-label="Add">
|
||||
<PlusIcon />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup className="flex-1">
|
||||
<InputGroup>
|
||||
<InputGroupInput
|
||||
placeholder={
|
||||
voiceEnabled ? "Record and send audio..." : "Send a message..."
|
||||
}
|
||||
disabled={voiceEnabled}
|
||||
/>
|
||||
<InputGroupAddon align="inline-end">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<InputGroupButton
|
||||
onClick={() => setVoiceEnabled(!voiceEnabled)}
|
||||
data-active={voiceEnabled}
|
||||
className="data-[active=true]:bg-primary data-[active=true]:text-primary-foreground"
|
||||
aria-pressed={voiceEnabled}
|
||||
size="icon-xs"
|
||||
aria-label="Voice Mode"
|
||||
>
|
||||
<AudioLinesIcon />
|
||||
</InputGroupButton>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Voice Mode</TooltipContent>
|
||||
</Tooltip>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</ButtonGroup>
|
||||
</ButtonGroup>
|
||||
)
|
||||
}
|
||||
32
apps/v4/app/(app)/(root)/components/button-group-nested.tsx
Normal file
32
apps/v4/app/(app)/(root)/components/button-group-nested.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
"use client"
|
||||
|
||||
import { ArrowLeftIcon, ArrowRightIcon } from "lucide-react"
|
||||
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import { ButtonGroup } from "@/registry/new-york-v4/ui/button-group"
|
||||
|
||||
export function ButtonGroupNested() {
|
||||
return (
|
||||
<ButtonGroup>
|
||||
<ButtonGroup>
|
||||
<Button variant="outline" size="sm">
|
||||
1
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
2
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
3
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup>
|
||||
<Button variant="outline" size="icon-sm" aria-label="Previous">
|
||||
<ArrowLeftIcon />
|
||||
</Button>
|
||||
<Button variant="outline" size="icon-sm" aria-label="Next">
|
||||
<ArrowRightIcon />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</ButtonGroup>
|
||||
)
|
||||
}
|
||||
45
apps/v4/app/(app)/(root)/components/button-group-popover.tsx
Normal file
45
apps/v4/app/(app)/(root)/components/button-group-popover.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import { BotIcon, ChevronDownIcon } from "lucide-react"
|
||||
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import { ButtonGroup } from "@/registry/new-york-v4/ui/button-group"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/registry/new-york-v4/ui/popover"
|
||||
import { Separator } from "@/registry/new-york-v4/ui/separator"
|
||||
import { Textarea } from "@/registry/new-york-v4/ui/textarea"
|
||||
|
||||
export function ButtonGroupPopover() {
|
||||
return (
|
||||
<ButtonGroup>
|
||||
<Button variant="outline" size="sm">
|
||||
<BotIcon /> Copilot
|
||||
</Button>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline" size="icon-sm" aria-label="Open Popover">
|
||||
<ChevronDownIcon />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent align="end" className="rounded-xl p-0 text-sm">
|
||||
<div className="px-4 py-3">
|
||||
<div className="text-sm font-medium">Agent Tasks</div>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="p-4 text-sm *:[p:not(:last-child)]:mb-2">
|
||||
<Textarea
|
||||
placeholder="Describe your task in natural language."
|
||||
className="mb-4 resize-none"
|
||||
/>
|
||||
<p className="font-medium">Start a new task with Copilot</p>
|
||||
<p className="text-muted-foreground">
|
||||
Describe your task in natural language. Copilot will work in the
|
||||
background and open a pull request for your review.
|
||||
</p>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</ButtonGroup>
|
||||
)
|
||||
}
|
||||
57
apps/v4/app/(app)/(root)/components/empty-avatar-group.tsx
Normal file
57
apps/v4/app/(app)/(root)/components/empty-avatar-group.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import { PlusIcon } from "lucide-react"
|
||||
|
||||
import {
|
||||
Avatar,
|
||||
AvatarFallback,
|
||||
AvatarImage,
|
||||
} from "@/registry/new-york-v4/ui/avatar"
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import {
|
||||
Empty,
|
||||
EmptyContent,
|
||||
EmptyDescription,
|
||||
EmptyHeader,
|
||||
EmptyMedia,
|
||||
EmptyTitle,
|
||||
} from "@/registry/new-york-v4/ui/empty"
|
||||
|
||||
export function EmptyAvatarGroup() {
|
||||
return (
|
||||
<Empty className="flex-none border">
|
||||
<EmptyHeader>
|
||||
<EmptyMedia>
|
||||
<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/maxleiter.png"
|
||||
alt="@maxleiter"
|
||||
/>
|
||||
<AvatarFallback>LR</AvatarFallback>
|
||||
</Avatar>
|
||||
<Avatar>
|
||||
<AvatarImage
|
||||
src="https://github.com/evilrabbit.png"
|
||||
alt="@evilrabbit"
|
||||
/>
|
||||
<AvatarFallback>ER</AvatarFallback>
|
||||
</Avatar>
|
||||
</div>
|
||||
</EmptyMedia>
|
||||
<EmptyTitle>No Team Members</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
Invite your team to collaborate on this project.
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
<EmptyContent>
|
||||
<Button size="sm">
|
||||
<PlusIcon />
|
||||
Invite Members
|
||||
</Button>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
)
|
||||
}
|
||||
43
apps/v4/app/(app)/(root)/components/empty-input-group.tsx
Normal file
43
apps/v4/app/(app)/(root)/components/empty-input-group.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import { SearchIcon } from "lucide-react"
|
||||
|
||||
import {
|
||||
Empty,
|
||||
EmptyContent,
|
||||
EmptyDescription,
|
||||
EmptyHeader,
|
||||
EmptyTitle,
|
||||
} from "@/registry/new-york-v4/ui/empty"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupInput,
|
||||
} from "@/registry/new-york-v4/ui/input-group"
|
||||
import { Kbd } from "@/registry/new-york-v4/ui/kbd"
|
||||
|
||||
export function EmptyInputGroup() {
|
||||
return (
|
||||
<Empty>
|
||||
<EmptyHeader>
|
||||
<EmptyTitle>404 - Not Found</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
The page you're looking for doesn't exist. Try searching for
|
||||
what you need below.
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
<EmptyContent>
|
||||
<InputGroup className="w-3/4">
|
||||
<InputGroupInput placeholder="Try searching for pages..." />
|
||||
<InputGroupAddon>
|
||||
<SearchIcon />
|
||||
</InputGroupAddon>
|
||||
<InputGroupAddon align="inline-end">
|
||||
<Kbd>/</Kbd>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<EmptyDescription>
|
||||
Need help? <a href="#">Contact support</a>
|
||||
</EmptyDescription>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
)
|
||||
}
|
||||
15
apps/v4/app/(app)/(root)/components/field-checkbox.tsx
Normal file
15
apps/v4/app/(app)/(root)/components/field-checkbox.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Checkbox } from "@/registry/new-york-v4/ui/checkbox"
|
||||
import { Field, FieldLabel } from "@/registry/new-york-v4/ui/field"
|
||||
|
||||
export function FieldCheckbox() {
|
||||
return (
|
||||
<FieldLabel htmlFor="checkbox-demo">
|
||||
<Field orientation="horizontal">
|
||||
<Checkbox id="checkbox-demo" defaultChecked />
|
||||
<FieldLabel htmlFor="checkbox-demo" className="line-clamp-1">
|
||||
I agree to the terms and conditions
|
||||
</FieldLabel>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
)
|
||||
}
|
||||
62
apps/v4/app/(app)/(root)/components/field-choice-card.tsx
Normal file
62
apps/v4/app/(app)/(root)/components/field-choice-card.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import {
|
||||
Field,
|
||||
FieldContent,
|
||||
FieldDescription,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldSet,
|
||||
FieldTitle,
|
||||
} from "@/registry/new-york-v4/ui/field"
|
||||
import {
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
} from "@/registry/new-york-v4/ui/radio-group"
|
||||
|
||||
export function FieldChoiceCard() {
|
||||
return (
|
||||
<div className="w-full max-w-md">
|
||||
<FieldGroup>
|
||||
<FieldSet>
|
||||
<FieldLabel htmlFor="compute-environment-p8w">
|
||||
Compute Environment
|
||||
</FieldLabel>
|
||||
<FieldDescription>
|
||||
Select the compute environment for your cluster.
|
||||
</FieldDescription>
|
||||
<RadioGroup defaultValue="kubernetes">
|
||||
<FieldLabel htmlFor="kubernetes-r2h">
|
||||
<Field orientation="horizontal">
|
||||
<RadioGroupItem
|
||||
value="kubernetes"
|
||||
id="kubernetes-r2h"
|
||||
aria-label="Kubernetes"
|
||||
/>
|
||||
<FieldContent>
|
||||
<FieldTitle>Kubernetes</FieldTitle>
|
||||
<FieldDescription>
|
||||
Run GPU workloads on a K8s configured cluster.
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
<FieldLabel htmlFor="vm-z4k">
|
||||
<Field orientation="horizontal">
|
||||
<RadioGroupItem
|
||||
value="vm"
|
||||
id="vm-z4k"
|
||||
aria-label="Virtual Machine"
|
||||
/>
|
||||
<FieldContent>
|
||||
<FieldTitle>Virtual Machine</FieldTitle>
|
||||
<FieldDescription>
|
||||
Access a VM configured cluster to run workloads.
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
</RadioGroup>
|
||||
</FieldSet>
|
||||
</FieldGroup>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
153
apps/v4/app/(app)/(root)/components/field-demo.tsx
Normal file
153
apps/v4/app/(app)/(root)/components/field-demo.tsx
Normal file
@@ -0,0 +1,153 @@
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import { Checkbox } from "@/registry/new-york-v4/ui/checkbox"
|
||||
import {
|
||||
Field,
|
||||
FieldDescription,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldLegend,
|
||||
FieldSeparator,
|
||||
FieldSet,
|
||||
} from "@/registry/new-york-v4/ui/field"
|
||||
import { Input } from "@/registry/new-york-v4/ui/input"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/registry/new-york-v4/ui/select"
|
||||
import { Textarea } from "@/registry/new-york-v4/ui/textarea"
|
||||
|
||||
export function FieldDemo() {
|
||||
return (
|
||||
<div className="w-full max-w-md rounded-lg border p-6">
|
||||
<form>
|
||||
<FieldGroup>
|
||||
<FieldSet>
|
||||
<FieldLegend>Payment Method</FieldLegend>
|
||||
<FieldDescription>
|
||||
All transactions are secure and encrypted
|
||||
</FieldDescription>
|
||||
<FieldGroup>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="checkout-7j9-card-name-43j">
|
||||
Name on Card
|
||||
</FieldLabel>
|
||||
<Input
|
||||
id="checkout-7j9-card-name-43j"
|
||||
placeholder="John Doe"
|
||||
required
|
||||
/>
|
||||
</Field>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<Field className="col-span-2">
|
||||
<FieldLabel htmlFor="checkout-7j9-card-number-uw1">
|
||||
Card Number
|
||||
</FieldLabel>
|
||||
<Input
|
||||
id="checkout-7j9-card-number-uw1"
|
||||
placeholder="1234 5678 9012 3456"
|
||||
required
|
||||
/>
|
||||
<FieldDescription>
|
||||
Enter your 16-digit number.
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
<Field className="col-span-1">
|
||||
<FieldLabel htmlFor="checkout-7j9-cvv">CVV</FieldLabel>
|
||||
<Input id="checkout-7j9-cvv" placeholder="123" required />
|
||||
</Field>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Field>
|
||||
<FieldLabel htmlFor="checkout-7j9-exp-month-ts6">
|
||||
Month
|
||||
</FieldLabel>
|
||||
<Select defaultValue="">
|
||||
<SelectTrigger id="checkout-7j9-exp-month-ts6">
|
||||
<SelectValue placeholder="MM" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="01">01</SelectItem>
|
||||
<SelectItem value="02">02</SelectItem>
|
||||
<SelectItem value="03">03</SelectItem>
|
||||
<SelectItem value="04">04</SelectItem>
|
||||
<SelectItem value="05">05</SelectItem>
|
||||
<SelectItem value="06">06</SelectItem>
|
||||
<SelectItem value="07">07</SelectItem>
|
||||
<SelectItem value="08">08</SelectItem>
|
||||
<SelectItem value="09">09</SelectItem>
|
||||
<SelectItem value="10">10</SelectItem>
|
||||
<SelectItem value="11">11</SelectItem>
|
||||
<SelectItem value="12">12</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="checkout-7j9-exp-year-f59">
|
||||
Year
|
||||
</FieldLabel>
|
||||
<Select defaultValue="">
|
||||
<SelectTrigger id="checkout-7j9-exp-year-f59">
|
||||
<SelectValue placeholder="YYYY" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="2024">2024</SelectItem>
|
||||
<SelectItem value="2025">2025</SelectItem>
|
||||
<SelectItem value="2026">2026</SelectItem>
|
||||
<SelectItem value="2027">2027</SelectItem>
|
||||
<SelectItem value="2028">2028</SelectItem>
|
||||
<SelectItem value="2029">2029</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
</div>
|
||||
</FieldGroup>
|
||||
</FieldSet>
|
||||
<FieldSeparator />
|
||||
<FieldSet>
|
||||
<FieldLegend>Billing Address</FieldLegend>
|
||||
<FieldDescription>
|
||||
The billing address associated with your payment method
|
||||
</FieldDescription>
|
||||
<FieldGroup>
|
||||
<Field orientation="horizontal">
|
||||
<Checkbox
|
||||
id="checkout-7j9-same-as-shipping-wgm"
|
||||
defaultChecked
|
||||
/>
|
||||
<FieldLabel
|
||||
htmlFor="checkout-7j9-same-as-shipping-wgm"
|
||||
className="font-normal"
|
||||
>
|
||||
Same as shipping address
|
||||
</FieldLabel>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</FieldSet>
|
||||
<FieldSeparator />
|
||||
<FieldSet>
|
||||
<FieldGroup>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="checkout-7j9-optional-comments">
|
||||
Comments
|
||||
</FieldLabel>
|
||||
<Textarea
|
||||
id="checkout-7j9-optional-comments"
|
||||
placeholder="Add any additional comments"
|
||||
/>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</FieldSet>
|
||||
<Field orientation="horizontal">
|
||||
<Button type="submit">Submit</Button>
|
||||
<Button variant="outline" type="button">
|
||||
Cancel
|
||||
</Button>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
72
apps/v4/app/(app)/(root)/components/field-hear.tsx
Normal file
72
apps/v4/app/(app)/(root)/components/field-hear.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
import { Card, CardContent } from "@/registry/new-york-v4/ui/card"
|
||||
import { Checkbox } from "@/registry/new-york-v4/ui/checkbox"
|
||||
import {
|
||||
Field,
|
||||
FieldDescription,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldLegend,
|
||||
FieldSet,
|
||||
FieldTitle,
|
||||
} from "@/registry/new-york-v4/ui/field"
|
||||
|
||||
const options = [
|
||||
{
|
||||
label: "Social Media",
|
||||
value: "social-media",
|
||||
},
|
||||
|
||||
{
|
||||
label: "Search Engine",
|
||||
value: "search-engine",
|
||||
},
|
||||
{
|
||||
label: "Referral",
|
||||
value: "referral",
|
||||
},
|
||||
{
|
||||
label: "Other",
|
||||
value: "other",
|
||||
},
|
||||
]
|
||||
|
||||
export function FieldHear() {
|
||||
return (
|
||||
<Card className="py-4 shadow-none">
|
||||
<CardContent className="px-4">
|
||||
<form>
|
||||
<FieldGroup>
|
||||
<FieldSet className="gap-4">
|
||||
<FieldLegend>How did you hear about us?</FieldLegend>
|
||||
<FieldDescription className="line-clamp-1">
|
||||
Select the option that best describes how you heard about us.
|
||||
</FieldDescription>
|
||||
<FieldGroup className="flex flex-row flex-wrap gap-2 [--radius:9999rem]">
|
||||
{options.map((option) => (
|
||||
<FieldLabel
|
||||
htmlFor={option.value}
|
||||
key={option.value}
|
||||
className="!w-fit"
|
||||
>
|
||||
<Field
|
||||
orientation="horizontal"
|
||||
className="gap-1.5 overflow-hidden !px-3 !py-1.5 transition-all duration-100 ease-linear group-has-data-[state=checked]/field-label:!px-2"
|
||||
>
|
||||
<Checkbox
|
||||
value={option.value}
|
||||
id={option.value}
|
||||
defaultChecked={option.value === "social-media"}
|
||||
className="-ml-6 -translate-x-1 rounded-full transition-all duration-100 ease-linear data-[state=checked]:ml-0 data-[state=checked]:translate-x-0"
|
||||
/>
|
||||
<FieldTitle>{option.label}</FieldTitle>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
))}
|
||||
</FieldGroup>
|
||||
</FieldSet>
|
||||
</FieldGroup>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
35
apps/v4/app/(app)/(root)/components/field-slider.tsx
Normal file
35
apps/v4/app/(app)/(root)/components/field-slider.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
|
||||
import {
|
||||
Field,
|
||||
FieldDescription,
|
||||
FieldTitle,
|
||||
} from "@/registry/new-york-v4/ui/field"
|
||||
import { Slider } from "@/registry/new-york-v4/ui/slider"
|
||||
|
||||
export function FieldSlider() {
|
||||
const [value, setValue] = useState([200, 800])
|
||||
return (
|
||||
<div className="w-full max-w-md">
|
||||
<Field>
|
||||
<FieldTitle>Price Range</FieldTitle>
|
||||
<FieldDescription>
|
||||
Set your budget range ($
|
||||
<span className="font-medium tabular-nums">{value[0]}</span> -{" "}
|
||||
<span className="font-medium tabular-nums">{value[1]}</span>).
|
||||
</FieldDescription>
|
||||
<Slider
|
||||
value={value}
|
||||
onValueChange={setValue}
|
||||
max={1000}
|
||||
min={0}
|
||||
step={10}
|
||||
className="mt-2 w-full"
|
||||
aria-label="Price Range"
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
52
apps/v4/app/(app)/(root)/components/index.tsx
Normal file
52
apps/v4/app/(app)/(root)/components/index.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import { FieldSeparator } from "@/registry/new-york-v4/ui/field"
|
||||
|
||||
import { AppearanceSettings } from "./appearance-settings"
|
||||
import { ButtonGroupDemo } from "./button-group-demo"
|
||||
import { ButtonGroupInputGroup } from "./button-group-input-group"
|
||||
import { ButtonGroupNested } from "./button-group-nested"
|
||||
import { ButtonGroupPopover } from "./button-group-popover"
|
||||
import { EmptyAvatarGroup } from "./empty-avatar-group"
|
||||
import { FieldCheckbox } from "./field-checkbox"
|
||||
import { FieldDemo } from "./field-demo"
|
||||
import { FieldHear } from "./field-hear"
|
||||
import { FieldSlider } from "./field-slider"
|
||||
import { InputGroupButtonExample } from "./input-group-button"
|
||||
import { InputGroupDemo } from "./input-group-demo"
|
||||
import { ItemDemo } from "./item-demo"
|
||||
import { NotionPromptForm } from "./notion-prompt-form"
|
||||
import { SpinnerBadge } from "./spinner-badge"
|
||||
import { SpinnerEmpty } from "./spinner-empty"
|
||||
|
||||
export function RootComponents() {
|
||||
return (
|
||||
<div className="theme-container mx-auto grid gap-8 py-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 xl:gap-6 2xl:gap-8">
|
||||
<div className="flex flex-col gap-6 *:[div]:w-full *:[div]:max-w-full">
|
||||
<FieldDemo />
|
||||
</div>
|
||||
<div className="flex flex-col gap-6 *:[div]:w-full *:[div]:max-w-full">
|
||||
<EmptyAvatarGroup />
|
||||
<SpinnerBadge />
|
||||
<ButtonGroupInputGroup />
|
||||
<FieldSlider />
|
||||
<InputGroupDemo />
|
||||
</div>
|
||||
<div className="flex flex-col gap-6 *:[div]:w-full *:[div]:max-w-full">
|
||||
<InputGroupButtonExample />
|
||||
<ItemDemo />
|
||||
<FieldSeparator className="my-4">Appearance Settings</FieldSeparator>
|
||||
<AppearanceSettings />
|
||||
</div>
|
||||
<div className="order-first flex flex-col gap-6 lg:hidden xl:order-last xl:flex *:[div]:w-full *:[div]:max-w-full">
|
||||
<NotionPromptForm />
|
||||
<ButtonGroupDemo />
|
||||
<FieldCheckbox />
|
||||
<div className="flex justify-between gap-4">
|
||||
<ButtonGroupNested />
|
||||
<ButtonGroupPopover />
|
||||
</div>
|
||||
<FieldHear />
|
||||
<SpinnerEmpty />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
68
apps/v4/app/(app)/(root)/components/input-group-button.tsx
Normal file
68
apps/v4/app/(app)/(root)/components/input-group-button.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { IconInfoCircle, IconStar } from "@tabler/icons-react"
|
||||
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupInput,
|
||||
} from "@/registry/new-york-v4/ui/input-group"
|
||||
import { Label } from "@/registry/new-york-v4/ui/label"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/registry/new-york-v4/ui/popover"
|
||||
|
||||
export function InputGroupButtonExample() {
|
||||
const [isFavorite, setIsFavorite] = React.useState(false)
|
||||
|
||||
return (
|
||||
<div className="grid w-full max-w-sm gap-6">
|
||||
<Label htmlFor="input-secure-19" className="sr-only">
|
||||
Input Secure
|
||||
</Label>
|
||||
<InputGroup className="[--radius:9999px]">
|
||||
<InputGroupInput id="input-secure-19" className="!pl-0.5" />
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<InputGroupAddon>
|
||||
<InputGroupButton
|
||||
variant="secondary"
|
||||
size="icon-xs"
|
||||
aria-label="Info"
|
||||
>
|
||||
<IconInfoCircle />
|
||||
</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
align="start"
|
||||
alignOffset={10}
|
||||
className="flex flex-col gap-1 rounded-xl text-sm"
|
||||
>
|
||||
<p className="font-medium">Your connection is not secure.</p>
|
||||
<p>You should not enter any sensitive information on this site.</p>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<InputGroupAddon className="text-muted-foreground !pl-1">
|
||||
https://
|
||||
</InputGroupAddon>
|
||||
<InputGroupAddon align="inline-end">
|
||||
<InputGroupButton
|
||||
onClick={() => setIsFavorite(!isFavorite)}
|
||||
size="icon-xs"
|
||||
aria-label="Favorite"
|
||||
>
|
||||
<IconStar
|
||||
data-favorite={isFavorite}
|
||||
className="data-[favorite=true]:fill-primary data-[favorite=true]:stroke-primary"
|
||||
/>
|
||||
</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
102
apps/v4/app/(app)/(root)/components/input-group-demo.tsx
Normal file
102
apps/v4/app/(app)/(root)/components/input-group-demo.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
import { IconCheck, IconInfoCircle, IconPlus } from "@tabler/icons-react"
|
||||
import { ArrowUpIcon, Search } from "lucide-react"
|
||||
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/registry/new-york-v4/ui/dropdown-menu"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupInput,
|
||||
InputGroupText,
|
||||
InputGroupTextarea,
|
||||
} from "@/registry/new-york-v4/ui/input-group"
|
||||
import { Separator } from "@/registry/new-york-v4/ui/separator"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/registry/new-york-v4/ui/tooltip"
|
||||
|
||||
export function InputGroupDemo() {
|
||||
return (
|
||||
<div className="grid w-full max-w-sm gap-6">
|
||||
<InputGroup>
|
||||
<InputGroupInput placeholder="Search..." />
|
||||
<InputGroupAddon>
|
||||
<Search />
|
||||
</InputGroupAddon>
|
||||
<InputGroupAddon align="inline-end">12 results</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<InputGroup>
|
||||
<InputGroupInput placeholder="example.com" className="!pl-1" />
|
||||
<InputGroupAddon>
|
||||
<InputGroupText>https://</InputGroupText>
|
||||
</InputGroupAddon>
|
||||
<InputGroupAddon align="inline-end">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<InputGroupButton
|
||||
className="rounded-full"
|
||||
size="icon-xs"
|
||||
aria-label="Info"
|
||||
>
|
||||
<IconInfoCircle />
|
||||
</InputGroupButton>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>This is content in a tooltip.</TooltipContent>
|
||||
</Tooltip>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<InputGroup>
|
||||
<InputGroupTextarea placeholder="Ask, Search or Chat..." />
|
||||
<InputGroupAddon align="block-end">
|
||||
<InputGroupButton
|
||||
variant="outline"
|
||||
className="rounded-full"
|
||||
size="icon-xs"
|
||||
aria-label="Add"
|
||||
>
|
||||
<IconPlus />
|
||||
</InputGroupButton>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<InputGroupButton variant="ghost">Auto</InputGroupButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
side="top"
|
||||
align="start"
|
||||
className="[--radius:0.95rem]"
|
||||
>
|
||||
<DropdownMenuItem>Auto</DropdownMenuItem>
|
||||
<DropdownMenuItem>Agent</DropdownMenuItem>
|
||||
<DropdownMenuItem>Manual</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<InputGroupText className="ml-auto">52% used</InputGroupText>
|
||||
<Separator orientation="vertical" className="!h-4" />
|
||||
<InputGroupButton
|
||||
variant="default"
|
||||
className="rounded-full"
|
||||
size="icon-xs"
|
||||
>
|
||||
<ArrowUpIcon />
|
||||
<span className="sr-only">Send</span>
|
||||
</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<InputGroup>
|
||||
<InputGroupInput placeholder="@shadcn" />
|
||||
<InputGroupAddon align="inline-end">
|
||||
<div className="bg-primary text-foreground flex size-4 items-center justify-center rounded-full">
|
||||
<IconCheck className="size-3 text-white" />
|
||||
</div>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
46
apps/v4/app/(app)/(root)/components/input-group-textarea.tsx
Normal file
46
apps/v4/app/(app)/(root)/components/input-group-textarea.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import {
|
||||
IconBrandJavascript,
|
||||
IconCopy,
|
||||
IconCornerDownLeft,
|
||||
IconRefresh,
|
||||
} from "@tabler/icons-react"
|
||||
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupText,
|
||||
InputGroupTextarea,
|
||||
} from "@/registry/new-york-v4/ui/input-group"
|
||||
|
||||
export function InputGroupTextareaExample() {
|
||||
return (
|
||||
<div className="grid w-full max-w-md gap-4">
|
||||
<InputGroup>
|
||||
<InputGroupTextarea
|
||||
id="textarea-code-32"
|
||||
placeholder="console.log('Hello, world!');"
|
||||
className="min-h-[180px]"
|
||||
/>
|
||||
<InputGroupAddon align="block-end" className="border-t">
|
||||
<InputGroupText>Line 1, Column 1</InputGroupText>
|
||||
<InputGroupButton size="sm" className="ml-auto" variant="default">
|
||||
Run <IconCornerDownLeft />
|
||||
</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
<InputGroupAddon align="block-start" className="border-b">
|
||||
<InputGroupText className="font-mono font-medium">
|
||||
<IconBrandJavascript />
|
||||
script.js
|
||||
</InputGroupText>
|
||||
<InputGroupButton className="ml-auto">
|
||||
<IconRefresh />
|
||||
</InputGroupButton>
|
||||
<InputGroupButton variant="ghost">
|
||||
<IconCopy />
|
||||
</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
78
apps/v4/app/(app)/(root)/components/item-avatar.tsx
Normal file
78
apps/v4/app/(app)/(root)/components/item-avatar.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
import { Plus } from "lucide-react"
|
||||
|
||||
import {
|
||||
Avatar,
|
||||
AvatarFallback,
|
||||
AvatarImage,
|
||||
} from "@/registry/new-york-v4/ui/avatar"
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import {
|
||||
Item,
|
||||
ItemActions,
|
||||
ItemContent,
|
||||
ItemDescription,
|
||||
ItemMedia,
|
||||
ItemTitle,
|
||||
} from "@/registry/new-york-v4/ui/item"
|
||||
|
||||
export function ItemAvatar() {
|
||||
return (
|
||||
<div className="flex w-full max-w-lg flex-col gap-6">
|
||||
<Item variant="outline" className="hidden">
|
||||
<ItemMedia>
|
||||
<Avatar className="size-10">
|
||||
<AvatarImage src="https://github.com/maxleiter.png" />
|
||||
<AvatarFallback>LR</AvatarFallback>
|
||||
</Avatar>
|
||||
</ItemMedia>
|
||||
<ItemContent>
|
||||
<ItemTitle>Max Leiter</ItemTitle>
|
||||
<ItemDescription>Last seen 5 months ago</ItemDescription>
|
||||
</ItemContent>
|
||||
<ItemActions>
|
||||
<Button
|
||||
size="icon-sm"
|
||||
variant="outline"
|
||||
className="rounded-full"
|
||||
aria-label="Invite"
|
||||
>
|
||||
<Plus />
|
||||
</Button>
|
||||
</ItemActions>
|
||||
</Item>
|
||||
<Item variant="outline">
|
||||
<ItemMedia>
|
||||
<div className="*:data-[slot=avatar]:ring-background flex -space-x-2 *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:grayscale">
|
||||
<Avatar className="hidden sm:flex">
|
||||
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
|
||||
<AvatarFallback>CN</AvatarFallback>
|
||||
</Avatar>
|
||||
<Avatar className="hidden sm:flex">
|
||||
<AvatarImage
|
||||
src="https://github.com/maxleiter.png"
|
||||
alt="@maxleiter"
|
||||
/>
|
||||
<AvatarFallback>LR</AvatarFallback>
|
||||
</Avatar>
|
||||
<Avatar>
|
||||
<AvatarImage
|
||||
src="https://github.com/evilrabbit.png"
|
||||
alt="@evilrabbit"
|
||||
/>
|
||||
<AvatarFallback>ER</AvatarFallback>
|
||||
</Avatar>
|
||||
</div>
|
||||
</ItemMedia>
|
||||
<ItemContent>
|
||||
<ItemTitle>No Team Members</ItemTitle>
|
||||
<ItemDescription>Invite your team to collaborate.</ItemDescription>
|
||||
</ItemContent>
|
||||
<ItemActions>
|
||||
<Button size="sm" variant="outline">
|
||||
Invite
|
||||
</Button>
|
||||
</ItemActions>
|
||||
</Item>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
42
apps/v4/app/(app)/(root)/components/item-demo.tsx
Normal file
42
apps/v4/app/(app)/(root)/components/item-demo.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import { BadgeCheckIcon, ChevronRightIcon } from "lucide-react"
|
||||
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import {
|
||||
Item,
|
||||
ItemActions,
|
||||
ItemContent,
|
||||
ItemDescription,
|
||||
ItemMedia,
|
||||
ItemTitle,
|
||||
} from "@/registry/new-york-v4/ui/item"
|
||||
|
||||
export function ItemDemo() {
|
||||
return (
|
||||
<div className="flex w-full max-w-md flex-col gap-6">
|
||||
<Item variant="outline">
|
||||
<ItemContent>
|
||||
<ItemTitle>Two-factor authentication</ItemTitle>
|
||||
<ItemDescription className="text-pretty xl:hidden 2xl:block">
|
||||
Verify via email or phone number.
|
||||
</ItemDescription>
|
||||
</ItemContent>
|
||||
<ItemActions>
|
||||
<Button size="sm">Enable</Button>
|
||||
</ItemActions>
|
||||
</Item>
|
||||
<Item variant="outline" size="sm" asChild>
|
||||
<a href="#">
|
||||
<ItemMedia>
|
||||
<BadgeCheckIcon className="size-5" />
|
||||
</ItemMedia>
|
||||
<ItemContent>
|
||||
<ItemTitle>Your profile has been verified.</ItemTitle>
|
||||
</ItemContent>
|
||||
<ItemActions>
|
||||
<ChevronRightIcon className="size-4" />
|
||||
</ItemActions>
|
||||
</a>
|
||||
</Item>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
456
apps/v4/app/(app)/(root)/components/notion-prompt-form.tsx
Normal file
456
apps/v4/app/(app)/(root)/components/notion-prompt-form.tsx
Normal file
@@ -0,0 +1,456 @@
|
||||
"use client"
|
||||
|
||||
import { useMemo, useState } from "react"
|
||||
import {
|
||||
IconApps,
|
||||
IconArrowUp,
|
||||
IconAt,
|
||||
IconBook,
|
||||
IconCircleDashedPlus,
|
||||
IconPaperclip,
|
||||
IconPlus,
|
||||
IconWorld,
|
||||
IconX,
|
||||
} from "@tabler/icons-react"
|
||||
|
||||
import {
|
||||
Avatar,
|
||||
AvatarFallback,
|
||||
AvatarImage,
|
||||
} from "@/registry/new-york-v4/ui/avatar"
|
||||
import { Badge } from "@/registry/new-york-v4/ui/badge"
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@/registry/new-york-v4/ui/command"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/registry/new-york-v4/ui/dropdown-menu"
|
||||
import { Field, FieldLabel } from "@/registry/new-york-v4/ui/field"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupTextarea,
|
||||
} from "@/registry/new-york-v4/ui/input-group"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/registry/new-york-v4/ui/popover"
|
||||
import { Switch } from "@/registry/new-york-v4/ui/switch"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/registry/new-york-v4/ui/tooltip"
|
||||
|
||||
const SAMPLE_DATA = {
|
||||
mentionable: [
|
||||
{
|
||||
type: "page",
|
||||
title: "Meeting Notes",
|
||||
image: "📝",
|
||||
},
|
||||
{
|
||||
type: "page",
|
||||
title: "Project Dashboard",
|
||||
image: "📊",
|
||||
},
|
||||
{
|
||||
type: "page",
|
||||
title: "Ideas & Brainstorming",
|
||||
image: "💡",
|
||||
},
|
||||
{
|
||||
type: "page",
|
||||
title: "Calendar & Events",
|
||||
image: "📅",
|
||||
},
|
||||
{
|
||||
type: "page",
|
||||
title: "Documentation",
|
||||
image: "📚",
|
||||
},
|
||||
{
|
||||
type: "page",
|
||||
title: "Goals & Objectives",
|
||||
image: "🎯",
|
||||
},
|
||||
{
|
||||
type: "page",
|
||||
title: "Budget Planning",
|
||||
image: "💰",
|
||||
},
|
||||
{
|
||||
type: "page",
|
||||
title: "Team Directory",
|
||||
image: "👥",
|
||||
},
|
||||
{
|
||||
type: "page",
|
||||
title: "Technical Specs",
|
||||
image: "🔧",
|
||||
},
|
||||
{
|
||||
type: "page",
|
||||
title: "Analytics Report",
|
||||
image: "📈",
|
||||
},
|
||||
{
|
||||
type: "user",
|
||||
title: "shadcn",
|
||||
image: "https://github.com/shadcn.png",
|
||||
workspace: "Workspace",
|
||||
},
|
||||
{
|
||||
type: "user",
|
||||
title: "maxleiter",
|
||||
image: "https://github.com/maxleiter.png",
|
||||
workspace: "Workspace",
|
||||
},
|
||||
{
|
||||
type: "user",
|
||||
title: "evilrabbit",
|
||||
image: "https://github.com/evilrabbit.png",
|
||||
workspace: "Workspace",
|
||||
},
|
||||
],
|
||||
models: [
|
||||
{
|
||||
name: "Auto",
|
||||
},
|
||||
{
|
||||
name: "Agent Mode",
|
||||
badge: "Beta",
|
||||
},
|
||||
{
|
||||
name: "Plan Mode",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
function MentionableIcon({
|
||||
item,
|
||||
}: {
|
||||
item: (typeof SAMPLE_DATA.mentionable)[0]
|
||||
}) {
|
||||
return item.type === "page" ? (
|
||||
<span className="flex size-4 items-center justify-center">
|
||||
{item.image}
|
||||
</span>
|
||||
) : (
|
||||
<Avatar className="size-4">
|
||||
<AvatarImage src={item.image} />
|
||||
<AvatarFallback>{item.title[0]}</AvatarFallback>
|
||||
</Avatar>
|
||||
)
|
||||
}
|
||||
|
||||
export function NotionPromptForm() {
|
||||
const [mentions, setMentions] = useState<string[]>([])
|
||||
const [mentionPopoverOpen, setMentionPopoverOpen] = useState(false)
|
||||
const [modelPopoverOpen, setModelPopoverOpen] = useState(false)
|
||||
const [selectedModel, setSelectedModel] = useState<
|
||||
(typeof SAMPLE_DATA.models)[0]
|
||||
>(SAMPLE_DATA.models[0])
|
||||
const [scopeMenuOpen, setScopeMenuOpen] = useState(false)
|
||||
|
||||
const grouped = useMemo(() => {
|
||||
return SAMPLE_DATA.mentionable.reduce(
|
||||
(acc, item) => {
|
||||
const isAvailable = !mentions.includes(item.title)
|
||||
|
||||
if (isAvailable) {
|
||||
if (!acc[item.type]) {
|
||||
acc[item.type] = []
|
||||
}
|
||||
acc[item.type].push(item)
|
||||
}
|
||||
return acc
|
||||
},
|
||||
{} as Record<string, typeof SAMPLE_DATA.mentionable>
|
||||
)
|
||||
}, [mentions])
|
||||
|
||||
const hasMentions = mentions.length > 0
|
||||
|
||||
return (
|
||||
<form className="[--radius:1.2rem]">
|
||||
<Field>
|
||||
<FieldLabel htmlFor="notion-prompt" className="sr-only">
|
||||
Prompt
|
||||
</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupTextarea
|
||||
id="notion-prompt"
|
||||
placeholder="Ask, search, or make anything..."
|
||||
/>
|
||||
<InputGroupAddon align="block-start">
|
||||
<Popover
|
||||
open={mentionPopoverOpen}
|
||||
onOpenChange={setMentionPopoverOpen}
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
asChild
|
||||
onFocusCapture={(e) => e.stopPropagation()}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<InputGroupButton
|
||||
variant="outline"
|
||||
size={!hasMentions ? "sm" : "icon-sm"}
|
||||
className="rounded-full transition-transform"
|
||||
>
|
||||
<IconAt /> {!hasMentions && "Add context"}
|
||||
</InputGroupButton>
|
||||
</PopoverTrigger>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Mention a person, page, or date</TooltipContent>
|
||||
</Tooltip>
|
||||
<PopoverContent className="p-0 [--radius:1.2rem]" align="start">
|
||||
<Command>
|
||||
<CommandInput placeholder="Search pages..." />
|
||||
<CommandList>
|
||||
<CommandEmpty>No pages found</CommandEmpty>
|
||||
{Object.entries(grouped).map(([type, items]) => (
|
||||
<CommandGroup
|
||||
key={type}
|
||||
heading={type === "page" ? "Pages" : "Users"}
|
||||
>
|
||||
{items.map((item) => (
|
||||
<CommandItem
|
||||
key={item.title}
|
||||
value={item.title}
|
||||
onSelect={(currentValue) => {
|
||||
setMentions((prev) => [...prev, currentValue])
|
||||
setMentionPopoverOpen(false)
|
||||
}}
|
||||
>
|
||||
<MentionableIcon item={item} />
|
||||
{item.title}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
))}
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<div className="no-scrollbar -m-1.5 flex gap-1 overflow-y-auto p-1.5">
|
||||
{mentions.map((mention) => {
|
||||
const item = SAMPLE_DATA.mentionable.find(
|
||||
(item) => item.title === mention
|
||||
)
|
||||
|
||||
if (!item) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<InputGroupButton
|
||||
key={mention}
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
className="rounded-full !pl-2"
|
||||
onClick={() => {
|
||||
setMentions((prev) => prev.filter((m) => m !== mention))
|
||||
}}
|
||||
>
|
||||
<MentionableIcon item={item} />
|
||||
{item.title}
|
||||
<IconX />
|
||||
</InputGroupButton>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</InputGroupAddon>
|
||||
<InputGroupAddon align="block-end" className="gap-1">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<InputGroupButton
|
||||
size="icon-sm"
|
||||
className="rounded-full"
|
||||
aria-label="Attach file"
|
||||
>
|
||||
<IconPaperclip />
|
||||
</InputGroupButton>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Attach file</TooltipContent>
|
||||
</Tooltip>
|
||||
<DropdownMenu
|
||||
open={modelPopoverOpen}
|
||||
onOpenChange={setModelPopoverOpen}
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<InputGroupButton size="sm" className="rounded-full">
|
||||
{selectedModel.name}
|
||||
</InputGroupButton>
|
||||
</DropdownMenuTrigger>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Select AI model</TooltipContent>
|
||||
</Tooltip>
|
||||
<DropdownMenuContent
|
||||
side="top"
|
||||
align="start"
|
||||
className="[--radius:1rem]"
|
||||
>
|
||||
<DropdownMenuGroup className="w-42">
|
||||
<DropdownMenuLabel className="text-muted-foreground text-xs">
|
||||
Select Agent Mode
|
||||
</DropdownMenuLabel>
|
||||
{SAMPLE_DATA.models.map((model) => (
|
||||
<DropdownMenuCheckboxItem
|
||||
key={model.name}
|
||||
checked={model.name === selectedModel.name}
|
||||
onCheckedChange={(checked) => {
|
||||
if (checked) {
|
||||
setSelectedModel(model)
|
||||
}
|
||||
}}
|
||||
className="pl-2 *:[span:first-child]:right-2 *:[span:first-child]:left-auto"
|
||||
>
|
||||
{model.name}
|
||||
{model.badge && (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="h-5 rounded-sm bg-blue-100 px-1 text-xs text-blue-800 dark:bg-blue-900 dark:text-blue-100"
|
||||
>
|
||||
{model.badge}
|
||||
</Badge>
|
||||
)}
|
||||
</DropdownMenuCheckboxItem>
|
||||
))}
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<DropdownMenu open={scopeMenuOpen} onOpenChange={setScopeMenuOpen}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<InputGroupButton size="sm" className="rounded-full">
|
||||
<IconWorld /> All Sources
|
||||
</InputGroupButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
side="top"
|
||||
align="end"
|
||||
className="[--radius:1rem]"
|
||||
>
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem
|
||||
asChild
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
<label htmlFor="web-search">
|
||||
<IconWorld /> Web Search{" "}
|
||||
<Switch
|
||||
id="web-search"
|
||||
className="ml-auto"
|
||||
defaultChecked
|
||||
/>
|
||||
</label>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem
|
||||
asChild
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
<label htmlFor="apps">
|
||||
<IconApps /> Apps and Integrations
|
||||
<Switch id="apps" className="ml-auto" defaultChecked />
|
||||
</label>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<IconCircleDashedPlus /> All Sources I can access
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger>
|
||||
<Avatar className="size-4">
|
||||
<AvatarImage src="https://github.com/shadcn.png" />
|
||||
<AvatarFallback>CN</AvatarFallback>
|
||||
</Avatar>
|
||||
shadcn
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuSubContent className="w-72 p-0 [--radius:1rem]">
|
||||
<Command>
|
||||
<CommandInput
|
||||
placeholder="Find or use knowledge in..."
|
||||
autoFocus
|
||||
/>
|
||||
<CommandList>
|
||||
<CommandEmpty>No knowledge found</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{SAMPLE_DATA.mentionable
|
||||
.filter((item) => item.type === "user")
|
||||
.map((user) => (
|
||||
<CommandItem
|
||||
key={user.title}
|
||||
value={user.title}
|
||||
onSelect={() => {
|
||||
// Handle user selection here
|
||||
console.log("Selected user:", user.title)
|
||||
}}
|
||||
>
|
||||
<Avatar className="size-4">
|
||||
<AvatarImage src={user.image} />
|
||||
<AvatarFallback>
|
||||
{user.title[0]}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
{user.title}{" "}
|
||||
<span className="text-muted-foreground">
|
||||
- {user.workspace}
|
||||
</span>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuSub>
|
||||
<DropdownMenuItem>
|
||||
<IconBook /> Help Center
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>
|
||||
<IconPlus /> Connect Apps
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuLabel className="text-muted-foreground text-xs">
|
||||
We'll only search in the sources selected here.
|
||||
</DropdownMenuLabel>
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<InputGroupButton
|
||||
aria-label="Send"
|
||||
className="ml-auto rounded-full"
|
||||
variant="default"
|
||||
size="icon-sm"
|
||||
>
|
||||
<IconArrowUp />
|
||||
</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</Field>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
21
apps/v4/app/(app)/(root)/components/spinner-badge.tsx
Normal file
21
apps/v4/app/(app)/(root)/components/spinner-badge.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import { Badge } from "@/registry/new-york-v4/ui/badge"
|
||||
import { Spinner } from "@/registry/new-york-v4/ui/spinner"
|
||||
|
||||
export function SpinnerBadge() {
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge>
|
||||
<Spinner />
|
||||
Syncing
|
||||
</Badge>
|
||||
<Badge variant="secondary">
|
||||
<Spinner />
|
||||
Updating
|
||||
</Badge>
|
||||
<Badge variant="outline">
|
||||
<Spinner />
|
||||
Loading
|
||||
</Badge>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
31
apps/v4/app/(app)/(root)/components/spinner-empty.tsx
Normal file
31
apps/v4/app/(app)/(root)/components/spinner-empty.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import {
|
||||
Empty,
|
||||
EmptyContent,
|
||||
EmptyDescription,
|
||||
EmptyHeader,
|
||||
EmptyMedia,
|
||||
EmptyTitle,
|
||||
} from "@/registry/new-york-v4/ui/empty"
|
||||
import { Spinner } from "@/registry/new-york-v4/ui/spinner"
|
||||
|
||||
export function SpinnerEmpty() {
|
||||
return (
|
||||
<Empty className="w-full border md:p-6">
|
||||
<EmptyHeader>
|
||||
<EmptyMedia variant="icon">
|
||||
<Spinner />
|
||||
</EmptyMedia>
|
||||
<EmptyTitle>Processing your request</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
Please wait while we process your request. Do not refresh the page.
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
<EmptyContent>
|
||||
<Button variant="outline" size="sm">
|
||||
Cancel
|
||||
</Button>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
)
|
||||
}
|
||||
@@ -3,7 +3,6 @@ import Image from "next/image"
|
||||
import Link from "next/link"
|
||||
|
||||
import { Announcement } from "@/components/announcement"
|
||||
import { CardsDemo } from "@/components/cards"
|
||||
import { ExamplesNav } from "@/components/examples-nav"
|
||||
import {
|
||||
PageActions,
|
||||
@@ -15,6 +14,8 @@ import { PageNav } from "@/components/page-nav"
|
||||
import { ThemeSelector } from "@/components/theme-selector"
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
|
||||
import { RootComponents } from "./components"
|
||||
|
||||
const title = "The Foundation for your Design System"
|
||||
const description =
|
||||
"A set of beautifully designed components that you can customize, extend, and build on. Start here then make it your own. Open Source. Open Code."
|
||||
@@ -87,7 +88,7 @@ export default function IndexPage() {
|
||||
/>
|
||||
</section>
|
||||
<section className="theme-container hidden md:block">
|
||||
<CardsDemo />
|
||||
<RootComponents />
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { getAllBlockIds } from "@/lib/blocks"
|
||||
import { registryCategories } from "@/lib/categories"
|
||||
import { BlockDisplay } from "@/components/block-display"
|
||||
import { registryCategories } from "@/registry/registry-categories"
|
||||
import { getActiveStyle } from "@/registry/styles"
|
||||
|
||||
export const revalidate = false
|
||||
export const dynamic = "force-static"
|
||||
@@ -17,13 +18,16 @@ export default async function BlocksPage({
|
||||
}: {
|
||||
params: Promise<{ categories?: string[] }>
|
||||
}) {
|
||||
const { categories = [] } = await params
|
||||
const [{ categories = [] }, activeStyle] = await Promise.all([
|
||||
params,
|
||||
getActiveStyle(),
|
||||
])
|
||||
const blocks = await getAllBlockIds(["registry:block"], categories)
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-12 md:gap-24">
|
||||
{blocks.map((name) => (
|
||||
<BlockDisplay name={name} key={name} />
|
||||
<BlockDisplay name={name} key={name} styleName={activeStyle.name} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -2,6 +2,7 @@ import Link from "next/link"
|
||||
|
||||
import { BlockDisplay } from "@/components/block-display"
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import { getActiveStyle } from "@/registry/styles"
|
||||
|
||||
export const dynamic = "force-static"
|
||||
export const revalidate = false
|
||||
@@ -15,10 +16,12 @@ const FEATURED_BLOCKS = [
|
||||
]
|
||||
|
||||
export default async function BlocksPage() {
|
||||
const activeStyle = await getActiveStyle()
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-12 md:gap-24">
|
||||
{FEATURED_BLOCKS.map((name) => (
|
||||
<BlockDisplay name={name} key={name} />
|
||||
<BlockDisplay name={name} key={name} styleName={activeStyle.name} />
|
||||
))}
|
||||
<div className="container-wrapper">
|
||||
<div className="container flex justify-center py-6">
|
||||
|
||||
@@ -3,6 +3,7 @@ import { notFound } from "next/navigation"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { ChartDisplay } from "@/components/chart-display"
|
||||
import { getActiveStyle } from "@/registry/styles"
|
||||
import { charts } from "@/app/(app)/charts/charts"
|
||||
|
||||
export const revalidate = false
|
||||
@@ -41,6 +42,7 @@ export default async function ChartPage({ params }: ChartPageProps) {
|
||||
|
||||
const chartType = type as ChartType
|
||||
const chartList = charts[chartType]
|
||||
const activeStyle = await getActiveStyle()
|
||||
|
||||
return (
|
||||
<div className="grid flex-1 gap-12 lg:gap-24">
|
||||
@@ -54,6 +56,7 @@ export default async function ChartPage({ params }: ChartPageProps) {
|
||||
<ChartDisplay
|
||||
key={chart.id}
|
||||
name={chart.id}
|
||||
styleName={activeStyle.name}
|
||||
className={cn(chart.fullWidth && "md:col-span-2 lg:col-span-3")}
|
||||
>
|
||||
<chart.component />
|
||||
|
||||
@@ -6,7 +6,9 @@ import {
|
||||
IconArrowRight,
|
||||
IconArrowUpRight,
|
||||
} from "@tabler/icons-react"
|
||||
import { findNeighbour } from "fumadocs-core/server"
|
||||
import fm from "front-matter"
|
||||
import { findNeighbour } from "fumadocs-core/page-tree"
|
||||
import z from "zod"
|
||||
|
||||
import { source } from "@/lib/source"
|
||||
import { absoluteUrl } from "@/lib/utils"
|
||||
@@ -25,7 +27,7 @@ export function generateStaticParams() {
|
||||
}
|
||||
|
||||
export async function generateMetadata(props: {
|
||||
params: Promise<{ slug?: string[] }>
|
||||
params: Promise<{ slug: string[] }>
|
||||
}) {
|
||||
const params = await props.params
|
||||
const page = source.getPage(params.slug)
|
||||
@@ -73,7 +75,7 @@ export async function generateMetadata(props: {
|
||||
}
|
||||
|
||||
export default async function Page(props: {
|
||||
params: Promise<{ slug?: string[] }>
|
||||
params: Promise<{ slug: string[] }>
|
||||
}) {
|
||||
const params = await props.params
|
||||
const page = source.getPage(params.slug)
|
||||
@@ -82,18 +84,24 @@ export default async function Page(props: {
|
||||
}
|
||||
|
||||
const doc = page.data
|
||||
// @ts-expect-error - revisit fumadocs types.
|
||||
const MDX = doc.body
|
||||
const neighbours = await findNeighbour(source.pageTree, page.url)
|
||||
const neighbours = findNeighbour(source.pageTree, page.url)
|
||||
|
||||
// @ts-expect-error - revisit fumadocs types.
|
||||
const links = doc.links
|
||||
const raw = await page.data.getText("raw")
|
||||
const { attributes } = fm(raw)
|
||||
const { links } = z
|
||||
.object({
|
||||
links: z
|
||||
.object({
|
||||
doc: z.string().optional(),
|
||||
api: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
.parse(attributes)
|
||||
|
||||
return (
|
||||
<div
|
||||
data-slot="docs"
|
||||
className="flex items-stretch text-[1.05rem] sm:text-[15px] xl:w-full"
|
||||
>
|
||||
<div className="flex items-stretch text-[1.05rem] sm:text-[15px] xl:w-full">
|
||||
<div className="flex min-w-0 flex-1 flex-col">
|
||||
<div className="h-(--top-spacing) shrink-0" />
|
||||
<div className="mx-auto flex w-full max-w-2xl min-w-0 flex-1 flex-col gap-8 px-4 py-6 text-neutral-800 md:px-0 lg:py-8 dark:text-neutral-300">
|
||||
@@ -104,11 +112,7 @@ export default async function Page(props: {
|
||||
{doc.title}
|
||||
</h1>
|
||||
<div className="docs-nav bg-background/80 border-border/50 fixed inset-x-0 bottom-0 isolate z-50 flex items-center gap-2 border-t px-6 py-4 backdrop-blur-sm sm:static sm:z-0 sm:border-t-0 sm:bg-transparent sm:px-0 sm:pt-1.5 sm:backdrop-blur-none">
|
||||
<DocsCopyPage
|
||||
// @ts-expect-error - revisit fumadocs types.
|
||||
page={doc.content}
|
||||
url={absoluteUrl(page.url)}
|
||||
/>
|
||||
<DocsCopyPage page={raw} url={absoluteUrl(page.url)} />
|
||||
{neighbours.previous && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
@@ -144,19 +148,19 @@ export default async function Page(props: {
|
||||
)}
|
||||
</div>
|
||||
{links ? (
|
||||
<div className="flex items-center space-x-2 pt-4">
|
||||
<div className="flex items-center gap-2 pt-4">
|
||||
{links?.doc && (
|
||||
<Badge asChild variant="secondary">
|
||||
<Link href={links.doc} target="_blank" rel="noreferrer">
|
||||
<Badge asChild variant="secondary" className="rounded-full">
|
||||
<a href={links.doc} target="_blank" rel="noreferrer">
|
||||
Docs <IconArrowUpRight />
|
||||
</Link>
|
||||
</a>
|
||||
</Badge>
|
||||
)}
|
||||
{links?.api && (
|
||||
<Badge asChild variant="secondary">
|
||||
<Link href={links.api} target="_blank" rel="noreferrer">
|
||||
<Badge asChild variant="secondary" className="rounded-full">
|
||||
<a href={links.api} target="_blank" rel="noreferrer">
|
||||
API Reference <IconArrowUpRight />
|
||||
</Link>
|
||||
</a>
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
@@ -195,10 +199,8 @@ export default async function Page(props: {
|
||||
</div>
|
||||
<div className="sticky top-[calc(var(--header-height)+1px)] z-30 ml-auto hidden h-[calc(100svh-var(--footer-height)+2rem)] w-72 flex-col gap-4 overflow-hidden overscroll-none pb-8 xl:flex">
|
||||
<div className="h-(--top-spacing) shrink-0" />
|
||||
{/* @ts-expect-error - revisit fumadocs types. */}
|
||||
{doc.toc?.length ? (
|
||||
<div className="no-scrollbar overflow-y-auto px-8">
|
||||
{/* @ts-expect-error - revisit fumadocs types. */}
|
||||
<DocsTableOfContents toc={doc.toc} />
|
||||
<div className="h-12" />
|
||||
</div>
|
||||
|
||||
@@ -5,8 +5,14 @@ import * as React from "react"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Icons } from "@/components/icons"
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import {
|
||||
Field,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldSeparator,
|
||||
} from "@/registry/new-york-v4/ui/field"
|
||||
import { Input } from "@/registry/new-york-v4/ui/input"
|
||||
import { Label } from "@/registry/new-york-v4/ui/label"
|
||||
import { Spinner } from "@/registry/new-york-v4/ui/spinner"
|
||||
|
||||
export function UserAuthForm({
|
||||
className,
|
||||
@@ -26,11 +32,11 @@ export function UserAuthForm({
|
||||
return (
|
||||
<div className={cn("grid gap-6", className)} {...props}>
|
||||
<form onSubmit={onSubmit}>
|
||||
<div className="grid gap-2">
|
||||
<div className="grid gap-1">
|
||||
<Label className="sr-only" htmlFor="email">
|
||||
<FieldGroup>
|
||||
<Field>
|
||||
<FieldLabel className="sr-only" htmlFor="email">
|
||||
Email
|
||||
</Label>
|
||||
</FieldLabel>
|
||||
<Input
|
||||
id="email"
|
||||
placeholder="name@example.com"
|
||||
@@ -40,31 +46,18 @@ export function UserAuthForm({
|
||||
autoCorrect="off"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</div>
|
||||
<Button disabled={isLoading}>
|
||||
{isLoading && (
|
||||
<Icons.spinner className="mr-2 h-4 w-4 animate-spin" />
|
||||
)}
|
||||
Sign In with Email
|
||||
</Button>
|
||||
</div>
|
||||
</Field>
|
||||
<Field>
|
||||
<Button disabled={isLoading}>
|
||||
{isLoading && <Spinner />}
|
||||
Sign In with Email
|
||||
</Button>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</form>
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
<span className="w-full border-t" />
|
||||
</div>
|
||||
<div className="relative flex justify-center text-xs uppercase">
|
||||
<span className="bg-background text-muted-foreground px-2">
|
||||
Or continue with
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<FieldSeparator>Or continue with</FieldSeparator>
|
||||
<Button variant="outline" type="button" disabled={isLoading}>
|
||||
{isLoading ? (
|
||||
<Icons.spinner className="mr-2 h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Icons.gitHub className="mr-2 h-4 w-4" />
|
||||
)}{" "}
|
||||
{isLoading ? <Spinner /> : <Icons.gitHub className="mr-2 h-4 w-4" />}{" "}
|
||||
GitHub
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,7 @@ import Link from "next/link"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { buttonVariants } from "@/registry/new-york-v4/ui/button"
|
||||
import { FieldDescription } from "@/registry/new-york-v4/ui/field"
|
||||
import { UserAuthForm } from "@/app/(app)/examples/authentication/components/user-auth-form"
|
||||
|
||||
export const metadata: Metadata = {
|
||||
@@ -78,23 +79,11 @@ export default function AuthenticationPage() {
|
||||
</p>
|
||||
</div>
|
||||
<UserAuthForm />
|
||||
<p className="text-muted-foreground px-8 text-center text-sm">
|
||||
<FieldDescription className="px-6 text-center">
|
||||
By clicking continue, you agree to our{" "}
|
||||
<Link
|
||||
href="/terms"
|
||||
className="hover:text-primary underline underline-offset-4"
|
||||
>
|
||||
Terms of Service
|
||||
</Link>{" "}
|
||||
and{" "}
|
||||
<Link
|
||||
href="/privacy"
|
||||
className="hover:text-primary underline underline-offset-4"
|
||||
>
|
||||
Privacy Policy
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
<Link href="/terms">Terms of Service</Link> and{" "}
|
||||
<Link href="/privacy">Privacy Policy</Link>.
|
||||
</FieldDescription>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -142,13 +142,7 @@ const chartConfig = {
|
||||
|
||||
export function ChartAreaInteractive() {
|
||||
const isMobile = useIsMobile()
|
||||
const [timeRange, setTimeRange] = React.useState("90d")
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isMobile) {
|
||||
setTimeRange("7d")
|
||||
}
|
||||
}, [isMobile])
|
||||
const [timeRange, setTimeRange] = React.useState("7d")
|
||||
|
||||
const filteredData = chartData.filter((item) => {
|
||||
const date = new Date(item.date)
|
||||
|
||||
@@ -16,8 +16,9 @@ import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
export const dynamic = "force-static"
|
||||
export const revalidate = false
|
||||
|
||||
const title = "Examples"
|
||||
const description = "Check out some examples app built using the components."
|
||||
const title = "The Foundation for your Design System"
|
||||
const description =
|
||||
"A set of beautifully designed components that you can customize, extend, and build on. Start here then make it your own. Open Source. Open Code."
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title,
|
||||
@@ -52,24 +53,20 @@ export default function ExamplesLayout({
|
||||
<>
|
||||
<PageHeader>
|
||||
<Announcement />
|
||||
<PageHeaderHeading>Build your Component Library</PageHeaderHeading>
|
||||
<PageHeaderDescription>
|
||||
A set of beautifully-designed, accessible components and a code
|
||||
distribution platform. Works with your favorite frameworks. Open
|
||||
Source. Open Code.
|
||||
</PageHeaderDescription>
|
||||
<PageHeaderHeading className="max-w-4xl">{title}</PageHeaderHeading>
|
||||
<PageHeaderDescription>{description}</PageHeaderDescription>
|
||||
<PageActions>
|
||||
<Button asChild size="sm">
|
||||
<Link href="/docs">Get Started</Link>
|
||||
<Link href="/docs/installation">Get Started</Link>
|
||||
</Button>
|
||||
<Button asChild size="sm" variant="ghost">
|
||||
<Link href="/blocks">Browse Blocks</Link>
|
||||
<Link href="/docs/components">View Components</Link>
|
||||
</Button>
|
||||
</PageActions>
|
||||
</PageHeader>
|
||||
<PageNav id="examples">
|
||||
<PageNav id="examples" className="hidden md:flex">
|
||||
<ExamplesNav className="[&>a:first-child]:text-primary flex-1 overflow-hidden" />
|
||||
<ThemeSelector className="mr-4 hidden md:block" />
|
||||
<ThemeSelector className="mr-4 hidden md:flex" />
|
||||
</PageNav>
|
||||
<div className="container-wrapper section-soft flex flex-1 flex-col pb-6">
|
||||
<div className="theme-container container flex flex-1 scroll-mt-20 flex-col">
|
||||
|
||||
@@ -3,7 +3,10 @@ import { SiteHeader } from "@/components/site-header"
|
||||
|
||||
export default function AppLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="bg-background relative z-10 flex min-h-svh flex-col">
|
||||
<div
|
||||
data-slot="layout"
|
||||
className="bg-background relative z-10 flex min-h-svh flex-col"
|
||||
>
|
||||
<SiteHeader />
|
||||
<main className="flex flex-1 flex-col">{children}</main>
|
||||
<SiteFooter />
|
||||
|
||||
@@ -1,23 +1,30 @@
|
||||
import { notFound } from "next/navigation"
|
||||
import { NextResponse, type NextRequest } from "next/server"
|
||||
|
||||
import { processMdxForLLMs } from "@/lib/llm"
|
||||
import { source } from "@/lib/source"
|
||||
import { getActiveStyle } from "@/registry/styles"
|
||||
|
||||
export const revalidate = false
|
||||
|
||||
export async function GET(
|
||||
_req: NextRequest,
|
||||
{ params }: { params: Promise<{ slug: string[] }> }
|
||||
{ params }: { params: Promise<{ slug?: string[] }> }
|
||||
) {
|
||||
const slug = (await params).slug
|
||||
const [{ slug }, activeStyle] = await Promise.all([params, getActiveStyle()])
|
||||
|
||||
const page = source.getPage(slug)
|
||||
|
||||
if (!page) {
|
||||
notFound()
|
||||
}
|
||||
|
||||
// @ts-expect-error - revisit fumadocs types.
|
||||
return new NextResponse(page.data.content, {
|
||||
const processedContent = processMdxForLLMs(
|
||||
await page.data.getText("raw"),
|
||||
activeStyle.name
|
||||
)
|
||||
|
||||
return new NextResponse(processedContent, {
|
||||
headers: {
|
||||
"Content-Type": "text/markdown; charset=utf-8",
|
||||
},
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
import Image from "next/image"
|
||||
import { CheckIcon } from "lucide-react"
|
||||
|
||||
import {
|
||||
Field,
|
||||
FieldContent,
|
||||
FieldDescription,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldLegend,
|
||||
FieldSeparator,
|
||||
FieldSet,
|
||||
FieldTitle,
|
||||
} from "@/registry/new-york-v4/ui/field"
|
||||
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 { Switch } from "@/registry/new-york-v4/ui/switch"
|
||||
|
||||
const modes = [
|
||||
{
|
||||
name: "Light",
|
||||
value: "light",
|
||||
image: "/placeholder.svg",
|
||||
},
|
||||
{
|
||||
name: "Dark",
|
||||
value: "dark",
|
||||
image: "/placeholder.svg",
|
||||
},
|
||||
{
|
||||
name: "System",
|
||||
value: "system",
|
||||
image: "/placeholder.svg",
|
||||
},
|
||||
]
|
||||
|
||||
const accents = [
|
||||
{
|
||||
name: "Blue",
|
||||
value: "#007AFF",
|
||||
},
|
||||
{
|
||||
name: "Purple",
|
||||
value: "#6A4695",
|
||||
},
|
||||
{
|
||||
name: "Red",
|
||||
value: "#FF3B30",
|
||||
},
|
||||
{
|
||||
name: "Orange",
|
||||
value: "#FF9500",
|
||||
},
|
||||
]
|
||||
|
||||
export function AppearanceSettings() {
|
||||
return (
|
||||
<FieldSet>
|
||||
<FieldLegend>Appearance</FieldLegend>
|
||||
<FieldDescription>
|
||||
Configure appearance. accent, scroll bar, and more.
|
||||
</FieldDescription>
|
||||
<FieldGroup>
|
||||
<FieldSet>
|
||||
<FieldLegend variant="label">Mode</FieldLegend>
|
||||
<FieldDescription>
|
||||
Select the mode to use for the appearance.
|
||||
</FieldDescription>
|
||||
<RadioGroup
|
||||
className="flex flex-col gap-4 @min-[28rem]/field-group:grid @min-[28rem]/field-group:grid-cols-3"
|
||||
defaultValue="light"
|
||||
>
|
||||
{modes.map((mode) => (
|
||||
<FieldLabel
|
||||
htmlFor={mode.value}
|
||||
className="gap-0 overflow-hidden"
|
||||
key={mode.value}
|
||||
>
|
||||
<Image
|
||||
src={mode.image}
|
||||
alt={mode.name}
|
||||
width={160}
|
||||
height={90}
|
||||
className="hidden aspect-video w-full object-cover @min-[28rem]/field-group:block dark:brightness-[0.2] dark:grayscale"
|
||||
/>
|
||||
<Field
|
||||
orientation="horizontal"
|
||||
className="@min-[28rem]/field-group:border-t-input @min-[28rem]/field-group:border-t"
|
||||
>
|
||||
<FieldTitle>{mode.name}</FieldTitle>
|
||||
<RadioGroupItem id={mode.value} value={mode.value} />
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</FieldSet>
|
||||
<FieldSeparator />
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldTitle>Accent</FieldTitle>
|
||||
<FieldDescription>
|
||||
Select the accent color to use for the appearance.
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<FieldSet aria-label="Accent">
|
||||
<RadioGroup className="flex flex-wrap gap-2" defaultValue="#007AFF">
|
||||
{accents.map((accent) => (
|
||||
<Label
|
||||
htmlFor={accent.value}
|
||||
key={accent.value}
|
||||
className="flex size-6 items-center justify-center rounded-full"
|
||||
style={{ backgroundColor: accent.value }}
|
||||
>
|
||||
<RadioGroupItem
|
||||
id={accent.value}
|
||||
value={accent.value}
|
||||
aria-label={accent.name}
|
||||
className="peer sr-only"
|
||||
/>
|
||||
<CheckIcon className="hidden size-4 stroke-white peer-data-[state=checked]:block" />
|
||||
</Label>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</FieldSet>
|
||||
</Field>
|
||||
<FieldSeparator />
|
||||
<Field orientation="responsive">
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor="icon-size">Sidebar Icon Size</FieldLabel>
|
||||
<FieldDescription>
|
||||
Select the size of the sidebar icons.
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<Select>
|
||||
<SelectTrigger id="icon-size" className="ml-auto">
|
||||
<SelectValue placeholder="Select" />
|
||||
</SelectTrigger>
|
||||
<SelectContent align="end">
|
||||
<SelectItem value="small">Small</SelectItem>
|
||||
<SelectItem value="medium">Medium</SelectItem>
|
||||
<SelectItem value="large">Large</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
<FieldSeparator />
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor="tinting">Wallpaper Tinting</FieldLabel>
|
||||
<FieldDescription>
|
||||
Allow the wallpaper to be tinted with the accent color.
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<Switch id="tinting" defaultChecked />
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</FieldSet>
|
||||
)
|
||||
}
|
||||
463
apps/v4/app/(internal)/sink/(pages)/forms/chat-settings.tsx
Normal file
463
apps/v4/app/(internal)/sink/(pages)/forms/chat-settings.tsx
Normal file
@@ -0,0 +1,463 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { CircleIcon, InfoIcon } from "lucide-react"
|
||||
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import { Checkbox } from "@/registry/new-york-v4/ui/checkbox"
|
||||
import {
|
||||
Field,
|
||||
FieldContent,
|
||||
FieldDescription,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldSeparator,
|
||||
FieldSet,
|
||||
FieldTitle,
|
||||
} from "@/registry/new-york-v4/ui/field"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupInput,
|
||||
} from "@/registry/new-york-v4/ui/input-group"
|
||||
import { Kbd } from "@/registry/new-york-v4/ui/kbd"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectSeparator,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/registry/new-york-v4/ui/select"
|
||||
import { Switch } from "@/registry/new-york-v4/ui/switch"
|
||||
import {
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from "@/registry/new-york-v4/ui/tabs"
|
||||
import { Textarea } from "@/registry/new-york-v4/ui/textarea"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/registry/new-york-v4/ui/tooltip"
|
||||
|
||||
const spokenLanguages = [
|
||||
{ label: "English", value: "en" },
|
||||
{ label: "Spanish", value: "es" },
|
||||
{ label: "French", value: "fr" },
|
||||
{ label: "German", value: "de" },
|
||||
{ label: "Italian", value: "it" },
|
||||
{ label: "Portuguese", value: "pt" },
|
||||
{ label: "Russian", value: "ru" },
|
||||
{ label: "Chinese", value: "zh" },
|
||||
{ label: "Japanese", value: "ja" },
|
||||
{ label: "Korean", value: "ko" },
|
||||
{ label: "Arabic", value: "ar" },
|
||||
{ label: "Hindi", value: "hi" },
|
||||
{ label: "Bengali", value: "bn" },
|
||||
{ label: "Telugu", value: "te" },
|
||||
{ label: "Marathi", value: "mr" },
|
||||
{ label: "Kannada", value: "kn" },
|
||||
{ label: "Malayalam", value: "ml" },
|
||||
]
|
||||
|
||||
const voices = [
|
||||
{ label: "Samantha", value: "samantha" },
|
||||
{ label: "Alex", value: "alex" },
|
||||
{ label: "Fred", value: "fred" },
|
||||
{ label: "Victoria", value: "victoria" },
|
||||
{ label: "Tom", value: "tom" },
|
||||
{ label: "Karen", value: "karen" },
|
||||
{ label: "Sam", value: "sam" },
|
||||
{ label: "Daniel", value: "daniel" },
|
||||
]
|
||||
|
||||
const personalities = [
|
||||
{
|
||||
label: "Friendly",
|
||||
value: "friendly",
|
||||
description: "Friendly and approachable.",
|
||||
},
|
||||
{
|
||||
label: "Professional",
|
||||
value: "professional",
|
||||
description: "Professional and authoritative.",
|
||||
},
|
||||
{ label: "Funny", value: "funny", description: "Funny and light-hearted." },
|
||||
{
|
||||
label: "Sarcastic",
|
||||
value: "sarcastic",
|
||||
description: "Sarcastic and witty.",
|
||||
},
|
||||
{ label: "Cynical", value: "cynical", description: "Cynical and skeptical." },
|
||||
]
|
||||
|
||||
const instructions = [
|
||||
{
|
||||
label: "Witty",
|
||||
value: "witty",
|
||||
description: "Use quick and clever responses when appropriate.",
|
||||
},
|
||||
{
|
||||
label: "Professional",
|
||||
value: "professional",
|
||||
description: "Have a professional and authoritative tone.",
|
||||
},
|
||||
{
|
||||
label: "Funny",
|
||||
value: "funny",
|
||||
description: "Use humor and wit to engage the user.",
|
||||
},
|
||||
{
|
||||
label: "Sarcastic",
|
||||
value: "sarcastic",
|
||||
description: "Use sarcasm and wit to engage the user.",
|
||||
},
|
||||
{
|
||||
label: "Cynical",
|
||||
value: "cynical",
|
||||
description: "Use cynicism and skepticism to engage the user.",
|
||||
},
|
||||
]
|
||||
|
||||
export function ChatSettings() {
|
||||
const [tab, setTab] = useState("general")
|
||||
const [theme, setTheme] = useState("system")
|
||||
const [accentColor, setAccentColor] = useState("default")
|
||||
const [spokenLanguage, setSpokenLanguage] = useState("en")
|
||||
const [voice, setVoice] = useState("samantha")
|
||||
const [personality, setPersonality] = useState("friendly")
|
||||
const [customInstructions, setCustomInstructions] = useState("")
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<Button variant="outline" asChild className="w-full md:hidden">
|
||||
<select
|
||||
value={tab}
|
||||
onChange={(e) => setTab(e.target.value)}
|
||||
className="appearance-none"
|
||||
>
|
||||
<option value="general">General</option>
|
||||
<option value="notifications">Notifications</option>
|
||||
<option value="personalization">Personalization</option>
|
||||
<option value="security">Security</option>
|
||||
</select>
|
||||
</Button>
|
||||
<Tabs value={tab} onValueChange={setTab}>
|
||||
<TabsList className="hidden md:flex">
|
||||
<TabsTrigger value="general">General</TabsTrigger>
|
||||
<TabsTrigger value="notifications">Notifications</TabsTrigger>
|
||||
<TabsTrigger value="personalization">Personalization</TabsTrigger>
|
||||
<TabsTrigger value="security">Security</TabsTrigger>
|
||||
</TabsList>
|
||||
<div className="rounded-lg border p-6 [&_[data-slot=select-trigger]]:min-w-[125px]">
|
||||
<TabsContent value="general">
|
||||
<FieldSet>
|
||||
<FieldGroup>
|
||||
<Field orientation="horizontal">
|
||||
<FieldLabel htmlFor="theme">Theme</FieldLabel>
|
||||
<Select value={theme} onValueChange={setTheme}>
|
||||
<SelectTrigger id="theme">
|
||||
<SelectValue placeholder="Select" />
|
||||
</SelectTrigger>
|
||||
<SelectContent align="end">
|
||||
<SelectItem value="light">Light</SelectItem>
|
||||
<SelectItem value="dark">Dark</SelectItem>
|
||||
<SelectItem value="system">System</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
<FieldSeparator />
|
||||
<Field orientation="horizontal">
|
||||
<FieldLabel htmlFor="accent-color">Accent Color</FieldLabel>
|
||||
<Select value={accentColor} onValueChange={setAccentColor}>
|
||||
<SelectTrigger id="accent-color">
|
||||
<SelectValue placeholder="Select" />
|
||||
</SelectTrigger>
|
||||
<SelectContent align="end">
|
||||
<SelectItem value="default">
|
||||
<CircleIcon className="fill-neutral-500 stroke-neutral-500 dark:fill-neutral-400 dark:stroke-neutral-400" />
|
||||
Default
|
||||
</SelectItem>
|
||||
<SelectItem value="red">
|
||||
<CircleIcon className="fill-red-500 stroke-red-500 dark:fill-red-400 dark:stroke-red-400" />
|
||||
Red
|
||||
</SelectItem>
|
||||
<SelectItem value="blue">
|
||||
<CircleIcon className="fill-blue-500 stroke-blue-500 dark:fill-blue-400 dark:stroke-blue-400" />
|
||||
Blue
|
||||
</SelectItem>
|
||||
<SelectItem value="green">
|
||||
<CircleIcon className="fill-green-500 stroke-green-500 dark:fill-green-400 dark:stroke-green-400" />
|
||||
Green
|
||||
</SelectItem>
|
||||
<SelectItem value="purple">
|
||||
<CircleIcon className="fill-purple-500 stroke-purple-500 dark:fill-purple-400 dark:stroke-purple-400" />
|
||||
Purple
|
||||
</SelectItem>
|
||||
<SelectItem value="pink">
|
||||
<CircleIcon className="fill-pink-500 stroke-pink-500 dark:fill-pink-400 dark:stroke-pink-400" />
|
||||
Pink
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
<FieldSeparator />
|
||||
<Field orientation="responsive">
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor="spoken-language">
|
||||
Spoken Language
|
||||
</FieldLabel>
|
||||
<FieldDescription>
|
||||
For best results, select the language you mainly speak. If
|
||||
it's not listed, it may still be supported via
|
||||
auto-detection.
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<Select
|
||||
value={spokenLanguage}
|
||||
onValueChange={setSpokenLanguage}
|
||||
>
|
||||
<SelectTrigger id="spoken-language">
|
||||
<SelectValue placeholder="Select" />
|
||||
</SelectTrigger>
|
||||
<SelectContent align="end" position="item-aligned">
|
||||
<SelectItem value="auto">Auto</SelectItem>
|
||||
<SelectSeparator />
|
||||
{spokenLanguages.map((language) => (
|
||||
<SelectItem key={language.value} value={language.value}>
|
||||
{language.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
<FieldSeparator />
|
||||
<Field orientation="horizontal">
|
||||
<FieldLabel htmlFor="voice">Voice</FieldLabel>
|
||||
<Select value={voice} onValueChange={setVoice}>
|
||||
<SelectTrigger id="voice">
|
||||
<SelectValue placeholder="Select" />
|
||||
</SelectTrigger>
|
||||
<SelectContent align="end" position="item-aligned">
|
||||
{voices.map((voice) => (
|
||||
<SelectItem key={voice.value} value={voice.value}>
|
||||
{voice.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</FieldSet>
|
||||
</TabsContent>
|
||||
<TabsContent value="notifications">
|
||||
<FieldGroup>
|
||||
<FieldSet>
|
||||
<FieldLabel>Responses</FieldLabel>
|
||||
<FieldDescription>
|
||||
Get notified when ChatGPT responds to requests that take time,
|
||||
like research or image generation.
|
||||
</FieldDescription>
|
||||
<FieldGroup data-slot="checkbox-group">
|
||||
<Field orientation="horizontal">
|
||||
<Checkbox id="push" defaultChecked disabled />
|
||||
<FieldLabel htmlFor="push" className="font-normal">
|
||||
Push notifications
|
||||
</FieldLabel>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</FieldSet>
|
||||
<FieldSeparator />
|
||||
<FieldSet>
|
||||
<FieldLabel>Tasks</FieldLabel>
|
||||
<FieldDescription>
|
||||
Get notified when tasks you've created have updates.{" "}
|
||||
<a href="#">Manage tasks</a>
|
||||
</FieldDescription>
|
||||
<FieldGroup data-slot="checkbox-group">
|
||||
<Field orientation="horizontal">
|
||||
<Checkbox id="push-tasks" />
|
||||
<FieldLabel htmlFor="push-tasks" className="font-normal">
|
||||
Push notifications
|
||||
</FieldLabel>
|
||||
</Field>
|
||||
<Field orientation="horizontal">
|
||||
<Checkbox id="email-tasks" />
|
||||
<FieldLabel htmlFor="email-tasks" className="font-normal">
|
||||
Email notifications
|
||||
</FieldLabel>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</FieldSet>
|
||||
</FieldGroup>
|
||||
</TabsContent>
|
||||
<TabsContent value="personalization">
|
||||
<FieldGroup>
|
||||
<Field orientation="responsive">
|
||||
<FieldLabel htmlFor="nickname">Nickname</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupInput
|
||||
id="nickname"
|
||||
placeholder="Broski"
|
||||
className="@md/field-group:max-w-[200px]"
|
||||
/>
|
||||
<InputGroupAddon align="inline-end">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<InputGroupButton size="icon-xs">
|
||||
<InfoIcon />
|
||||
</InputGroupButton>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="flex items-center gap-2">
|
||||
Used to identify you in the chat. <Kbd>N</Kbd>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</Field>
|
||||
<FieldSeparator />
|
||||
<Field
|
||||
orientation="responsive"
|
||||
className="@md/field-group:flex-col @2xl/field-group:flex-row"
|
||||
>
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor="about">More about you</FieldLabel>
|
||||
<FieldDescription>
|
||||
Tell us more about yourself. This will be used to help us
|
||||
personalize your experience.
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<Textarea
|
||||
id="about"
|
||||
placeholder="I'm a software engineer..."
|
||||
className="min-h-[120px] @md/field-group:min-w-full @2xl/field-group:min-w-[300px]"
|
||||
/>
|
||||
</Field>
|
||||
<FieldSeparator />
|
||||
<FieldLabel>
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor="customization">
|
||||
Enable customizations
|
||||
</FieldLabel>
|
||||
<FieldDescription>
|
||||
Enable customizations to make ChatGPT more personalized.
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<Switch id="customization" defaultChecked />
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
<FieldSeparator />
|
||||
<Field orientation="responsive">
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor="personality">
|
||||
ChatGPT Personality
|
||||
</FieldLabel>
|
||||
<FieldDescription>
|
||||
Set the style and tone ChatGPT should use when responding.
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<Select value={personality} onValueChange={setPersonality}>
|
||||
<SelectTrigger id="personality">
|
||||
{personalities.find((p) => p.value === personality)?.label}
|
||||
</SelectTrigger>
|
||||
<SelectContent align="end">
|
||||
{personalities.map((personality) => (
|
||||
<SelectItem
|
||||
key={personality.value}
|
||||
value={personality.value}
|
||||
>
|
||||
<FieldContent className="gap-0.5">
|
||||
<FieldLabel>{personality.label}</FieldLabel>
|
||||
<FieldDescription className="text-xs">
|
||||
{personality.description}
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
<FieldSeparator />
|
||||
<Field>
|
||||
<FieldLabel htmlFor="instructions">
|
||||
Custom Instructions
|
||||
</FieldLabel>
|
||||
<Textarea
|
||||
id="instructions"
|
||||
value={customInstructions}
|
||||
onChange={(e) => setCustomInstructions(e.target.value)}
|
||||
/>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{instructions.map((instruction) => (
|
||||
<Button
|
||||
variant="outline"
|
||||
key={instruction.value}
|
||||
value={instruction.value}
|
||||
className="rounded-full"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
setCustomInstructions(
|
||||
`${customInstructions} ${instruction.description}`
|
||||
)
|
||||
}
|
||||
>
|
||||
{instruction.label}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</TabsContent>
|
||||
<TabsContent value="security">
|
||||
<FieldGroup>
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor="2fa">
|
||||
Multi-factor authentication
|
||||
</FieldLabel>
|
||||
<FieldDescription>
|
||||
Enable multi-factor authentication to secure your account.
|
||||
If you do not have a two-factor authentication device, you
|
||||
can use a one-time code sent to your email.
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<Switch id="2fa" />
|
||||
</Field>
|
||||
<FieldSeparator />
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldTitle>Log out</FieldTitle>
|
||||
<FieldDescription>
|
||||
Log out of your account on this device.
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<Button variant="outline" size="sm">
|
||||
Log Out
|
||||
</Button>
|
||||
</Field>
|
||||
<FieldSeparator />
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldTitle>Log out of all devices</FieldTitle>
|
||||
<FieldDescription>
|
||||
This will log you out of all devices, including the current
|
||||
session. It may take up to 30 minutes for the changes to
|
||||
take effect.
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<Button variant="outline" size="sm">
|
||||
Log Out All
|
||||
</Button>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</TabsContent>
|
||||
</div>
|
||||
</Tabs>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
137
apps/v4/app/(internal)/sink/(pages)/forms/display-settings.tsx
Normal file
137
apps/v4/app/(internal)/sink/(pages)/forms/display-settings.tsx
Normal file
@@ -0,0 +1,137 @@
|
||||
import { SunDimIcon, SunIcon } from "lucide-react"
|
||||
|
||||
import { Checkbox } from "@/registry/new-york-v4/ui/checkbox"
|
||||
import {
|
||||
Field,
|
||||
FieldContent,
|
||||
FieldDescription,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldLegend,
|
||||
FieldSeparator,
|
||||
FieldSet,
|
||||
FieldTitle,
|
||||
} from "@/registry/new-york-v4/ui/field"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/registry/new-york-v4/ui/select"
|
||||
import { Slider } from "@/registry/new-york-v4/ui/slider"
|
||||
import { Switch } from "@/registry/new-york-v4/ui/switch"
|
||||
|
||||
export function DisplaySettings() {
|
||||
return (
|
||||
<FieldSet>
|
||||
<FieldLegend>Display</FieldLegend>
|
||||
<FieldDescription>
|
||||
Configure display settings, brightness, refresh rate, and more.
|
||||
</FieldDescription>
|
||||
<FieldGroup>
|
||||
<Field orientation="responsive">
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor="resolution">Resolution</FieldLabel>
|
||||
<FieldDescription>Select the display resolution.</FieldDescription>
|
||||
</FieldContent>
|
||||
<Select>
|
||||
<SelectTrigger id="resolution" className="ml-auto">
|
||||
<SelectValue placeholder="Select" />
|
||||
</SelectTrigger>
|
||||
<SelectContent align="end">
|
||||
<SelectItem value="1920x1080">1920 x 1080</SelectItem>
|
||||
<SelectItem value="2560x1440">2560 x 1440</SelectItem>
|
||||
<SelectItem value="3840x2160">3840 x 2160</SelectItem>
|
||||
<SelectItem value="auto">Auto</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
<FieldSeparator />
|
||||
<Field orientation="responsive">
|
||||
<FieldContent>
|
||||
<FieldTitle>Brightness</FieldTitle>
|
||||
<FieldDescription>
|
||||
Adjust the display brightness level.
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<div className="flex min-w-[150px] items-center gap-2">
|
||||
<SunDimIcon className="size-4 shrink-0" />
|
||||
<Slider
|
||||
id="brightness"
|
||||
defaultValue={[75]}
|
||||
max={100}
|
||||
step={1}
|
||||
aria-label="Brightness"
|
||||
/>
|
||||
<SunIcon className="size-4 shrink-0" />
|
||||
</div>
|
||||
</Field>
|
||||
<FieldSeparator />
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor="auto-brightness">
|
||||
Automatically Adjust Brightness
|
||||
</FieldLabel>
|
||||
<FieldDescription>
|
||||
Automatically adjust brightness based on ambient light.
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<Checkbox id="auto-brightness" defaultChecked />
|
||||
</Field>
|
||||
<FieldSeparator />
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor="true-tone">True Tone</FieldLabel>
|
||||
<FieldDescription>
|
||||
Automatically adjust colors to match ambient lighting.
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<Switch id="true-tone" />
|
||||
</Field>
|
||||
<FieldSeparator />
|
||||
<Field orientation="responsive">
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor="refresh-rate">Refresh Rate</FieldLabel>
|
||||
<FieldDescription>
|
||||
Select the display refresh rate.
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<Select>
|
||||
<SelectTrigger id="refresh-rate" className="ml-auto min-w-[200px]">
|
||||
<SelectValue placeholder="Select" />
|
||||
</SelectTrigger>
|
||||
<SelectContent align="end">
|
||||
<SelectItem value="60hz">60 Hz</SelectItem>
|
||||
<SelectItem value="120hz">120 Hz</SelectItem>
|
||||
<SelectItem value="144hz">144 Hz</SelectItem>
|
||||
<SelectItem value="240hz">240 Hz</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
<FieldSeparator />
|
||||
<Field orientation="responsive">
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor="tv-connection">
|
||||
When connected to TV
|
||||
</FieldLabel>
|
||||
<FieldDescription>
|
||||
Choose display behavior when connected to a TV.
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<Select>
|
||||
<SelectTrigger id="tv-connection" className="ml-auto">
|
||||
<SelectValue placeholder="Select" />
|
||||
</SelectTrigger>
|
||||
<SelectContent align="end">
|
||||
<SelectItem value="mirror">Mirror Display</SelectItem>
|
||||
<SelectItem value="extend">Extend Display</SelectItem>
|
||||
<SelectItem value="tv-only">TV Only</SelectItem>
|
||||
<SelectItem value="auto">Auto</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</FieldSet>
|
||||
)
|
||||
}
|
||||
464
apps/v4/app/(internal)/sink/(pages)/forms/notion-prompt-form.tsx
Normal file
464
apps/v4/app/(internal)/sink/(pages)/forms/notion-prompt-form.tsx
Normal file
@@ -0,0 +1,464 @@
|
||||
"use client"
|
||||
|
||||
import { useMemo, useState } from "react"
|
||||
import {
|
||||
IconApps,
|
||||
IconArrowUp,
|
||||
IconAt,
|
||||
IconBook,
|
||||
IconBrandAbstract,
|
||||
IconBrandOpenai,
|
||||
IconBrandZeit,
|
||||
IconCircleDashedPlus,
|
||||
IconPaperclip,
|
||||
IconPlus,
|
||||
IconWorld,
|
||||
IconX,
|
||||
} from "@tabler/icons-react"
|
||||
|
||||
import {
|
||||
Avatar,
|
||||
AvatarFallback,
|
||||
AvatarImage,
|
||||
} from "@/registry/new-york-v4/ui/avatar"
|
||||
import { Badge } from "@/registry/new-york-v4/ui/badge"
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@/registry/new-york-v4/ui/command"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/registry/new-york-v4/ui/dropdown-menu"
|
||||
import { Field, FieldLabel } from "@/registry/new-york-v4/ui/field"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupTextarea,
|
||||
} from "@/registry/new-york-v4/ui/input-group"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/registry/new-york-v4/ui/popover"
|
||||
import { Switch } from "@/registry/new-york-v4/ui/switch"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/registry/new-york-v4/ui/tooltip"
|
||||
|
||||
const SAMPLE_DATA = {
|
||||
mentionable: [
|
||||
{
|
||||
type: "page",
|
||||
title: "Meeting Notes",
|
||||
image: "📝",
|
||||
},
|
||||
{
|
||||
type: "page",
|
||||
title: "Project Dashboard",
|
||||
image: "📊",
|
||||
},
|
||||
{
|
||||
type: "page",
|
||||
title: "Ideas & Brainstorming",
|
||||
image: "💡",
|
||||
},
|
||||
{
|
||||
type: "page",
|
||||
title: "Calendar & Events",
|
||||
image: "📅",
|
||||
},
|
||||
{
|
||||
type: "page",
|
||||
title: "Documentation",
|
||||
image: "📚",
|
||||
},
|
||||
{
|
||||
type: "page",
|
||||
title: "Goals & Objectives",
|
||||
image: "🎯",
|
||||
},
|
||||
{
|
||||
type: "page",
|
||||
title: "Budget Planning",
|
||||
image: "💰",
|
||||
},
|
||||
{
|
||||
type: "page",
|
||||
title: "Team Directory",
|
||||
image: "👥",
|
||||
},
|
||||
{
|
||||
type: "page",
|
||||
title: "Technical Specs",
|
||||
image: "🔧",
|
||||
},
|
||||
{
|
||||
type: "page",
|
||||
title: "Analytics Report",
|
||||
image: "📈",
|
||||
},
|
||||
{
|
||||
type: "user",
|
||||
title: "shadcn",
|
||||
image: "https://github.com/shadcn.png",
|
||||
workspace: "Workspace",
|
||||
},
|
||||
{
|
||||
type: "user",
|
||||
title: "maxleiter",
|
||||
image: "https://github.com/maxleiter.png",
|
||||
workspace: "Cursor",
|
||||
},
|
||||
{
|
||||
type: "user",
|
||||
title: "evilrabbit",
|
||||
image: "https://github.com/evilrabbit.png",
|
||||
workspace: "Vercel",
|
||||
},
|
||||
],
|
||||
models: [
|
||||
{
|
||||
name: "Auto",
|
||||
icon: IconBrandZeit,
|
||||
},
|
||||
{
|
||||
name: "Claude Sonnet 4",
|
||||
icon: IconBrandAbstract,
|
||||
badge: "Beta",
|
||||
},
|
||||
{
|
||||
name: "GPT-5",
|
||||
icon: IconBrandOpenai,
|
||||
badge: "Beta",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
function MentionableIcon({
|
||||
item,
|
||||
}: {
|
||||
item: (typeof SAMPLE_DATA.mentionable)[0]
|
||||
}) {
|
||||
return item.type === "page" ? (
|
||||
<span className="flex size-4 items-center justify-center">
|
||||
{item.image}
|
||||
</span>
|
||||
) : (
|
||||
<Avatar className="size-4">
|
||||
<AvatarImage src={item.image} />
|
||||
<AvatarFallback>{item.title[0]}</AvatarFallback>
|
||||
</Avatar>
|
||||
)
|
||||
}
|
||||
|
||||
export function NotionPromptForm() {
|
||||
const [mentions, setMentions] = useState<string[]>([])
|
||||
const [mentionPopoverOpen, setMentionPopoverOpen] = useState(false)
|
||||
const [modelPopoverOpen, setModelPopoverOpen] = useState(false)
|
||||
const [selectedModel, setSelectedModel] = useState<
|
||||
(typeof SAMPLE_DATA.models)[0]
|
||||
>(SAMPLE_DATA.models[0])
|
||||
const [scopeMenuOpen, setScopeMenuOpen] = useState(false)
|
||||
|
||||
const grouped = useMemo(() => {
|
||||
return SAMPLE_DATA.mentionable.reduce(
|
||||
(acc, item) => {
|
||||
const isAvailable = !mentions.includes(item.title)
|
||||
|
||||
if (isAvailable) {
|
||||
if (!acc[item.type]) {
|
||||
acc[item.type] = []
|
||||
}
|
||||
acc[item.type].push(item)
|
||||
}
|
||||
return acc
|
||||
},
|
||||
{} as Record<string, typeof SAMPLE_DATA.mentionable>
|
||||
)
|
||||
}, [mentions])
|
||||
|
||||
const hasMentions = mentions.length > 0
|
||||
|
||||
return (
|
||||
<form className="[--radius:1.2rem]">
|
||||
<Field>
|
||||
<FieldLabel htmlFor="notion-prompt" className="sr-only">
|
||||
Prompt
|
||||
</FieldLabel>
|
||||
<InputGroup className="bg-background dark:bg-background shadow-none">
|
||||
<InputGroupTextarea
|
||||
id="notion-prompt"
|
||||
placeholder="Ask, search, or make anything..."
|
||||
/>
|
||||
<InputGroupAddon align="block-start">
|
||||
<Popover
|
||||
open={mentionPopoverOpen}
|
||||
onOpenChange={setMentionPopoverOpen}
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<PopoverTrigger asChild>
|
||||
<InputGroupButton
|
||||
variant="outline"
|
||||
size={!hasMentions ? "sm" : "icon-sm"}
|
||||
className="rounded-full transition-transform"
|
||||
>
|
||||
<IconAt /> {!hasMentions && "Add context"}
|
||||
</InputGroupButton>
|
||||
</PopoverTrigger>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Mention a person, page, or date</TooltipContent>
|
||||
</Tooltip>
|
||||
<PopoverContent className="p-0 [--radius:1.2rem]" align="start">
|
||||
<Command>
|
||||
<CommandInput placeholder="Search pages..." />
|
||||
<CommandList>
|
||||
<CommandEmpty>No pages found</CommandEmpty>
|
||||
{Object.entries(grouped).map(([type, items]) => (
|
||||
<CommandGroup
|
||||
key={type}
|
||||
heading={type === "page" ? "Pages" : "Users"}
|
||||
>
|
||||
{items.map((item) => (
|
||||
<CommandItem
|
||||
key={item.title}
|
||||
value={item.title}
|
||||
onSelect={(currentValue) => {
|
||||
setMentions((prev) => [...prev, currentValue])
|
||||
setMentionPopoverOpen(false)
|
||||
}}
|
||||
>
|
||||
<MentionableIcon item={item} />
|
||||
{item.title}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
))}
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<div className="no-scrollbar -m-1.5 flex gap-1 overflow-y-auto p-1.5">
|
||||
{mentions.map((mention) => {
|
||||
const item = SAMPLE_DATA.mentionable.find(
|
||||
(item) => item.title === mention
|
||||
)
|
||||
|
||||
if (!item) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<InputGroupButton
|
||||
key={mention}
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
className="rounded-full !pl-2"
|
||||
onClick={() => {
|
||||
setMentions((prev) => prev.filter((m) => m !== mention))
|
||||
}}
|
||||
>
|
||||
<MentionableIcon item={item} />
|
||||
{item.title}
|
||||
<IconX />
|
||||
</InputGroupButton>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</InputGroupAddon>
|
||||
<InputGroupAddon align="block-end" className="gap-1">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<InputGroupButton
|
||||
size="icon-sm"
|
||||
className="rounded-full"
|
||||
aria-label="Attach file"
|
||||
>
|
||||
<IconPaperclip />
|
||||
</InputGroupButton>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Attach file</TooltipContent>
|
||||
</Tooltip>
|
||||
<DropdownMenu
|
||||
open={modelPopoverOpen}
|
||||
onOpenChange={setModelPopoverOpen}
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<InputGroupButton size="sm" className="rounded-full">
|
||||
{selectedModel.icon && selectedModel.name !== "Auto" && (
|
||||
<selectedModel.icon />
|
||||
)}
|
||||
{selectedModel.name}
|
||||
</InputGroupButton>
|
||||
</DropdownMenuTrigger>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Select AI model</TooltipContent>
|
||||
</Tooltip>
|
||||
<DropdownMenuContent
|
||||
side="top"
|
||||
align="start"
|
||||
className="[--radius:1.2rem]"
|
||||
>
|
||||
<DropdownMenuGroup className="w-72">
|
||||
<DropdownMenuLabel className="text-muted-foreground text-xs">
|
||||
Get answers about your workspace
|
||||
</DropdownMenuLabel>
|
||||
{SAMPLE_DATA.models.map((model) => (
|
||||
<DropdownMenuCheckboxItem
|
||||
key={model.name}
|
||||
checked={model.name === selectedModel.name}
|
||||
onCheckedChange={(checked) => {
|
||||
if (checked) {
|
||||
setSelectedModel(model)
|
||||
}
|
||||
}}
|
||||
className="pl-2 *:[span:first-child]:right-2 *:[span:first-child]:left-auto"
|
||||
>
|
||||
{model.icon && <model.icon />}
|
||||
{model.name}
|
||||
{model.badge && (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="h-5 rounded-sm bg-blue-100 px-1 text-xs text-blue-800 dark:bg-blue-900 dark:text-blue-100"
|
||||
>
|
||||
{model.badge}
|
||||
</Badge>
|
||||
)}
|
||||
</DropdownMenuCheckboxItem>
|
||||
))}
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<DropdownMenu open={scopeMenuOpen} onOpenChange={setScopeMenuOpen}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<InputGroupButton size="sm" className="rounded-full">
|
||||
<IconWorld /> All Sources
|
||||
</InputGroupButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
side="top"
|
||||
align="start"
|
||||
className="[--radius:1.2rem]"
|
||||
>
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem
|
||||
asChild
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
<label htmlFor="web-search">
|
||||
<IconWorld /> Web Search{" "}
|
||||
<Switch
|
||||
id="web-search"
|
||||
className="ml-auto"
|
||||
defaultChecked
|
||||
/>
|
||||
</label>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem
|
||||
asChild
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
<label htmlFor="apps">
|
||||
<IconApps /> Apps and Integrations
|
||||
<Switch id="apps" className="ml-auto" defaultChecked />
|
||||
</label>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<IconCircleDashedPlus /> All Sources I can access
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger>
|
||||
<Avatar className="size-4">
|
||||
<AvatarImage src="https://github.com/shadcn.png" />
|
||||
<AvatarFallback>CN</AvatarFallback>
|
||||
</Avatar>
|
||||
shadcn
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuSubContent className="w-72 p-0 [--radius:1.2rem]">
|
||||
<Command>
|
||||
<CommandInput
|
||||
placeholder="Find or use knowledge in..."
|
||||
autoFocus
|
||||
/>
|
||||
<CommandList>
|
||||
<CommandEmpty>No knowledge found</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{SAMPLE_DATA.mentionable
|
||||
.filter((item) => item.type === "user")
|
||||
.map((user) => (
|
||||
<CommandItem
|
||||
key={user.title}
|
||||
value={user.title}
|
||||
onSelect={() => {
|
||||
// Handle user selection here
|
||||
console.log("Selected user:", user.title)
|
||||
}}
|
||||
>
|
||||
<Avatar className="size-4">
|
||||
<AvatarImage src={user.image} />
|
||||
<AvatarFallback>
|
||||
{user.title[0]}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
{user.title}{" "}
|
||||
<span className="text-muted-foreground">
|
||||
- {user.workspace}
|
||||
</span>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuSub>
|
||||
<DropdownMenuItem>
|
||||
<IconBook /> Help Center
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>
|
||||
<IconPlus /> Connect Apps
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuLabel className="text-muted-foreground text-xs">
|
||||
We'll only search in the sources selected here.
|
||||
</DropdownMenuLabel>
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<InputGroupButton
|
||||
aria-label="Send"
|
||||
className="ml-auto rounded-full"
|
||||
variant="default"
|
||||
size="icon-sm"
|
||||
>
|
||||
<IconArrowUp />
|
||||
</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</Field>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
31
apps/v4/app/(internal)/sink/(pages)/forms/page.tsx
Normal file
31
apps/v4/app/(internal)/sink/(pages)/forms/page.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import { AppearanceSettings } from "@/app/(internal)/sink/(pages)/forms/appearance-settings"
|
||||
import { ChatSettings } from "@/app/(internal)/sink/(pages)/forms/chat-settings"
|
||||
import { DisplaySettings } from "@/app/(internal)/sink/(pages)/forms/display-settings"
|
||||
import { NotionPromptForm } from "@/app/(internal)/sink/(pages)/forms/notion-prompt-form"
|
||||
import { ShipRegistrationForm } from "@/app/(internal)/sink/(pages)/forms/ship-registration-form"
|
||||
import { ShippingForm } from "@/app/(internal)/sink/(pages)/forms/shipping-form"
|
||||
|
||||
export default function FormsPage() {
|
||||
return (
|
||||
<div className="@container flex flex-1 flex-col gap-12 p-4">
|
||||
<div className="grid flex-1 gap-12 @3xl:grid-cols-2 @5xl:grid-cols-3 @[120rem]:grid-cols-4 [&>div]:max-w-lg">
|
||||
<div className="flex flex-col gap-12">
|
||||
<NotionPromptForm />
|
||||
<ChatSettings />
|
||||
</div>
|
||||
<div className="flex flex-col gap-12">
|
||||
<AppearanceSettings />
|
||||
</div>
|
||||
<div className="flex flex-col gap-12">
|
||||
<DisplaySettings />
|
||||
</div>
|
||||
<div className="flex flex-col gap-12">
|
||||
<ShippingForm />
|
||||
</div>
|
||||
<div className="col-span-2 flex flex-col gap-12">
|
||||
<ShipRegistrationForm />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import { Checkbox } from "@/registry/new-york-v4/ui/checkbox"
|
||||
import {
|
||||
Field,
|
||||
FieldContent,
|
||||
FieldDescription,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldLegend,
|
||||
FieldSet,
|
||||
FieldTitle,
|
||||
} from "@/registry/new-york-v4/ui/field"
|
||||
import { Input } from "@/registry/new-york-v4/ui/input"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupInput,
|
||||
} from "@/registry/new-york-v4/ui/input-group"
|
||||
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"
|
||||
|
||||
export function ShipRegistrationForm() {
|
||||
return (
|
||||
<div className="flex max-w-md flex-col gap-6">
|
||||
<div className="flex flex-col gap-2">
|
||||
<h1 className="text-2xl font-bold tracking-tight">
|
||||
Join us in SF or online on October 23
|
||||
</h1>
|
||||
<FieldDescription>
|
||||
Already signed up? <a href="#">Log in</a>
|
||||
</FieldDescription>
|
||||
</div>
|
||||
<form>
|
||||
<FieldGroup>
|
||||
<FieldSet>
|
||||
<FieldLegend>1. Select your ticket type</FieldLegend>
|
||||
<FieldDescription>
|
||||
Select your ticket type to join us in San Francisco or online on
|
||||
October 23.
|
||||
</FieldDescription>
|
||||
<Field>
|
||||
<RadioGroup>
|
||||
<FieldLabel htmlFor="in-person">
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldTitle>In Person</FieldTitle>
|
||||
<FieldDescription>
|
||||
Join us in San Francisco on October 23.
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<RadioGroupItem value="in-person" id="in-person" />
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
<FieldLabel htmlFor="online">
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldTitle>Online</FieldTitle>
|
||||
<FieldDescription>
|
||||
Join us online on October 23.
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<RadioGroupItem value="online" id="online" />
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
</RadioGroup>
|
||||
</Field>
|
||||
</FieldSet>
|
||||
<Field orientation="horizontal">
|
||||
<Checkbox id="next-conf" />
|
||||
<FieldLabel htmlFor="next-conf">
|
||||
Also sign up for Next.js Conf 2025
|
||||
</FieldLabel>
|
||||
</Field>
|
||||
<FieldSet>
|
||||
<FieldLegend>2. Complete your attendee information</FieldLegend>
|
||||
<FieldDescription>
|
||||
By entering your information, you acknowledge that you have read
|
||||
and agree to the <a href="#">Terms of Service</a> and{" "}
|
||||
<a href="#">Privacy Policy</a>.
|
||||
</FieldDescription>
|
||||
<FieldGroup className="grid grid-cols-2 gap-x-4">
|
||||
<Field>
|
||||
<FieldLabel htmlFor="first-name">First Name</FieldLabel>
|
||||
<Input id="first-name" placeholder="Jane" required />
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="last-name">Last Name</FieldLabel>
|
||||
<Input id="last-name" placeholder="Doe" required />
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="email">Email</FieldLabel>
|
||||
<Input id="email" placeholder="jane.doe@example.com" required />
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="company">Company</FieldLabel>
|
||||
<Input id="company" placeholder="Example Inc." required />
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="job-title">Job Title</FieldLabel>
|
||||
<Input
|
||||
id="job-title"
|
||||
placeholder="Software Engineer"
|
||||
required
|
||||
/>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="country">Country</FieldLabel>
|
||||
<Select>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a country" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="us">United States</SelectItem>
|
||||
<SelectItem value="uk">United Kingdom</SelectItem>
|
||||
<SelectItem value="ca">Canada</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
<Field className="col-span-2">
|
||||
<FieldLabel htmlFor="topics">
|
||||
What AI-related topics are you most curious about?
|
||||
</FieldLabel>
|
||||
<Textarea
|
||||
id="topics"
|
||||
placeholder="Agents, Security, Improving UX/Personalization, etc."
|
||||
className="min-h-[100px]"
|
||||
/>
|
||||
</Field>
|
||||
<Field className="col-span-2">
|
||||
<FieldLabel htmlFor="workloads">
|
||||
What types of AI workloads are you tackling right now?
|
||||
</FieldLabel>
|
||||
<Textarea id="workloads" className="min-h-[100px]" />
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</FieldSet>
|
||||
<FieldSet>
|
||||
<FieldLegend>3. Buy your ticket</FieldLegend>
|
||||
<FieldDescription>
|
||||
Enter your card details to purchase your ticket.
|
||||
</FieldDescription>
|
||||
<FieldGroup className="grid grid-cols-2 gap-x-4">
|
||||
<Field className="col-span-2">
|
||||
<FieldLabel htmlFor="card-number">Card Number</FieldLabel>
|
||||
<Input
|
||||
id="card-number"
|
||||
placeholder="1234 5678 9012 3456"
|
||||
required
|
||||
/>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="expiry-date">Expiry Date</FieldLabel>
|
||||
<Input id="expiry-date" placeholder="MM/YY" required />
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="cvv">CVV</FieldLabel>
|
||||
<Input id="cvv" placeholder="123" required />
|
||||
</Field>
|
||||
<Field className="col-span-2">
|
||||
<FieldLabel htmlFor="promo-code">Promo Code</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupInput id="promo-code" placeholder="PROMO10" />
|
||||
<InputGroupAddon align="inline-end">
|
||||
<InputGroupButton>Apply</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</FieldSet>
|
||||
<Field>
|
||||
<Button type="submit">Purchase Ticket</Button>
|
||||
<FieldDescription>
|
||||
By clicking Purchase Ticket, you agree to the{" "}
|
||||
<a href="#">Terms of Service</a> and{" "}
|
||||
<a href="#">Privacy Policy</a>.
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
121
apps/v4/app/(internal)/sink/(pages)/forms/shipping-form.tsx
Normal file
121
apps/v4/app/(internal)/sink/(pages)/forms/shipping-form.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
import { Badge } from "@/registry/new-york-v4/ui/badge"
|
||||
import { Checkbox } from "@/registry/new-york-v4/ui/checkbox"
|
||||
import {
|
||||
Field,
|
||||
FieldContent,
|
||||
FieldDescription,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldLegend,
|
||||
FieldSet,
|
||||
FieldTitle,
|
||||
} from "@/registry/new-york-v4/ui/field"
|
||||
import { Input } from "@/registry/new-york-v4/ui/input"
|
||||
import {
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
} from "@/registry/new-york-v4/ui/radio-group"
|
||||
import { Textarea } from "@/registry/new-york-v4/ui/textarea"
|
||||
|
||||
export function ShippingForm() {
|
||||
return (
|
||||
<FieldSet>
|
||||
<FieldLegend>Shipping Details</FieldLegend>
|
||||
<FieldDescription>
|
||||
Please provide your shipping details so we can deliver your order.
|
||||
</FieldDescription>
|
||||
<FieldGroup>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="street-address">Street Address</FieldLabel>
|
||||
<Input id="street-address" autoComplete="off" />
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="city">City</FieldLabel>
|
||||
<Input id="city" />
|
||||
</Field>
|
||||
<FieldSet>
|
||||
<FieldLegend variant="label">Shipping Method</FieldLegend>
|
||||
<FieldDescription>
|
||||
Please select the shipping method for your order.
|
||||
</FieldDescription>
|
||||
<RadioGroup>
|
||||
<Field orientation="horizontal">
|
||||
<RadioGroupItem value="standard" id="shipping-method-1" />
|
||||
<FieldLabel htmlFor="shipping-method-1" className="font-normal">
|
||||
Standard{" "}
|
||||
<Badge className="rounded-full py-px" variant="outline">
|
||||
Free
|
||||
</Badge>
|
||||
</FieldLabel>
|
||||
</Field>
|
||||
<Field orientation="horizontal">
|
||||
<RadioGroupItem value="express" id="shipping-method-2" />
|
||||
<FieldLabel htmlFor="shipping-method-2" className="font-normal">
|
||||
Express
|
||||
</FieldLabel>
|
||||
</Field>
|
||||
</RadioGroup>
|
||||
</FieldSet>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="message">Message</FieldLabel>
|
||||
<Textarea id="message" />
|
||||
<FieldDescription>Anything else you want to add?</FieldDescription>
|
||||
</Field>
|
||||
<FieldSet>
|
||||
<FieldLegend>Additional Items</FieldLegend>
|
||||
<FieldDescription>
|
||||
Please select the additional items for your order.
|
||||
</FieldDescription>
|
||||
<FieldGroup data-slot="checkbox-group">
|
||||
<FieldLabel htmlFor="gift-wrapping">
|
||||
<Field orientation="horizontal">
|
||||
<Checkbox
|
||||
value="gift-wrapping"
|
||||
id="gift-wrapping"
|
||||
aria-label="Gift Wrapping"
|
||||
/>
|
||||
<FieldContent>
|
||||
<FieldTitle>Gift Wrapping</FieldTitle>
|
||||
<FieldDescription>
|
||||
Add elegant gift wrapping with a personalized message.
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
<FieldLabel htmlFor="insurance">
|
||||
<Field orientation="horizontal">
|
||||
<Checkbox
|
||||
value="insurance"
|
||||
id="insurance"
|
||||
aria-label="Package Insurance"
|
||||
/>
|
||||
<FieldContent>
|
||||
<FieldTitle>Package Insurance</FieldTitle>
|
||||
<FieldDescription>
|
||||
Protect your shipment with comprehensive insurance coverage.
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
<FieldLabel htmlFor="signature-confirmation">
|
||||
<Field orientation="horizontal">
|
||||
<Checkbox
|
||||
value="signature-confirmation"
|
||||
id="signature-confirmation"
|
||||
aria-label="Signature Confirmation"
|
||||
/>
|
||||
<FieldContent>
|
||||
<FieldTitle>Signature Confirmation</FieldTitle>
|
||||
<FieldDescription>
|
||||
Require recipient signature upon delivery for added
|
||||
security.
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
</FieldGroup>
|
||||
</FieldSet>
|
||||
</FieldGroup>
|
||||
</FieldSet>
|
||||
)
|
||||
}
|
||||
55
apps/v4/app/(internal)/sink/(pages)/next-form/actions.ts
Normal file
55
apps/v4/app/(internal)/sink/(pages)/next-form/actions.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
"use server"
|
||||
|
||||
import { FormState } from "@/app/(internal)/sink/(pages)/next-form/example-form"
|
||||
import { exampleFormSchema } from "@/app/(internal)/sink/(pages)/schema"
|
||||
|
||||
export async function subscriptionAction(
|
||||
_prevState: FormState,
|
||||
formData: FormData
|
||||
): Promise<FormState> {
|
||||
// Simulate server processing
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
|
||||
const values = {
|
||||
name: formData.get("name") as string,
|
||||
email: formData.get("email") as string,
|
||||
plan: formData.get("plan") as "basic" | "pro",
|
||||
billingPeriod: formData.get("billingPeriod") as string,
|
||||
addons: formData.getAll("addons") as string[],
|
||||
teamSize: parseInt(formData.get("teamSize") as string) || 1,
|
||||
emailNotifications: formData.get("emailNotifications") === "on",
|
||||
startDate: formData.get("startDate")
|
||||
? new Date(formData.get("startDate") as string)
|
||||
: new Date(),
|
||||
theme: formData.get("theme") as string,
|
||||
password: formData.get("password") as string,
|
||||
comments: formData.get("comments") as string,
|
||||
}
|
||||
|
||||
const result = exampleFormSchema.safeParse(values)
|
||||
|
||||
if (!result.success) {
|
||||
return {
|
||||
values,
|
||||
success: false,
|
||||
errors: result.error.flatten().fieldErrors,
|
||||
}
|
||||
}
|
||||
|
||||
// Simulate some business logic validation
|
||||
if (result.data.email.includes("invalid")) {
|
||||
return {
|
||||
values,
|
||||
success: false,
|
||||
errors: {
|
||||
email: ["This email domain is not supported"],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
values,
|
||||
errors: null,
|
||||
success: true,
|
||||
}
|
||||
}
|
||||
391
apps/v4/app/(internal)/sink/(pages)/next-form/example-form.tsx
Normal file
391
apps/v4/app/(internal)/sink/(pages)/next-form/example-form.tsx
Normal file
@@ -0,0 +1,391 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import Form from "next/form"
|
||||
import { z } from "zod"
|
||||
|
||||
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 {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/registry/new-york-v4/ui/dialog"
|
||||
import {
|
||||
Field,
|
||||
FieldContent,
|
||||
FieldDescription,
|
||||
FieldError,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldLegend,
|
||||
FieldSeparator,
|
||||
FieldSet,
|
||||
FieldTitle,
|
||||
} from "@/registry/new-york-v4/ui/field"
|
||||
import { Input } from "@/registry/new-york-v4/ui/input"
|
||||
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 { Spinner } from "@/registry/new-york-v4/ui/spinner"
|
||||
import { Switch } from "@/registry/new-york-v4/ui/switch"
|
||||
import { Textarea } from "@/registry/new-york-v4/ui/textarea"
|
||||
import { addons, exampleFormSchema } from "@/app/(internal)/sink/(pages)/schema"
|
||||
|
||||
import { subscriptionAction } from "./actions"
|
||||
|
||||
export type FormState = {
|
||||
values: z.infer<typeof exampleFormSchema>
|
||||
errors: null | Partial<
|
||||
Record<keyof z.infer<typeof exampleFormSchema>, string[]>
|
||||
>
|
||||
success: boolean
|
||||
}
|
||||
|
||||
export function ExampleForm() {
|
||||
const formId = React.useId()
|
||||
const [formKey, setFormKey] = React.useState(formId)
|
||||
const [showResults, setShowResults] = React.useState(false)
|
||||
const [formState, formAction, pending] = React.useActionState<
|
||||
FormState,
|
||||
FormData
|
||||
>(subscriptionAction, {
|
||||
values: {
|
||||
name: "",
|
||||
email: "",
|
||||
plan: "basic",
|
||||
billingPeriod: "",
|
||||
addons: ["analytics"],
|
||||
teamSize: 1,
|
||||
emailNotifications: false,
|
||||
comments: "",
|
||||
startDate: new Date(),
|
||||
theme: "system",
|
||||
password: "",
|
||||
},
|
||||
errors: null,
|
||||
success: false,
|
||||
})
|
||||
|
||||
React.useEffect(() => {
|
||||
if (formState.success) {
|
||||
setShowResults(true)
|
||||
}
|
||||
}, [formState.success])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card className="w-full max-w-sm">
|
||||
<CardHeader className="border-b">
|
||||
<CardTitle>Subscription Form</CardTitle>
|
||||
<CardDescription>
|
||||
Create your subscription using server actions and useActionState.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Form action={formAction} id="subscription-form" key={formKey}>
|
||||
<FieldGroup>
|
||||
<Field data-invalid={!!formState.errors?.name?.length}>
|
||||
<FieldLabel htmlFor="name">Name</FieldLabel>
|
||||
<Input
|
||||
id="name"
|
||||
name="name"
|
||||
defaultValue={formState.values.name}
|
||||
disabled={pending}
|
||||
aria-invalid={!!formState.errors?.name?.length}
|
||||
autoComplete="off"
|
||||
/>
|
||||
<FieldDescription>Enter your name</FieldDescription>
|
||||
{formState.errors?.name && (
|
||||
<FieldError>{formState.errors.name[0]}</FieldError>
|
||||
)}
|
||||
</Field>
|
||||
<Field data-invalid={!!formState.errors?.email?.length}>
|
||||
<FieldLabel htmlFor="email">Email</FieldLabel>
|
||||
<Input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
defaultValue={formState.values.email}
|
||||
disabled={pending}
|
||||
aria-invalid={!!formState.errors?.email?.length}
|
||||
autoComplete="off"
|
||||
/>
|
||||
<FieldDescription>Enter your email address</FieldDescription>
|
||||
{formState.errors?.email && (
|
||||
<FieldError>{formState.errors.email[0]}</FieldError>
|
||||
)}
|
||||
</Field>
|
||||
<FieldSeparator />
|
||||
<FieldSet data-invalid={!!formState.errors?.plan?.length}>
|
||||
<FieldLegend>Subscription Plan</FieldLegend>
|
||||
<FieldDescription>
|
||||
Choose your subscription plan.
|
||||
</FieldDescription>
|
||||
<RadioGroup
|
||||
name="plan"
|
||||
defaultValue={formState.values.plan}
|
||||
disabled={pending}
|
||||
aria-invalid={!!formState.errors?.plan?.length}
|
||||
>
|
||||
<FieldLabel htmlFor="basic">
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldTitle>Basic</FieldTitle>
|
||||
<FieldDescription>
|
||||
For individuals and small teams
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<RadioGroupItem value="basic" id="basic" />
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
<FieldLabel htmlFor="pro">
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldTitle>Pro</FieldTitle>
|
||||
<FieldDescription>
|
||||
For businesses with higher demands
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<RadioGroupItem value="pro" id="pro" />
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
</RadioGroup>
|
||||
{formState.errors?.plan && (
|
||||
<FieldError>{formState.errors.plan[0]}</FieldError>
|
||||
)}
|
||||
</FieldSet>
|
||||
<FieldSeparator />
|
||||
<Field data-invalid={!!formState.errors?.billingPeriod?.length}>
|
||||
<FieldLabel htmlFor="billingPeriod">Billing Period</FieldLabel>
|
||||
<Select
|
||||
name="billingPeriod"
|
||||
defaultValue={formState.values.billingPeriod}
|
||||
disabled={pending}
|
||||
aria-invalid={!!formState.errors?.billingPeriod?.length}
|
||||
>
|
||||
<SelectTrigger id="billingPeriod">
|
||||
<SelectValue placeholder="Select billing period" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="monthly">Monthly</SelectItem>
|
||||
<SelectItem value="yearly">Yearly</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FieldDescription>
|
||||
Choose how often you want to be billed.
|
||||
</FieldDescription>
|
||||
{formState.errors?.billingPeriod && (
|
||||
<FieldError>{formState.errors.billingPeriod[0]}</FieldError>
|
||||
)}
|
||||
</Field>
|
||||
<FieldSeparator />
|
||||
<FieldSet data-invalid={!!formState.errors?.addons?.length}>
|
||||
<FieldLegend>Add-ons</FieldLegend>
|
||||
<FieldDescription>
|
||||
Select additional features you'd like to include.
|
||||
</FieldDescription>
|
||||
<FieldGroup data-slot="checkbox-group">
|
||||
{addons.map((addon) => (
|
||||
<Field key={addon.id} orientation="horizontal">
|
||||
<Checkbox
|
||||
id={addon.id}
|
||||
name="addons"
|
||||
value={addon.id}
|
||||
defaultChecked={formState.values.addons.includes(
|
||||
addon.id
|
||||
)}
|
||||
disabled={pending}
|
||||
aria-invalid={!!formState.errors?.addons?.length}
|
||||
/>
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor={addon.id}>
|
||||
{addon.title}
|
||||
</FieldLabel>
|
||||
<FieldDescription>{addon.description}</FieldDescription>
|
||||
</FieldContent>
|
||||
</Field>
|
||||
))}
|
||||
</FieldGroup>
|
||||
{formState.errors?.addons && (
|
||||
<FieldError>{formState.errors.addons[0]}</FieldError>
|
||||
)}
|
||||
</FieldSet>
|
||||
<FieldSeparator />
|
||||
<Field data-invalid={!!formState.errors?.teamSize?.length}>
|
||||
<FieldLabel htmlFor="teamSize">Team Size</FieldLabel>
|
||||
<Input
|
||||
id="teamSize"
|
||||
name="teamSize"
|
||||
type="number"
|
||||
min="1"
|
||||
max="50"
|
||||
defaultValue={formState.values.teamSize.toString()}
|
||||
disabled={pending}
|
||||
aria-invalid={!!formState.errors?.teamSize?.length}
|
||||
/>
|
||||
<FieldDescription>
|
||||
How many people will be using the subscription? (1-50)
|
||||
</FieldDescription>
|
||||
{formState.errors?.teamSize && (
|
||||
<FieldError>{formState.errors.teamSize[0]}</FieldError>
|
||||
)}
|
||||
</Field>
|
||||
<FieldSeparator />
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor="emailNotifications">
|
||||
Email Notifications
|
||||
</FieldLabel>
|
||||
<FieldDescription>
|
||||
Receive email updates about your subscription
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<Switch
|
||||
id="emailNotifications"
|
||||
name="emailNotifications"
|
||||
defaultChecked={formState.values.emailNotifications}
|
||||
disabled={pending}
|
||||
aria-invalid={!!formState.errors?.emailNotifications?.length}
|
||||
/>
|
||||
</Field>
|
||||
<FieldSeparator />
|
||||
<Field data-invalid={!!formState.errors?.startDate?.length}>
|
||||
<FieldLabel htmlFor="startDate">Start Date</FieldLabel>
|
||||
<Input
|
||||
id="startDate"
|
||||
name="startDate"
|
||||
type="date"
|
||||
defaultValue={
|
||||
formState.values.startDate.toISOString().split("T")[0]
|
||||
}
|
||||
disabled={pending}
|
||||
aria-invalid={!!formState.errors?.startDate?.length}
|
||||
/>
|
||||
<FieldDescription>
|
||||
Choose when your subscription should start
|
||||
</FieldDescription>
|
||||
{formState.errors?.startDate && (
|
||||
<FieldError>{formState.errors.startDate[0]}</FieldError>
|
||||
)}
|
||||
</Field>
|
||||
<FieldSeparator />
|
||||
<Field data-invalid={!!formState.errors?.theme?.length}>
|
||||
<FieldLabel htmlFor="theme">Theme Preference</FieldLabel>
|
||||
<Select
|
||||
name="theme"
|
||||
defaultValue={formState.values.theme}
|
||||
disabled={pending}
|
||||
aria-invalid={!!formState.errors?.theme?.length}
|
||||
>
|
||||
<SelectTrigger id="theme">
|
||||
<SelectValue placeholder="Select theme" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="light">Light</SelectItem>
|
||||
<SelectItem value="dark">Dark</SelectItem>
|
||||
<SelectItem value="system">System</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FieldDescription>
|
||||
Choose your preferred color theme
|
||||
</FieldDescription>
|
||||
{formState.errors?.theme && (
|
||||
<FieldError>{formState.errors.theme[0]}</FieldError>
|
||||
)}
|
||||
</Field>
|
||||
<FieldSeparator />
|
||||
<Field data-invalid={!!formState.errors?.password?.length}>
|
||||
<FieldLabel htmlFor="password">Password</FieldLabel>
|
||||
<Input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
defaultValue={formState.values.password}
|
||||
placeholder="Enter your password"
|
||||
disabled={pending}
|
||||
aria-invalid={!!formState.errors?.password?.length}
|
||||
/>
|
||||
<FieldDescription>
|
||||
Must contain uppercase, lowercase, number, and be 8+
|
||||
characters
|
||||
</FieldDescription>
|
||||
{formState.errors?.password && (
|
||||
<FieldError>{formState.errors.password[0]}</FieldError>
|
||||
)}
|
||||
</Field>
|
||||
<FieldSeparator />
|
||||
<Field data-invalid={!!formState.errors?.comments?.length}>
|
||||
<FieldLabel htmlFor="comments">Additional Comments</FieldLabel>
|
||||
<Textarea
|
||||
id="comments"
|
||||
name="comments"
|
||||
defaultValue={formState.values.comments}
|
||||
placeholder="Tell us more about your needs..."
|
||||
rows={3}
|
||||
disabled={pending}
|
||||
aria-invalid={!!formState.errors?.comments?.length}
|
||||
/>
|
||||
<FieldDescription>
|
||||
Share any additional requirements or feedback (10-240
|
||||
characters)
|
||||
</FieldDescription>
|
||||
{formState.errors?.comments && (
|
||||
<FieldError>{formState.errors.comments[0]}</FieldError>
|
||||
)}
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</Form>
|
||||
</CardContent>
|
||||
<CardFooter className="border-t">
|
||||
<Field orientation="horizontal" className="justify-end">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
disabled={pending}
|
||||
form="subscription-form"
|
||||
onClick={() => setFormKey(formKey + 1)}
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
<Button type="submit" disabled={pending} form="subscription-form">
|
||||
{pending && <Spinner />}
|
||||
Create Subscription
|
||||
</Button>
|
||||
</Field>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
<Dialog open={showResults} onOpenChange={setShowResults}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Subscription Created!</DialogTitle>
|
||||
<DialogDescription>
|
||||
Here are the details of your subscription.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<pre className="overflow-x-auto rounded-md bg-black p-4 font-mono text-sm text-white">
|
||||
<code>{JSON.stringify(formState.values, null, 2)}</code>
|
||||
</pre>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
)
|
||||
}
|
||||
9
apps/v4/app/(internal)/sink/(pages)/next-form/page.tsx
Normal file
9
apps/v4/app/(internal)/sink/(pages)/next-form/page.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import { ExampleForm } from "@/app/(internal)/sink/(pages)/next-form/example-form"
|
||||
|
||||
export default function NextFormPage() {
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center p-4">
|
||||
<ExampleForm />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,490 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { format } from "date-fns"
|
||||
import { Controller, useForm } from "react-hook-form"
|
||||
import z from "zod"
|
||||
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import { Calendar } from "@/registry/new-york-v4/ui/calendar"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/new-york-v4/ui/card"
|
||||
import { Checkbox } from "@/registry/new-york-v4/ui/checkbox"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/registry/new-york-v4/ui/dialog"
|
||||
import {
|
||||
Field,
|
||||
FieldContent,
|
||||
FieldDescription,
|
||||
FieldError,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldLegend,
|
||||
FieldSeparator,
|
||||
FieldSet,
|
||||
FieldTitle,
|
||||
} from "@/registry/new-york-v4/ui/field"
|
||||
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 { Slider } from "@/registry/new-york-v4/ui/slider"
|
||||
import { Switch } from "@/registry/new-york-v4/ui/switch"
|
||||
import { Textarea } from "@/registry/new-york-v4/ui/textarea"
|
||||
import {
|
||||
ToggleGroup,
|
||||
ToggleGroupItem,
|
||||
} from "@/registry/new-york-v4/ui/toggle-group"
|
||||
import { addons, exampleFormSchema } from "@/app/(internal)/sink/(pages)/schema"
|
||||
|
||||
export function ExampleForm() {
|
||||
const [values, setValues] = useState<z.infer<typeof exampleFormSchema>>()
|
||||
const [open, setOpen] = useState(false)
|
||||
const form = useForm<z.infer<typeof exampleFormSchema>>({
|
||||
resolver: zodResolver(exampleFormSchema),
|
||||
mode: "onChange",
|
||||
defaultValues: {
|
||||
name: "",
|
||||
email: "",
|
||||
plan: "basic" as const,
|
||||
billingPeriod: "",
|
||||
addons: ["analytics"],
|
||||
emailNotifications: false,
|
||||
teamSize: 1,
|
||||
comments: "",
|
||||
startDate: new Date(),
|
||||
theme: "system",
|
||||
password: "",
|
||||
},
|
||||
})
|
||||
|
||||
function onSubmit(data: z.infer<typeof exampleFormSchema>) {
|
||||
setValues(data)
|
||||
setOpen(true)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card className="w-full max-w-sm">
|
||||
<CardHeader className="border-b">
|
||||
<CardTitle>React Hook Form</CardTitle>
|
||||
<CardDescription>
|
||||
This form uses React Hook Form with Zod validation.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form id="subscription-form" onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<FieldGroup>
|
||||
<Controller
|
||||
name="name"
|
||||
control={form.control}
|
||||
render={({ field, fieldState }) => {
|
||||
const isInvalid = fieldState.invalid
|
||||
return (
|
||||
<Field data-invalid={isInvalid}>
|
||||
<FieldLabel htmlFor={field.name}>Name</FieldLabel>
|
||||
<Input
|
||||
{...field}
|
||||
id={field.name}
|
||||
aria-invalid={isInvalid}
|
||||
autoComplete="off"
|
||||
/>
|
||||
<FieldDescription>Enter your name</FieldDescription>
|
||||
{isInvalid && <FieldError errors={[fieldState.error]} />}
|
||||
</Field>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Controller
|
||||
name="email"
|
||||
control={form.control}
|
||||
render={({ field, fieldState }) => {
|
||||
const isInvalid = fieldState.invalid
|
||||
return (
|
||||
<Field data-invalid={isInvalid}>
|
||||
<FieldLabel htmlFor={field.name}>Email</FieldLabel>
|
||||
<Input
|
||||
{...field}
|
||||
type="email"
|
||||
id={field.name}
|
||||
aria-invalid={isInvalid}
|
||||
autoComplete="off"
|
||||
/>
|
||||
<FieldDescription>
|
||||
Enter your email address
|
||||
</FieldDescription>
|
||||
{isInvalid && <FieldError errors={[fieldState.error]} />}
|
||||
</Field>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<FieldSeparator />
|
||||
<Controller
|
||||
name="plan"
|
||||
control={form.control}
|
||||
render={({ field, fieldState }) => {
|
||||
const isInvalid = fieldState.invalid
|
||||
return (
|
||||
<FieldSet data-invalid={isInvalid}>
|
||||
<FieldLegend>Subscription Plan</FieldLegend>
|
||||
<FieldDescription>
|
||||
Choose your subscription plan.
|
||||
</FieldDescription>
|
||||
<RadioGroup
|
||||
name={field.name}
|
||||
value={field.value}
|
||||
onValueChange={field.onChange}
|
||||
aria-invalid={isInvalid}
|
||||
>
|
||||
<FieldLabel htmlFor="basic">
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldTitle>Basic</FieldTitle>
|
||||
<FieldDescription>
|
||||
For individuals and small teams
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<RadioGroupItem
|
||||
value="basic"
|
||||
id="basic"
|
||||
aria-invalid={isInvalid}
|
||||
/>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
<FieldLabel htmlFor="pro">
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldTitle>Pro</FieldTitle>
|
||||
<FieldDescription>
|
||||
For businesses with higher demands
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<RadioGroupItem
|
||||
value="pro"
|
||||
id="pro"
|
||||
aria-invalid={isInvalid}
|
||||
/>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
</RadioGroup>
|
||||
{isInvalid && <FieldError errors={[fieldState.error]} />}
|
||||
</FieldSet>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<FieldSeparator />
|
||||
<Controller
|
||||
name="billingPeriod"
|
||||
control={form.control}
|
||||
render={({ field, fieldState }) => {
|
||||
const isInvalid = fieldState.invalid
|
||||
return (
|
||||
<Field data-invalid={isInvalid}>
|
||||
<FieldLabel htmlFor={field.name}>
|
||||
Billing Period
|
||||
</FieldLabel>
|
||||
<Select
|
||||
name={field.name}
|
||||
value={field.value}
|
||||
onValueChange={field.onChange}
|
||||
aria-invalid={isInvalid}
|
||||
>
|
||||
<SelectTrigger id={field.name}>
|
||||
<SelectValue placeholder="Select billing period" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="monthly">Monthly</SelectItem>
|
||||
<SelectItem value="yearly">Yearly</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FieldDescription>
|
||||
Choose how often you want to be billed.
|
||||
</FieldDescription>
|
||||
{isInvalid && <FieldError errors={[fieldState.error]} />}
|
||||
</Field>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<FieldSeparator />
|
||||
<Controller
|
||||
name="addons"
|
||||
control={form.control}
|
||||
render={({ field, fieldState }) => {
|
||||
const isInvalid = fieldState.invalid
|
||||
return (
|
||||
<FieldSet data-invalid={isInvalid}>
|
||||
<FieldLegend>Add-ons</FieldLegend>
|
||||
<FieldDescription>
|
||||
Select additional features you'd like to include.
|
||||
</FieldDescription>
|
||||
<FieldGroup data-slot="checkbox-group">
|
||||
{addons.map((addon) => (
|
||||
<Field key={addon.id} orientation="horizontal">
|
||||
<Checkbox
|
||||
id={addon.id}
|
||||
name={field.name}
|
||||
aria-invalid={isInvalid}
|
||||
checked={field.value.includes(addon.id)}
|
||||
onCheckedChange={(checked) => {
|
||||
const newValue = checked
|
||||
? [...field.value, addon.id]
|
||||
: field.value.filter(
|
||||
(value) => value !== addon.id
|
||||
)
|
||||
field.onChange(newValue)
|
||||
field.onBlur()
|
||||
}}
|
||||
/>
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor={addon.id}>
|
||||
{addon.title}
|
||||
</FieldLabel>
|
||||
<FieldDescription>
|
||||
{addon.description}
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
</Field>
|
||||
))}
|
||||
</FieldGroup>
|
||||
{isInvalid && <FieldError errors={[fieldState.error]} />}
|
||||
</FieldSet>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<FieldSeparator />
|
||||
<Controller
|
||||
name="teamSize"
|
||||
control={form.control}
|
||||
render={({ field, fieldState }) => {
|
||||
const isInvalid = fieldState.invalid
|
||||
return (
|
||||
<Field data-invalid={isInvalid}>
|
||||
<FieldTitle>Team Size</FieldTitle>
|
||||
<FieldDescription>
|
||||
How many people will be using the subscription?
|
||||
</FieldDescription>
|
||||
<Slider
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
value={[field.value]}
|
||||
onValueChange={field.onChange}
|
||||
min={1}
|
||||
max={50}
|
||||
step={1}
|
||||
aria-invalid={isInvalid}
|
||||
/>
|
||||
{isInvalid && <FieldError errors={[fieldState.error]} />}
|
||||
</Field>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<FieldSeparator />
|
||||
<Controller
|
||||
name="emailNotifications"
|
||||
control={form.control}
|
||||
render={({ field, fieldState }) => {
|
||||
const isInvalid = fieldState.invalid
|
||||
return (
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor={field.name}>
|
||||
Email Notifications
|
||||
</FieldLabel>
|
||||
<FieldDescription>
|
||||
Receive email updates about your subscription
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<Switch
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
aria-invalid={isInvalid}
|
||||
/>
|
||||
{isInvalid && <FieldError errors={[fieldState.error]} />}
|
||||
</Field>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<FieldSeparator />
|
||||
<Controller
|
||||
name="startDate"
|
||||
control={form.control}
|
||||
render={({ field, fieldState }) => {
|
||||
const isInvalid = fieldState.invalid
|
||||
return (
|
||||
<Field data-invalid={isInvalid}>
|
||||
<FieldLabel htmlFor={field.name}>Start Date</FieldLabel>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
id={field.name}
|
||||
variant="outline"
|
||||
className="justify-start"
|
||||
aria-invalid={isInvalid}
|
||||
>
|
||||
{field.value ? (
|
||||
format(field.value, "PPP")
|
||||
) : (
|
||||
<span>Pick a date</span>
|
||||
)}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0" align="start">
|
||||
<Calendar
|
||||
required
|
||||
mode="single"
|
||||
selected={field.value}
|
||||
onSelect={field.onChange}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<FieldDescription>
|
||||
Choose when your subscription should start
|
||||
</FieldDescription>
|
||||
{isInvalid && <FieldError errors={[fieldState.error]} />}
|
||||
</Field>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<FieldSeparator />
|
||||
<Controller
|
||||
name="theme"
|
||||
control={form.control}
|
||||
render={({ field, fieldState }) => {
|
||||
const isInvalid = fieldState.invalid
|
||||
return (
|
||||
<Field data-invalid={isInvalid}>
|
||||
<FieldTitle>Theme Preference</FieldTitle>
|
||||
<ToggleGroup
|
||||
type="single"
|
||||
variant="outline"
|
||||
value={field.value}
|
||||
onValueChange={(value) =>
|
||||
value && field.onChange(value)
|
||||
}
|
||||
aria-invalid={isInvalid}
|
||||
>
|
||||
<ToggleGroupItem value="light">Light</ToggleGroupItem>
|
||||
<ToggleGroupItem value="dark">Dark</ToggleGroupItem>
|
||||
<ToggleGroupItem value="system">System</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
<FieldDescription>
|
||||
Choose your preferred color theme
|
||||
</FieldDescription>
|
||||
{isInvalid && <FieldError errors={[fieldState.error]} />}
|
||||
</Field>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<FieldSeparator />
|
||||
<Controller
|
||||
name="password"
|
||||
control={form.control}
|
||||
render={({ field, fieldState }) => {
|
||||
const isInvalid = fieldState.invalid
|
||||
return (
|
||||
<Field data-invalid={isInvalid}>
|
||||
<FieldLabel htmlFor={field.name}>Password</FieldLabel>
|
||||
<Input
|
||||
{...field}
|
||||
type="password"
|
||||
placeholder="Enter your password"
|
||||
id={field.name}
|
||||
aria-invalid={isInvalid}
|
||||
/>
|
||||
<FieldDescription>
|
||||
Must contain uppercase, lowercase, number, and be 8+
|
||||
characters
|
||||
</FieldDescription>
|
||||
{isInvalid && <FieldError errors={[fieldState.error]} />}
|
||||
</Field>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<FieldSeparator />
|
||||
<Controller
|
||||
name="comments"
|
||||
control={form.control}
|
||||
render={({ field, fieldState }) => {
|
||||
const isInvalid = fieldState.invalid
|
||||
return (
|
||||
<Field data-invalid={isInvalid}>
|
||||
<FieldLabel htmlFor={field.name}>
|
||||
Additional Comments
|
||||
</FieldLabel>
|
||||
<Textarea
|
||||
{...field}
|
||||
id={field.name}
|
||||
placeholder="Tell us more about your needs..."
|
||||
rows={3}
|
||||
aria-invalid={isInvalid}
|
||||
/>
|
||||
<FieldDescription>
|
||||
Share any additional requirements or feedback (10-240
|
||||
characters)
|
||||
</FieldDescription>
|
||||
{isInvalid && <FieldError errors={[fieldState.error]} />}
|
||||
</Field>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</FieldGroup>
|
||||
</form>
|
||||
</CardContent>
|
||||
<CardFooter className="border-t">
|
||||
<Field orientation="horizontal" className="justify-end">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => form.reset()}
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
<Button type="submit" form="subscription-form">
|
||||
Submit
|
||||
</Button>
|
||||
</Field>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Submitted Values</DialogTitle>
|
||||
<DialogDescription>
|
||||
Here are the values you submitted.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<pre className="overflow-x-auto rounded-md bg-black p-4 font-mono text-sm text-white">
|
||||
<code>{JSON.stringify(values, null, 2)}</code>
|
||||
</pre>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { ExampleForm } from "@/app/(internal)/sink/(pages)/react-hook-form/example-form"
|
||||
|
||||
export default function ReactHookFormPage() {
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center p-4">
|
||||
<ExampleForm />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
97
apps/v4/app/(internal)/sink/(pages)/schema.ts
Normal file
97
apps/v4/app/(internal)/sink/(pages)/schema.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import { z } from "zod"
|
||||
|
||||
export const addons = [
|
||||
{
|
||||
id: "analytics",
|
||||
title: "Analytics",
|
||||
description: "Advanced analytics and reporting",
|
||||
},
|
||||
{
|
||||
id: "backup",
|
||||
title: "Backup",
|
||||
description: "Automated daily backups",
|
||||
},
|
||||
{
|
||||
id: "support",
|
||||
title: "Priority Support",
|
||||
description: "24/7 premium customer support",
|
||||
},
|
||||
] as const
|
||||
|
||||
export const exampleFormSchema = z.object({
|
||||
name: z
|
||||
.string({
|
||||
required_error: "Name is required",
|
||||
invalid_type_error: "Name must be a string",
|
||||
})
|
||||
.min(2, "Name must be at least 2 characters")
|
||||
.max(50, "Name must be less than 50 characters")
|
||||
.refine((value) => !/\d/.test(value), {
|
||||
message: "Name must not contain numbers",
|
||||
}),
|
||||
|
||||
email: z
|
||||
.string({
|
||||
required_error: "Email is required",
|
||||
})
|
||||
.email("Please enter a valid email address"),
|
||||
|
||||
plan: z
|
||||
.string({
|
||||
required_error: "Please select a subscription plan",
|
||||
})
|
||||
.min(1, "Please select a subscription plan")
|
||||
.refine((value) => value === "basic" || value === "pro", {
|
||||
message: "Invalid plan selection. Please choose Basic or Pro",
|
||||
}),
|
||||
|
||||
billingPeriod: z
|
||||
.string({
|
||||
required_error: "Please select a billing period",
|
||||
})
|
||||
.min(1, "Please select a billing period"),
|
||||
|
||||
addons: z
|
||||
.array(z.string())
|
||||
.min(1, "Please select at least one add-on")
|
||||
.max(3, "You can select up to 3 add-ons"),
|
||||
|
||||
teamSize: z.number().min(1).max(10),
|
||||
emailNotifications: z.boolean({
|
||||
required_error: "Please choose email notification preference",
|
||||
}),
|
||||
comments: z
|
||||
.string()
|
||||
.min(10, "Comments must be at least 10 characters")
|
||||
.max(240, "Comments must not exceed 240 characters"),
|
||||
startDate: z
|
||||
.date({
|
||||
required_error: "Please select a start date",
|
||||
invalid_type_error: "Invalid date format",
|
||||
})
|
||||
.min(new Date(), "Start date cannot be in the past")
|
||||
.refine(
|
||||
(date) => {
|
||||
const now = new Date()
|
||||
const oneWeekFromNow = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000)
|
||||
return date <= oneWeekFromNow
|
||||
},
|
||||
{
|
||||
message: "Start date must be within the current week",
|
||||
}
|
||||
),
|
||||
theme: z
|
||||
.string({
|
||||
required_error: "Please select a theme",
|
||||
})
|
||||
.min(1, "Please select a theme"),
|
||||
password: z
|
||||
.string({
|
||||
required_error: "Password is required",
|
||||
})
|
||||
.min(8, "Password must be at least 8 characters")
|
||||
.regex(
|
||||
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/,
|
||||
"Password must contain at least one uppercase letter, one lowercase letter, and one number"
|
||||
),
|
||||
})
|
||||
@@ -0,0 +1,532 @@
|
||||
/* eslint-disable react/no-children-prop */
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { useForm } from "@tanstack/react-form"
|
||||
import { format } from "date-fns"
|
||||
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import { Calendar } from "@/registry/new-york-v4/ui/calendar"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/new-york-v4/ui/card"
|
||||
import { Checkbox } from "@/registry/new-york-v4/ui/checkbox"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/registry/new-york-v4/ui/dialog"
|
||||
import {
|
||||
Field,
|
||||
FieldContent,
|
||||
FieldDescription,
|
||||
FieldError,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldLegend,
|
||||
FieldSeparator,
|
||||
FieldSet,
|
||||
FieldTitle,
|
||||
} from "@/registry/new-york-v4/ui/field"
|
||||
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 { Slider } from "@/registry/new-york-v4/ui/slider"
|
||||
import { Switch } from "@/registry/new-york-v4/ui/switch"
|
||||
import { Textarea } from "@/registry/new-york-v4/ui/textarea"
|
||||
import {
|
||||
ToggleGroup,
|
||||
ToggleGroupItem,
|
||||
} from "@/registry/new-york-v4/ui/toggle-group"
|
||||
import { addons, exampleFormSchema } from "@/app/(internal)/sink/(pages)/schema"
|
||||
|
||||
export function ExampleForm() {
|
||||
const form = useForm({
|
||||
defaultValues: {
|
||||
name: "",
|
||||
email: "",
|
||||
plan: "",
|
||||
billingPeriod: "",
|
||||
addons: ["analytics"],
|
||||
emailNotifications: false,
|
||||
teamSize: 1,
|
||||
comments: "",
|
||||
startDate: new Date(),
|
||||
theme: "system",
|
||||
password: "",
|
||||
},
|
||||
validators: {
|
||||
onChange: exampleFormSchema,
|
||||
},
|
||||
onSubmit: async ({ value }) => {
|
||||
setValues(value)
|
||||
setOpen(true)
|
||||
},
|
||||
})
|
||||
const [values, setValues] = React.useState<typeof form.state.values>()
|
||||
const [open, setOpen] = React.useState(false)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card className="w-full max-w-sm">
|
||||
<CardHeader className="border-b">
|
||||
<CardTitle>Example Form</CardTitle>
|
||||
<CardDescription>
|
||||
This is an example form using TanStack Form.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form
|
||||
id="example-form"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
void form.handleSubmit()
|
||||
}}
|
||||
>
|
||||
<FieldGroup>
|
||||
<form.Field
|
||||
name="name"
|
||||
children={(field) => {
|
||||
const isInvalid =
|
||||
field.state.meta.isTouched && !field.state.meta.isValid
|
||||
return (
|
||||
<Field data-invalid={isInvalid}>
|
||||
<FieldLabel htmlFor={field.name}>Name</FieldLabel>
|
||||
<Input
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
value={field.state.value}
|
||||
onBlur={field.handleBlur}
|
||||
onChange={(e) => field.handleChange(e.target.value)}
|
||||
aria-invalid={isInvalid}
|
||||
autoComplete="off"
|
||||
/>
|
||||
<FieldDescription>Enter your name</FieldDescription>
|
||||
{isInvalid && (
|
||||
<FieldError errors={field.state.meta.errors} />
|
||||
)}
|
||||
</Field>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<form.Field
|
||||
name="email"
|
||||
children={(field) => {
|
||||
const isInvalid =
|
||||
field.state.meta.isTouched && !field.state.meta.isValid
|
||||
return (
|
||||
<Field data-invalid={isInvalid}>
|
||||
<FieldLabel htmlFor={field.name}>Email</FieldLabel>
|
||||
<Input
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
type="email"
|
||||
value={field.state.value}
|
||||
onBlur={field.handleBlur}
|
||||
onChange={(e) => field.handleChange(e.target.value)}
|
||||
aria-invalid={isInvalid}
|
||||
autoComplete="off"
|
||||
/>
|
||||
<FieldDescription>
|
||||
Enter your email address
|
||||
</FieldDescription>
|
||||
{isInvalid && (
|
||||
<FieldError errors={field.state.meta.errors} />
|
||||
)}
|
||||
</Field>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<FieldSeparator />
|
||||
<form.Field
|
||||
name="plan"
|
||||
children={(field) => {
|
||||
const isInvalid =
|
||||
field.state.meta.isTouched && !field.state.meta.isValid
|
||||
return (
|
||||
<FieldSet data-invalid={isInvalid}>
|
||||
<FieldLegend>Subscription Plan</FieldLegend>
|
||||
<FieldDescription>
|
||||
Choose your subscription plan.
|
||||
</FieldDescription>
|
||||
<RadioGroup
|
||||
name={field.name}
|
||||
value={field.state.value}
|
||||
onValueChange={field.handleChange}
|
||||
aria-invalid={isInvalid}
|
||||
>
|
||||
<FieldLabel htmlFor="basic">
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldTitle>Basic</FieldTitle>
|
||||
<FieldDescription>
|
||||
For individuals and small teams
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<RadioGroupItem
|
||||
value="basic"
|
||||
id="basic"
|
||||
aria-invalid={isInvalid}
|
||||
/>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
<FieldLabel htmlFor="pro">
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldTitle>Pro</FieldTitle>
|
||||
<FieldDescription>
|
||||
For businesses with higher demands
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<RadioGroupItem
|
||||
value="pro"
|
||||
id="pro"
|
||||
aria-invalid={isInvalid}
|
||||
/>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
</RadioGroup>
|
||||
{isInvalid && (
|
||||
<FieldError errors={field.state.meta.errors} />
|
||||
)}
|
||||
</FieldSet>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<FieldSeparator />
|
||||
<form.Field
|
||||
name="billingPeriod"
|
||||
children={(field) => {
|
||||
const isInvalid =
|
||||
field.state.meta.isTouched && !field.state.meta.isValid
|
||||
return (
|
||||
<Field data-invalid={isInvalid}>
|
||||
<FieldLabel htmlFor={field.name}>
|
||||
Billing Period
|
||||
</FieldLabel>
|
||||
<Select
|
||||
name={field.name}
|
||||
value={field.state.value}
|
||||
onValueChange={field.handleChange}
|
||||
aria-invalid={isInvalid}
|
||||
>
|
||||
<SelectTrigger id={field.name}>
|
||||
<SelectValue placeholder="Select billing period" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="monthly">Monthly</SelectItem>
|
||||
<SelectItem value="yearly">Yearly</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FieldDescription>
|
||||
Choose how often you want to be billed.
|
||||
</FieldDescription>
|
||||
{isInvalid && (
|
||||
<FieldError errors={field.state.meta.errors} />
|
||||
)}
|
||||
</Field>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<FieldSeparator />
|
||||
<form.Field
|
||||
name="addons"
|
||||
mode="array"
|
||||
children={(field) => {
|
||||
const isInvalid =
|
||||
field.state.meta.isTouched && !field.state.meta.isValid
|
||||
return (
|
||||
<FieldSet data-invalid={isInvalid}>
|
||||
<FieldLegend>Add-ons</FieldLegend>
|
||||
<FieldDescription>
|
||||
Select additional features you'd like to include.
|
||||
</FieldDescription>
|
||||
<FieldGroup data-slot="checkbox-group">
|
||||
{addons.map((addon) => (
|
||||
<Field key={addon.id} orientation="horizontal">
|
||||
<Checkbox
|
||||
id={addon.id}
|
||||
name={field.name}
|
||||
aria-invalid={isInvalid}
|
||||
checked={field.state.value.includes(addon.id)}
|
||||
onCheckedChange={(checked) => {
|
||||
if (checked) {
|
||||
field.pushValue(addon.id)
|
||||
} else {
|
||||
const index = field.state.value.indexOf(
|
||||
addon.id
|
||||
)
|
||||
if (index > -1) {
|
||||
field.removeValue(index)
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor={addon.id}>
|
||||
{addon.title}
|
||||
</FieldLabel>
|
||||
<FieldDescription>
|
||||
{addon.description}
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
</Field>
|
||||
))}
|
||||
</FieldGroup>
|
||||
{isInvalid && (
|
||||
<FieldError errors={field.state.meta.errors} />
|
||||
)}
|
||||
</FieldSet>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<FieldSeparator />
|
||||
<form.Field
|
||||
name="teamSize"
|
||||
children={(field) => {
|
||||
const isInvalid =
|
||||
field.state.meta.isTouched && !field.state.meta.isValid
|
||||
return (
|
||||
<Field data-invalid={isInvalid}>
|
||||
<FieldTitle>Team Size</FieldTitle>
|
||||
<FieldDescription>
|
||||
How many people will be using the subscription?
|
||||
</FieldDescription>
|
||||
<Slider
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
value={[field.state.value]}
|
||||
onValueChange={(value) => field.handleChange(value[0])}
|
||||
min={1}
|
||||
max={50}
|
||||
step={10}
|
||||
/>
|
||||
{isInvalid && (
|
||||
<FieldError errors={field.state.meta.errors} />
|
||||
)}
|
||||
</Field>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<FieldSeparator />
|
||||
<form.Field
|
||||
name="emailNotifications"
|
||||
children={(field) => {
|
||||
const isInvalid =
|
||||
field.state.meta.isTouched && !field.state.meta.isValid
|
||||
return (
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor={field.name}>
|
||||
Email Notifications
|
||||
</FieldLabel>
|
||||
<FieldDescription>
|
||||
Receive email updates about your subscription
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<Switch
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
checked={field.state.value}
|
||||
onCheckedChange={field.handleChange}
|
||||
/>
|
||||
{isInvalid && (
|
||||
<FieldError errors={field.state.meta.errors} />
|
||||
)}
|
||||
</Field>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<FieldSeparator />
|
||||
<form.Field
|
||||
name="startDate"
|
||||
children={(field) => {
|
||||
const isInvalid =
|
||||
field.state.meta.isTouched && !field.state.meta.isValid
|
||||
return (
|
||||
<Field data-invalid={isInvalid}>
|
||||
<FieldLabel htmlFor={field.name}>Start Date</FieldLabel>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
id={field.name}
|
||||
variant="outline"
|
||||
className="justify-start"
|
||||
>
|
||||
{field.state.value ? (
|
||||
format(field.state.value, "PPP")
|
||||
) : (
|
||||
<span>Pick a date</span>
|
||||
)}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0" align="start">
|
||||
<Calendar
|
||||
required
|
||||
mode="single"
|
||||
selected={field.state.value}
|
||||
onSelect={field.handleChange}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<FieldDescription>
|
||||
Choose when your subscription should start
|
||||
</FieldDescription>
|
||||
{isInvalid && (
|
||||
<FieldError errors={field.state.meta.errors} />
|
||||
)}
|
||||
</Field>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<FieldSeparator />
|
||||
<form.Field
|
||||
name="theme"
|
||||
children={(field) => {
|
||||
const isInvalid =
|
||||
field.state.meta.isTouched && !field.state.meta.isValid
|
||||
return (
|
||||
<Field data-invalid={isInvalid}>
|
||||
<FieldTitle>Theme Preference</FieldTitle>
|
||||
<ToggleGroup
|
||||
id={field.name}
|
||||
type="single"
|
||||
variant="outline"
|
||||
value={field.state.value}
|
||||
onValueChange={(value) =>
|
||||
value && field.handleChange(value)
|
||||
}
|
||||
aria-invalid={isInvalid}
|
||||
>
|
||||
<ToggleGroupItem value="light">Light</ToggleGroupItem>
|
||||
<ToggleGroupItem value="dark">Dark</ToggleGroupItem>
|
||||
<ToggleGroupItem value="system">System</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
<FieldDescription>
|
||||
Choose your preferred color theme
|
||||
</FieldDescription>
|
||||
{isInvalid && (
|
||||
<FieldError errors={field.state.meta.errors} />
|
||||
)}
|
||||
</Field>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<FieldSeparator />
|
||||
<form.Field
|
||||
name="password"
|
||||
children={(field) => {
|
||||
const isInvalid =
|
||||
field.state.meta.isTouched && !field.state.meta.isValid
|
||||
return (
|
||||
<Field data-invalid={isInvalid}>
|
||||
<FieldLabel htmlFor={field.name}>Password</FieldLabel>
|
||||
<Input
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
type="password"
|
||||
value={field.state.value}
|
||||
onChange={(e) => field.handleChange(e.target.value)}
|
||||
onBlur={field.handleBlur}
|
||||
placeholder="Enter your password"
|
||||
aria-invalid={isInvalid}
|
||||
/>
|
||||
<FieldDescription>
|
||||
Must contain uppercase, lowercase, number, and be 8+
|
||||
characters
|
||||
</FieldDescription>
|
||||
{isInvalid && (
|
||||
<FieldError errors={field.state.meta.errors} />
|
||||
)}
|
||||
</Field>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<FieldSeparator />
|
||||
<form.Field
|
||||
name="comments"
|
||||
children={(field) => {
|
||||
const isInvalid =
|
||||
field.state.meta.isTouched && !field.state.meta.isValid
|
||||
return (
|
||||
<Field data-invalid={isInvalid}>
|
||||
<FieldLabel htmlFor={field.name}>
|
||||
Additional Comments
|
||||
</FieldLabel>
|
||||
<Textarea
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
value={field.state.value}
|
||||
onChange={(e) => field.handleChange(e.target.value)}
|
||||
onBlur={field.handleBlur}
|
||||
placeholder="Tell us more about your needs..."
|
||||
rows={3}
|
||||
aria-invalid={isInvalid}
|
||||
/>
|
||||
<FieldDescription>
|
||||
Share any additional requirements or feedback (10-240
|
||||
characters)
|
||||
</FieldDescription>
|
||||
{isInvalid && (
|
||||
<FieldError errors={field.state.meta.errors} />
|
||||
)}
|
||||
</Field>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</FieldGroup>
|
||||
</form>
|
||||
</CardContent>
|
||||
<CardFooter className="border-t">
|
||||
<Field orientation="horizontal" className="justify-end">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => form.reset()}
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
<Button type="submit" form="example-form">
|
||||
Submit
|
||||
</Button>
|
||||
</Field>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Submitted Values</DialogTitle>
|
||||
<DialogDescription>
|
||||
Here are the values you submitted.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<pre className="overflow-x-auto rounded-md bg-black p-4 font-mono text-sm text-white">
|
||||
<code>{JSON.stringify(values, null, 2)}</code>
|
||||
</pre>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { ExampleForm } from "@/app/(internal)/sink/(pages)/tanstack-form/example-form"
|
||||
|
||||
export default function TanstackFormPage() {
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center p-4">
|
||||
<ExampleForm />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
54
apps/v4/app/(internal)/sink/[name]/page.tsx
Normal file
54
apps/v4/app/(internal)/sink/[name]/page.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import { Metadata } from "next"
|
||||
import { notFound } from "next/navigation"
|
||||
|
||||
import { componentRegistry } from "@/app/(internal)/sink/component-registry"
|
||||
|
||||
export const dynamic = "force-static"
|
||||
export const revalidate = false
|
||||
|
||||
export async function generateStaticParams() {
|
||||
return Object.keys(componentRegistry).map((name) => ({
|
||||
name,
|
||||
}))
|
||||
}
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ name: string }>
|
||||
}): Promise<Metadata> {
|
||||
const { name } = await params
|
||||
const component = componentRegistry[name as keyof typeof componentRegistry]
|
||||
|
||||
if (!component) {
|
||||
return {
|
||||
title: "Component Not Found",
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
title: `${component.name} - Kitchen Sink`,
|
||||
description: `Demo page for ${component.name} component`,
|
||||
}
|
||||
}
|
||||
|
||||
export default async function ComponentPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ name: string }>
|
||||
}) {
|
||||
const { name } = await params
|
||||
const component = componentRegistry[name as keyof typeof componentRegistry]
|
||||
|
||||
if (!component) {
|
||||
notFound()
|
||||
}
|
||||
|
||||
const Component = component.component
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<Component />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
437
apps/v4/app/(internal)/sink/component-registry.ts
Normal file
437
apps/v4/app/(internal)/sink/component-registry.ts
Normal file
@@ -0,0 +1,437 @@
|
||||
import FormsPage from "@/app/(internal)/sink/(pages)/forms/page"
|
||||
|
||||
import NextFormPage from "./(pages)/next-form/page"
|
||||
import ReactHookFormPage from "./(pages)/react-hook-form/page"
|
||||
import TanstackFormPage from "./(pages)/tanstack-form/page"
|
||||
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 { ButtonGroupDemo } from "./components/button-group-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 { 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 { EmptyDemo } from "./components/empty-demo"
|
||||
import { FieldDemo } from "./components/field-demo"
|
||||
import { FormDemo } from "./components/form-demo"
|
||||
import { HoverCardDemo } from "./components/hover-card-demo"
|
||||
import { InputDemo } from "./components/input-demo"
|
||||
import { InputGroupDemo } from "./components/input-group-demo"
|
||||
import { InputOTPDemo } from "./components/input-otp-demo"
|
||||
import { ItemDemo } from "./components/item-demo"
|
||||
import { KbdDemo } from "./components/kbd-demo"
|
||||
import { LabelDemo } from "./components/label-demo"
|
||||
import { MenubarDemo } from "./components/menubar-demo"
|
||||
import { NativeSelectDemo } from "./components/native-select-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 { SpinnerDemo } from "./components/spinner-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"
|
||||
|
||||
type ComponentConfig = {
|
||||
name: string
|
||||
component: React.ComponentType
|
||||
className?: string
|
||||
type: "registry:ui" | "registry:page" | "registry:block"
|
||||
href: string
|
||||
label?: string
|
||||
}
|
||||
|
||||
export const componentRegistry: Record<string, ComponentConfig> = {
|
||||
accordion: {
|
||||
name: "Accordion",
|
||||
component: AccordionDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/accordion",
|
||||
},
|
||||
alert: {
|
||||
name: "Alert",
|
||||
component: AlertDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/alert",
|
||||
},
|
||||
"alert-dialog": {
|
||||
name: "Alert Dialog",
|
||||
component: AlertDialogDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/alert-dialog",
|
||||
},
|
||||
"aspect-ratio": {
|
||||
name: "Aspect Ratio",
|
||||
component: AspectRatioDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/aspect-ratio",
|
||||
},
|
||||
avatar: {
|
||||
name: "Avatar",
|
||||
component: AvatarDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/avatar",
|
||||
},
|
||||
badge: {
|
||||
name: "Badge",
|
||||
component: BadgeDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/badge",
|
||||
},
|
||||
breadcrumb: {
|
||||
name: "Breadcrumb",
|
||||
component: BreadcrumbDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/breadcrumb",
|
||||
},
|
||||
button: {
|
||||
name: "Button",
|
||||
component: ButtonDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/button",
|
||||
},
|
||||
"button-group": {
|
||||
name: "Button Group",
|
||||
component: ButtonGroupDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/button-group",
|
||||
label: "New",
|
||||
},
|
||||
calendar: {
|
||||
name: "Calendar",
|
||||
component: CalendarDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/calendar",
|
||||
},
|
||||
card: {
|
||||
name: "Card",
|
||||
component: CardDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/card",
|
||||
},
|
||||
carousel: {
|
||||
name: "Carousel",
|
||||
component: CarouselDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/carousel",
|
||||
},
|
||||
chart: {
|
||||
name: "Chart",
|
||||
component: ChartDemo,
|
||||
className: "w-full",
|
||||
type: "registry:ui",
|
||||
href: "/sink/chart",
|
||||
},
|
||||
checkbox: {
|
||||
name: "Checkbox",
|
||||
component: CheckboxDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/checkbox",
|
||||
},
|
||||
collapsible: {
|
||||
name: "Collapsible",
|
||||
component: CollapsibleDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/collapsible",
|
||||
},
|
||||
combobox: {
|
||||
name: "Combobox",
|
||||
component: ComboboxDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/combobox",
|
||||
},
|
||||
command: {
|
||||
name: "Command",
|
||||
component: CommandDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/command",
|
||||
},
|
||||
"context-menu": {
|
||||
name: "Context Menu",
|
||||
component: ContextMenuDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/context-menu",
|
||||
},
|
||||
"date-picker": {
|
||||
name: "Date Picker",
|
||||
component: DatePickerDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/date-picker",
|
||||
},
|
||||
dialog: {
|
||||
name: "Dialog",
|
||||
component: DialogDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/dialog",
|
||||
},
|
||||
drawer: {
|
||||
name: "Drawer",
|
||||
component: DrawerDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/drawer",
|
||||
},
|
||||
"dropdown-menu": {
|
||||
name: "Dropdown Menu",
|
||||
component: DropdownMenuDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/dropdown-menu",
|
||||
},
|
||||
empty: {
|
||||
name: "Empty",
|
||||
component: EmptyDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/empty",
|
||||
label: "New",
|
||||
},
|
||||
field: {
|
||||
name: "Field",
|
||||
component: FieldDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/field",
|
||||
label: "New",
|
||||
},
|
||||
form: {
|
||||
name: "Form",
|
||||
component: FormDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/form",
|
||||
},
|
||||
"hover-card": {
|
||||
name: "Hover Card",
|
||||
component: HoverCardDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/hover-card",
|
||||
},
|
||||
input: {
|
||||
name: "Input",
|
||||
component: InputDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/input",
|
||||
},
|
||||
"input-group": {
|
||||
name: "Input Group",
|
||||
component: InputGroupDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/input-group",
|
||||
label: "New",
|
||||
},
|
||||
"input-otp": {
|
||||
name: "Input OTP",
|
||||
component: InputOTPDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/input-otp",
|
||||
},
|
||||
item: {
|
||||
name: "Item",
|
||||
component: ItemDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/item",
|
||||
label: "New",
|
||||
},
|
||||
kbd: {
|
||||
name: "Kbd",
|
||||
component: KbdDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/kbd",
|
||||
label: "New",
|
||||
},
|
||||
label: {
|
||||
name: "Label",
|
||||
component: LabelDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/label",
|
||||
},
|
||||
menubar: {
|
||||
name: "Menubar",
|
||||
component: MenubarDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/menubar",
|
||||
},
|
||||
"navigation-menu": {
|
||||
name: "Navigation Menu",
|
||||
component: NavigationMenuDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/navigation-menu",
|
||||
},
|
||||
"native-select": {
|
||||
name: "Native Select",
|
||||
component: NativeSelectDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/native-select",
|
||||
label: "New",
|
||||
},
|
||||
pagination: {
|
||||
name: "Pagination",
|
||||
component: PaginationDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/pagination",
|
||||
},
|
||||
popover: {
|
||||
name: "Popover",
|
||||
component: PopoverDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/popover",
|
||||
},
|
||||
progress: {
|
||||
name: "Progress",
|
||||
component: ProgressDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/progress",
|
||||
},
|
||||
"radio-group": {
|
||||
name: "Radio Group",
|
||||
component: RadioGroupDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/radio-group",
|
||||
},
|
||||
resizable: {
|
||||
name: "Resizable",
|
||||
component: ResizableDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/resizable",
|
||||
},
|
||||
"scroll-area": {
|
||||
name: "Scroll Area",
|
||||
component: ScrollAreaDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/scroll-area",
|
||||
},
|
||||
select: {
|
||||
name: "Select",
|
||||
component: SelectDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/select",
|
||||
},
|
||||
separator: {
|
||||
name: "Separator",
|
||||
component: SeparatorDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/separator",
|
||||
},
|
||||
sheet: {
|
||||
name: "Sheet",
|
||||
component: SheetDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/sheet",
|
||||
},
|
||||
skeleton: {
|
||||
name: "Skeleton",
|
||||
component: SkeletonDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/skeleton",
|
||||
},
|
||||
slider: {
|
||||
name: "Slider",
|
||||
component: SliderDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/slider",
|
||||
},
|
||||
sonner: {
|
||||
name: "Sonner",
|
||||
component: SonnerDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/sonner",
|
||||
},
|
||||
spinner: {
|
||||
name: "Spinner",
|
||||
component: SpinnerDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/spinner",
|
||||
label: "New",
|
||||
},
|
||||
switch: {
|
||||
name: "Switch",
|
||||
component: SwitchDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/switch",
|
||||
},
|
||||
table: {
|
||||
name: "Table",
|
||||
component: TableDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/table",
|
||||
},
|
||||
tabs: {
|
||||
name: "Tabs",
|
||||
component: TabsDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/tabs",
|
||||
},
|
||||
textarea: {
|
||||
name: "Textarea",
|
||||
component: TextareaDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/textarea",
|
||||
},
|
||||
toggle: {
|
||||
name: "Toggle",
|
||||
component: ToggleDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/toggle",
|
||||
},
|
||||
"toggle-group": {
|
||||
name: "Toggle Group",
|
||||
component: ToggleGroupDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/toggle-group",
|
||||
},
|
||||
tooltip: {
|
||||
name: "Tooltip",
|
||||
component: TooltipDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/tooltip",
|
||||
},
|
||||
blocks: {
|
||||
name: "Forms",
|
||||
component: FormsPage,
|
||||
type: "registry:page",
|
||||
href: "/sink/forms",
|
||||
},
|
||||
"next-form": {
|
||||
name: "Next.js Form",
|
||||
component: NextFormPage,
|
||||
type: "registry:page",
|
||||
href: "/sink/next-form",
|
||||
},
|
||||
"tanstack-form": {
|
||||
name: "Tanstack Form",
|
||||
component: TanstackFormPage,
|
||||
type: "registry:page",
|
||||
href: "/sink/tanstack-form",
|
||||
},
|
||||
"react-hook-form": {
|
||||
name: "React Hook Form",
|
||||
component: ReactHookFormPage,
|
||||
type: "registry:page",
|
||||
href: "/sink/react-hook-form",
|
||||
},
|
||||
}
|
||||
|
||||
export type ComponentKey = keyof typeof componentRegistry
|
||||
43
apps/v4/app/(internal)/sink/components/app-breadcrumbs.tsx
Normal file
43
apps/v4/app/(internal)/sink/components/app-breadcrumbs.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
"use client"
|
||||
|
||||
import { useParams } from "next/navigation"
|
||||
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbList,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
} from "@/registry/new-york-v4/ui/breadcrumb"
|
||||
|
||||
export function AppBreadcrumbs() {
|
||||
const params = useParams()
|
||||
const { name } = params
|
||||
|
||||
if (!name) {
|
||||
return (
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbPage>Kitchen Sink</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink href="/sink">Kitchen Sink</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator className="hidden sm:flex" />
|
||||
<BreadcrumbItem className="hidden sm:block">
|
||||
<BreadcrumbPage className="capitalize">{name}</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
)
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import * as React from "react"
|
||||
import Link from "next/link"
|
||||
import { usePathname } from "next/navigation"
|
||||
import {
|
||||
AudioWaveform,
|
||||
BookOpen,
|
||||
@@ -9,7 +10,7 @@ import {
|
||||
ChevronRightIcon,
|
||||
Command,
|
||||
GalleryVerticalEnd,
|
||||
Search,
|
||||
SearchIcon,
|
||||
Settings2,
|
||||
SquareTerminal,
|
||||
} from "lucide-react"
|
||||
@@ -22,6 +23,11 @@ import {
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from "@/registry/new-york-v4/ui/collapsible"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupInput,
|
||||
} from "@/registry/new-york-v4/ui/input-group"
|
||||
import { Label } from "@/registry/new-york-v4/ui/label"
|
||||
import {
|
||||
Sidebar,
|
||||
@@ -31,7 +37,6 @@ import {
|
||||
SidebarGroupContent,
|
||||
SidebarGroupLabel,
|
||||
SidebarHeader,
|
||||
SidebarInput,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
@@ -40,6 +45,7 @@ import {
|
||||
SidebarMenuSubItem,
|
||||
SidebarRail,
|
||||
} from "@/registry/new-york-v4/ui/sidebar"
|
||||
import { componentRegistry } from "@/app/(internal)/sink/component-registry"
|
||||
|
||||
// This is sample data.
|
||||
const data = {
|
||||
@@ -163,8 +169,9 @@ const data = {
|
||||
}
|
||||
|
||||
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
const pathname = usePathname()
|
||||
return (
|
||||
<Sidebar collapsible="icon" {...props}>
|
||||
<Sidebar side="left" collapsible="icon" {...props}>
|
||||
<SidebarHeader>
|
||||
<TeamSwitcher teams={data.teams} />
|
||||
<SidebarGroup className="py-0 group-data-[collapsible=icon]:hidden">
|
||||
@@ -173,12 +180,17 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
<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" />
|
||||
<InputGroup className="bg-background h-8 shadow-none">
|
||||
<InputGroupInput
|
||||
id="search"
|
||||
placeholder="Search the docs..."
|
||||
className="h-7"
|
||||
data-slot="input-group-control"
|
||||
/>
|
||||
<InputGroupAddon>
|
||||
<SearchIcon className="text-muted-foreground" />
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</form>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
@@ -221,17 +233,58 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
</SidebarMenu>
|
||||
</SidebarGroup>
|
||||
<SidebarGroup className="group-data-[collapsible=icon]:hidden">
|
||||
<SidebarGroupLabel>Components</SidebarGroupLabel>
|
||||
<SidebarMenu>
|
||||
{data.components.map((item) => (
|
||||
<SidebarMenuItem key={item.name}>
|
||||
<SidebarMenuButton asChild>
|
||||
<Link href={`/sink#${item.name}`}>
|
||||
<span>{getComponentName(item.name)}</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
))}
|
||||
{["registry:ui", "registry:page", "registry:block"].map((type) => {
|
||||
const typeComponents = Object.entries(componentRegistry).filter(
|
||||
([, item]) => item.type === type
|
||||
)
|
||||
if (typeComponents.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Collapsible
|
||||
key={type}
|
||||
asChild
|
||||
defaultOpen={pathname.includes("/sink/")}
|
||||
className="group/collapsible"
|
||||
>
|
||||
<SidebarMenuItem>
|
||||
<CollapsibleTrigger asChild>
|
||||
<SidebarMenuButton>
|
||||
<span>
|
||||
{type === "registry:ui"
|
||||
? "Components"
|
||||
: type === "registry:page"
|
||||
? "Pages"
|
||||
: "Blocks"}
|
||||
</span>
|
||||
<ChevronRightIcon className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
|
||||
</SidebarMenuButton>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<SidebarMenuSub>
|
||||
{typeComponents.map(([key, item]) => (
|
||||
<SidebarMenuSubItem key={key}>
|
||||
<SidebarMenuSubButton
|
||||
asChild
|
||||
isActive={pathname === item.href}
|
||||
>
|
||||
<Link href={item.href}>
|
||||
<span>{item.name}</span>
|
||||
{item.label && (
|
||||
<span className="flex size-2 rounded-full bg-blue-500" />
|
||||
)}
|
||||
</Link>
|
||||
</SidebarMenuSubButton>
|
||||
</SidebarMenuSubItem>
|
||||
))}
|
||||
</SidebarMenuSub>
|
||||
</CollapsibleContent>
|
||||
</SidebarMenuItem>
|
||||
</Collapsible>
|
||||
)
|
||||
})}
|
||||
</SidebarMenu>
|
||||
</SidebarGroup>
|
||||
</SidebarContent>
|
||||
@@ -242,8 +295,3 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
</Sidebar>
|
||||
)
|
||||
}
|
||||
|
||||
function getComponentName(name: string) {
|
||||
// convert kebab-case to title case
|
||||
return name.replace(/-/g, " ").replace(/\b\w/g, (char) => char.toUpperCase())
|
||||
}
|
||||
|
||||
@@ -31,7 +31,10 @@ export function AvatarDemo() {
|
||||
<AvatarFallback>CN</AvatarFallback>
|
||||
</Avatar>
|
||||
<Avatar>
|
||||
<AvatarImage src="https://github.com/leerob.png" alt="@leerob" />
|
||||
<AvatarImage
|
||||
src="https://github.com/maxleiter.png"
|
||||
alt="@maxleiter"
|
||||
/>
|
||||
<AvatarFallback>LR</AvatarFallback>
|
||||
</Avatar>
|
||||
<Avatar>
|
||||
@@ -48,7 +51,10 @@ export function AvatarDemo() {
|
||||
<AvatarFallback>CN</AvatarFallback>
|
||||
</Avatar>
|
||||
<Avatar>
|
||||
<AvatarImage src="https://github.com/leerob.png" alt="@leerob" />
|
||||
<AvatarImage
|
||||
src="https://github.com/maxleiter.png"
|
||||
alt="@maxleiter"
|
||||
/>
|
||||
<AvatarFallback>LR</AvatarFallback>
|
||||
</Avatar>
|
||||
<Avatar>
|
||||
@@ -65,7 +71,10 @@ export function AvatarDemo() {
|
||||
<AvatarFallback>CN</AvatarFallback>
|
||||
</Avatar>
|
||||
<Avatar>
|
||||
<AvatarImage src="https://github.com/leerob.png" alt="@leerob" />
|
||||
<AvatarImage
|
||||
src="https://github.com/maxleiter.png"
|
||||
alt="@maxleiter"
|
||||
/>
|
||||
<AvatarFallback>LR</AvatarFallback>
|
||||
</Avatar>
|
||||
<Avatar>
|
||||
|
||||
581
apps/v4/app/(internal)/sink/components/button-group-demo.tsx
Normal file
581
apps/v4/app/(internal)/sink/components/button-group-demo.tsx
Normal file
@@ -0,0 +1,581 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import {
|
||||
IconArrowRight,
|
||||
IconBrandGithubCopilot,
|
||||
IconChevronDown,
|
||||
IconCircleCheck,
|
||||
IconCloudCode,
|
||||
IconHeart,
|
||||
IconMinus,
|
||||
IconPin,
|
||||
IconPlus,
|
||||
IconUserCircle,
|
||||
} from "@tabler/icons-react"
|
||||
import {
|
||||
AlertTriangleIcon,
|
||||
ArrowLeftIcon,
|
||||
ArrowRightIcon,
|
||||
AudioLinesIcon,
|
||||
CheckIcon,
|
||||
ChevronDownIcon,
|
||||
CopyIcon,
|
||||
FlipHorizontalIcon,
|
||||
FlipVerticalIcon,
|
||||
MoreHorizontalIcon,
|
||||
PercentIcon,
|
||||
RotateCwIcon,
|
||||
SearchIcon,
|
||||
ShareIcon,
|
||||
TrashIcon,
|
||||
UserRoundXIcon,
|
||||
VolumeOffIcon,
|
||||
} from "lucide-react"
|
||||
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import {
|
||||
ButtonGroup,
|
||||
ButtonGroupSeparator,
|
||||
ButtonGroupText,
|
||||
} from "@/registry/new-york-v4/ui/button-group"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/registry/new-york-v4/ui/dropdown-menu"
|
||||
import { Field, FieldGroup } from "@/registry/new-york-v4/ui/field"
|
||||
import { Input } from "@/registry/new-york-v4/ui/input"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupInput,
|
||||
} from "@/registry/new-york-v4/ui/input-group"
|
||||
import { Label } from "@/registry/new-york-v4/ui/label"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/registry/new-york-v4/ui/popover"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/registry/new-york-v4/ui/select"
|
||||
import { Separator } from "@/registry/new-york-v4/ui/separator"
|
||||
import { Textarea } from "@/registry/new-york-v4/ui/textarea"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/registry/new-york-v4/ui/tooltip"
|
||||
|
||||
export function ButtonGroupDemo() {
|
||||
const [currency, setCurrency] = useState("$")
|
||||
return (
|
||||
<div className="flex gap-12">
|
||||
<div className="flex max-w-sm flex-col gap-6">
|
||||
<ButtonGroup>
|
||||
<Button>Button</Button>
|
||||
<Button>
|
||||
Get Started <IconArrowRight />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup>
|
||||
<Button>Button</Button>
|
||||
<ButtonGroupSeparator className="bg-primary/80" />
|
||||
<Button>
|
||||
Get Started <IconArrowRight />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup>
|
||||
<Button variant="outline">Button</Button>
|
||||
<Input placeholder="Type something here..." />
|
||||
</ButtonGroup>
|
||||
<ButtonGroup>
|
||||
<Input placeholder="Type something here..." />
|
||||
<Button variant="outline">Button</Button>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup>
|
||||
<Button variant="outline">Button</Button>
|
||||
<Button variant="outline">Another Button</Button>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup>
|
||||
<ButtonGroupText>Text</ButtonGroupText>
|
||||
<Button variant="outline">Another Button</Button>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup>
|
||||
<ButtonGroupText asChild>
|
||||
<Label htmlFor="input">
|
||||
<IconCloudCode /> GPU Size
|
||||
</Label>
|
||||
</ButtonGroupText>
|
||||
<Input id="input" placeholder="Type something here..." />
|
||||
</ButtonGroup>
|
||||
<ButtonGroup>
|
||||
<ButtonGroupText>Prefix</ButtonGroupText>
|
||||
<Input id="input" placeholder="Type something here..." />
|
||||
<ButtonGroupText>Suffix</ButtonGroupText>
|
||||
</ButtonGroup>
|
||||
<div className="flex gap-4">
|
||||
<ButtonGroup>
|
||||
<Button variant="outline">Update</Button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline">
|
||||
<ChevronDownIcon />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem>Disable</DropdownMenuItem>
|
||||
<DropdownMenuItem variant="destructive">
|
||||
Uninstall
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup className="[--radius:9999px]">
|
||||
<Button variant="outline">Follow</Button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" className="!pl-2">
|
||||
<ChevronDownIcon />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="[--radius:0.95rem]">
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>
|
||||
<VolumeOffIcon />
|
||||
Mute Conversation
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<CheckIcon />
|
||||
Mark as Read
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<AlertTriangleIcon />
|
||||
Report Conversation
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<UserRoundXIcon />
|
||||
Block User
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<ShareIcon />
|
||||
Share Conversation
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<CopyIcon />
|
||||
Copy Conversation
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem variant="destructive">
|
||||
<TrashIcon />
|
||||
Delete Conversation
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup className="[--radius:0.9rem]">
|
||||
<Button variant="secondary">Actions</Button>
|
||||
<ButtonGroupSeparator />
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="secondary">
|
||||
<MoreHorizontalIcon />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="[--radius:0.9rem]">
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>
|
||||
<IconCircleCheck />
|
||||
Select Messages
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<IconPin />
|
||||
Edit Pins
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<IconUserCircle />
|
||||
Set Up Name & Photo
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
<Field>
|
||||
<Label htmlFor="amount">Amount</Label>
|
||||
<ButtonGroup>
|
||||
<Select value={currency} onValueChange={setCurrency}>
|
||||
<SelectTrigger className="font-mono">{currency}</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="$">$</SelectItem>
|
||||
<SelectItem value="€">€</SelectItem>
|
||||
<SelectItem value="£">£</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Input placeholder="Enter amount to send" />
|
||||
<Button variant="outline">
|
||||
<ArrowRightIcon />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</Field>
|
||||
</div>
|
||||
<div className="flex max-w-xs flex-col gap-6">
|
||||
<ButtonGroup className="[--spacing:0.2rem]">
|
||||
<Button variant="outline">
|
||||
<FlipHorizontalIcon />
|
||||
</Button>
|
||||
<Button variant="outline">
|
||||
<FlipVerticalIcon />
|
||||
</Button>
|
||||
<Button variant="outline">
|
||||
<RotateCwIcon />
|
||||
</Button>
|
||||
<InputGroup>
|
||||
<InputGroupInput placeholder="0.00" />
|
||||
<InputGroupAddon
|
||||
align="inline-end"
|
||||
className="text-muted-foreground"
|
||||
>
|
||||
<PercentIcon />
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</ButtonGroup>
|
||||
<div className="flex gap-2 [--radius:0.95rem] [--ring:var(--color-blue-300)] [--spacing:0.22rem] **:[.shadow-xs]:shadow-none">
|
||||
<InputGroup>
|
||||
<InputGroupInput placeholder="Type to search..." />
|
||||
<InputGroupAddon
|
||||
align="inline-start"
|
||||
className="text-muted-foreground"
|
||||
>
|
||||
<SearchIcon />
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<ButtonGroup>
|
||||
<Button variant="outline">
|
||||
<IconBrandGithubCopilot />
|
||||
</Button>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline">
|
||||
<IconCloudCode />
|
||||
<IconChevronDown />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent align="end" className="rounded-xl p-0 text-sm">
|
||||
<div className="px-4 py-3">
|
||||
<div className="text-sm font-medium">Agent Tasks</div>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="p-4 *:[p:not(:last-child)]:mb-2">
|
||||
<Textarea
|
||||
placeholder="Describe your task in natural language."
|
||||
className="mb-4 resize-none"
|
||||
/>
|
||||
<p className="font-medium">Start a new task with Copilot</p>
|
||||
<p className="text-muted-foreground">
|
||||
Describe your task in natural language. Copilot will work in
|
||||
the background and open a pull request for your review.
|
||||
</p>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
<FieldGroup className="grid grid-cols-2 gap-4 [--spacing:0.22rem]">
|
||||
<Field>
|
||||
<Label htmlFor="width">Width</Label>
|
||||
<ButtonGroup>
|
||||
<InputGroup>
|
||||
<InputGroupInput id="width" />
|
||||
<InputGroupAddon className="text-muted-foreground">
|
||||
W
|
||||
</InputGroupAddon>
|
||||
<InputGroupAddon
|
||||
align="inline-end"
|
||||
className="text-muted-foreground"
|
||||
>
|
||||
px
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<Button variant="outline" size="icon">
|
||||
<IconMinus />
|
||||
</Button>
|
||||
<Button variant="outline" size="icon">
|
||||
<IconPlus />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</Field>
|
||||
<Field className="w-full">
|
||||
<Label htmlFor="color">Color</Label>
|
||||
<ButtonGroup className="w-full">
|
||||
<InputGroup>
|
||||
<InputGroupInput id="color" />
|
||||
<InputGroupAddon align="inline-start">
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<InputGroupButton>
|
||||
<span className="size-4 rounded-xs bg-blue-600" />
|
||||
</InputGroupButton>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
align="start"
|
||||
className="max-w-48 rounded-lg p-2"
|
||||
alignOffset={-8}
|
||||
sideOffset={8}
|
||||
>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{[
|
||||
"#EA4335", // Red
|
||||
"#FBBC04", // Yellow
|
||||
"#34A853", // Green
|
||||
"#4285F4", // Blue
|
||||
"#9333EA", // Purple
|
||||
"#EC4899", // Pink
|
||||
"#10B981", // Emerald
|
||||
"#F97316", // Orange
|
||||
"#6366F1", // Indigo
|
||||
"#14B8A6", // Teal
|
||||
"#8B5CF6", // Violet
|
||||
"#F59E0B", // Amber
|
||||
].map((color) => (
|
||||
<div
|
||||
key={color}
|
||||
className="size-6 cursor-pointer rounded-sm transition-transform hover:scale-110"
|
||||
style={{ backgroundColor: color }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</InputGroupAddon>
|
||||
<InputGroupAddon
|
||||
align="inline-end"
|
||||
className="text-muted-foreground"
|
||||
>
|
||||
%
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</ButtonGroup>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
<ButtonGroup>
|
||||
<Button variant="outline">
|
||||
<IconHeart /> Like
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
asChild
|
||||
className="text-muted-foreground pointer-events-none px-2"
|
||||
>
|
||||
<span>1.2K</span>
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<ExportButtonGroup />
|
||||
<ButtonGroup>
|
||||
<Select defaultValue="hours">
|
||||
<SelectTrigger id="duration">
|
||||
<SelectValue placeholder="Select duration" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="hours">Hours</SelectItem>
|
||||
<SelectItem value="days">Days</SelectItem>
|
||||
<SelectItem value="weeks">Weeks</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Input />
|
||||
</ButtonGroup>
|
||||
<ButtonGroup className="[--radius:9999rem]">
|
||||
<ButtonGroup>
|
||||
<Button variant="outline" size="icon">
|
||||
<IconPlus />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup>
|
||||
<InputGroup>
|
||||
<InputGroupInput placeholder="Send a message..." />
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<InputGroupAddon align="inline-end">
|
||||
<AudioLinesIcon />
|
||||
</InputGroupAddon>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Voice Mode</TooltipContent>
|
||||
</Tooltip>
|
||||
</InputGroup>
|
||||
</ButtonGroup>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup>
|
||||
<Button variant="outline" size="sm">
|
||||
<ArrowLeftIcon />
|
||||
Previous
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
1
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
2
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
3
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
4
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
5
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
Next
|
||||
<ArrowRightIcon />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup className="[--radius:0.9rem] [--spacing:0.22rem]">
|
||||
<ButtonGroup>
|
||||
<Button variant="outline">1</Button>
|
||||
<Button variant="outline">2</Button>
|
||||
<Button variant="outline">3</Button>
|
||||
<Button variant="outline">4</Button>
|
||||
<Button variant="outline">5</Button>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup>
|
||||
<Button variant="outline" size="icon">
|
||||
<ArrowLeftIcon />
|
||||
</Button>
|
||||
<Button variant="outline" size="icon">
|
||||
<ArrowRightIcon />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup>
|
||||
<ButtonGroup>
|
||||
<Button variant="outline">
|
||||
<ArrowLeftIcon />
|
||||
</Button>
|
||||
<Button variant="outline">
|
||||
<ArrowRightIcon />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup aria-label="Single navigation button">
|
||||
<Button variant="outline" size="icon">
|
||||
<ArrowLeftIcon />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
<div className="flex max-w-xs flex-col gap-6">
|
||||
<Field>
|
||||
<Label id="alignment-label">Text Alignment</Label>
|
||||
<ButtonGroup aria-labelledby="alignment-label">
|
||||
<Button variant="outline" size="sm">
|
||||
Left
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
Center
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
Right
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
Justify
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</Field>
|
||||
<div className="flex gap-6">
|
||||
<ButtonGroup
|
||||
orientation="vertical"
|
||||
aria-label="Media controls"
|
||||
className="h-fit"
|
||||
>
|
||||
<Button variant="outline" size="icon">
|
||||
<IconPlus />
|
||||
</Button>
|
||||
<Button variant="outline" size="icon">
|
||||
<IconMinus />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup orientation="vertical" aria-label="Design tools palette">
|
||||
<ButtonGroup orientation="vertical">
|
||||
<Button variant="outline" size="icon">
|
||||
<SearchIcon />
|
||||
</Button>
|
||||
<Button variant="outline" size="icon">
|
||||
<CopyIcon />
|
||||
</Button>
|
||||
<Button variant="outline" size="icon">
|
||||
<ShareIcon />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup orientation="vertical">
|
||||
<Button variant="outline" size="icon">
|
||||
<FlipHorizontalIcon />
|
||||
</Button>
|
||||
<Button variant="outline" size="icon">
|
||||
<FlipVerticalIcon />
|
||||
</Button>
|
||||
<Button variant="outline" size="icon">
|
||||
<RotateCwIcon />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup>
|
||||
<Button variant="outline" size="icon">
|
||||
<TrashIcon />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup orientation="vertical">
|
||||
<Button variant="outline" size="sm">
|
||||
<IconPlus /> Increase
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
<IconMinus /> Decrease
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup orientation="vertical">
|
||||
<Button variant="secondary" size="sm">
|
||||
<IconPlus /> Increase
|
||||
</Button>
|
||||
<ButtonGroupSeparator orientation="horizontal" />
|
||||
<Button variant="secondary" size="sm">
|
||||
<IconMinus /> Decrease
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ExportButtonGroup() {
|
||||
const [exportType, setExportType] = useState("pdf")
|
||||
|
||||
return (
|
||||
<ButtonGroup>
|
||||
<Input />
|
||||
<Select value={exportType} onValueChange={setExportType}>
|
||||
<SelectTrigger>
|
||||
<SelectValue asChild>
|
||||
<span>{exportType}</span>
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent align="end">
|
||||
<SelectItem value="pdf">pdf</SelectItem>
|
||||
<SelectItem value="xlsx">xlsx</SelectItem>
|
||||
<SelectItem value="csv">csv</SelectItem>
|
||||
<SelectItem value="json">json</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</ButtonGroup>
|
||||
)
|
||||
}
|
||||
@@ -98,7 +98,10 @@ export function CardDemo() {
|
||||
<AvatarFallback>CN</AvatarFallback>
|
||||
</Avatar>
|
||||
<Avatar>
|
||||
<AvatarImage src="https://github.com/leerob.png" alt="@leerob" />
|
||||
<AvatarImage
|
||||
src="https://github.com/maxleiter.png"
|
||||
alt="@maxleiter"
|
||||
/>
|
||||
<AvatarFallback>LR</AvatarFallback>
|
||||
</Avatar>
|
||||
<Avatar>
|
||||
|
||||
@@ -62,7 +62,7 @@ const users = [
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
username: "leerob",
|
||||
username: "maxleiter",
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
|
||||
@@ -274,7 +274,10 @@ function DropdownMenuAvatarOnly() {
|
||||
className="size-8 rounded-full border-none p-0"
|
||||
>
|
||||
<Avatar>
|
||||
<AvatarImage src="https://github.com/leerob.png" alt="leerob" />
|
||||
<AvatarImage
|
||||
src="https://github.com/maxleiter.png"
|
||||
alt="maxleiter"
|
||||
/>
|
||||
<AvatarFallback className="rounded-lg">LR</AvatarFallback>
|
||||
</Avatar>
|
||||
</Button>
|
||||
@@ -286,13 +289,16 @@ function DropdownMenuAvatarOnly() {
|
||||
<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" />
|
||||
<AvatarImage
|
||||
src="https://github.com/maxleiter.png"
|
||||
alt="maxleiter"
|
||||
/>
|
||||
<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="truncate font-semibold">maxleiter</span>
|
||||
<span className="text-muted-foreground truncate text-xs">
|
||||
leerob@example.com
|
||||
maxleiter@example.com
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
250
apps/v4/app/(internal)/sink/components/empty-demo.tsx
Normal file
250
apps/v4/app/(internal)/sink/components/empty-demo.tsx
Normal file
@@ -0,0 +1,250 @@
|
||||
import { IconArrowUpRight, IconFolderCode } from "@tabler/icons-react"
|
||||
import { PlusIcon, SearchIcon } from "lucide-react"
|
||||
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import { Card, CardContent } from "@/registry/new-york-v4/ui/card"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/registry/new-york-v4/ui/dialog"
|
||||
import {
|
||||
Empty,
|
||||
EmptyContent,
|
||||
EmptyDescription,
|
||||
EmptyHeader,
|
||||
EmptyMedia,
|
||||
EmptyTitle,
|
||||
} from "@/registry/new-york-v4/ui/empty"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupInput,
|
||||
} from "@/registry/new-york-v4/ui/input-group"
|
||||
import { Kbd } from "@/registry/new-york-v4/ui/kbd"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/registry/new-york-v4/ui/popover"
|
||||
|
||||
export function EmptyDemo() {
|
||||
return (
|
||||
<div className="grid w-full gap-8">
|
||||
<Empty className="min-h-[80svh]">
|
||||
<EmptyHeader>
|
||||
<EmptyMedia variant="icon">
|
||||
<IconFolderCode />
|
||||
</EmptyMedia>
|
||||
<EmptyTitle>No projects yet</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
You haven't created any projects yet. Get started by creating
|
||||
your first project.
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
<EmptyContent>
|
||||
<div className="flex gap-2">
|
||||
<Button asChild>
|
||||
<a href="#">Create project</a>
|
||||
</Button>
|
||||
<Button variant="outline">Import project</Button>
|
||||
</div>
|
||||
<Button variant="link" asChild className="text-muted-foreground">
|
||||
<a href="#">
|
||||
Learn more <IconArrowUpRight />
|
||||
</a>
|
||||
</Button>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
<Empty className="bg-muted min-h-[80svh]">
|
||||
<EmptyHeader>
|
||||
<EmptyTitle>No results found</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
No results found for your search. Try adjusting your search terms.
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
<EmptyContent>
|
||||
<Button>Try again</Button>
|
||||
<Button variant="link" asChild className="text-muted-foreground">
|
||||
<a href="#">
|
||||
Learn more <IconArrowUpRight />
|
||||
</a>
|
||||
</Button>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
<Empty className="min-h-[80svh] border">
|
||||
<EmptyHeader>
|
||||
<EmptyTitle>404 - Not Found</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
The page you're looking for doesn't exist. Try searching
|
||||
for what you need below.
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
<EmptyContent>
|
||||
<InputGroup className="w-3/4">
|
||||
<InputGroupInput placeholder="Try searching for pages..." />
|
||||
<InputGroupAddon>
|
||||
<SearchIcon />
|
||||
</InputGroupAddon>
|
||||
<InputGroupAddon align="inline-end">
|
||||
<Kbd>/</Kbd>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<EmptyDescription>
|
||||
Need help? <a href="#">Contact support</a>
|
||||
</EmptyDescription>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
<Empty className="min-h-[80svh]">
|
||||
<EmptyHeader>
|
||||
<EmptyTitle>Nothing to see here</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
No posts have been created yet. Get started by{" "}
|
||||
<a href="#">creating your first post</a> to share with the
|
||||
community.
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
<EmptyContent>
|
||||
<Button variant="outline">
|
||||
<PlusIcon />
|
||||
New Post
|
||||
</Button>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
<div className="bg-muted flex min-h-[800px] items-center justify-center rounded-lg p-20">
|
||||
<Card className="max-w-sm">
|
||||
<CardContent>
|
||||
<Empty className="p-4">
|
||||
<EmptyHeader>
|
||||
<EmptyTitle>404 - Not Found</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
The page you're looking for doesn't exist. Try
|
||||
searching for what you need below.
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
<EmptyContent>
|
||||
<InputGroup className="w-3/4">
|
||||
<InputGroupInput placeholder="Try searching for pages..." />
|
||||
<InputGroupAddon>
|
||||
<SearchIcon />
|
||||
</InputGroupAddon>
|
||||
<InputGroupAddon align="inline-end">
|
||||
<Kbd>/</Kbd>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<EmptyDescription>
|
||||
Need help? <a href="#">Contact support</a>
|
||||
</EmptyDescription>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
<div className="bg-muted flex min-h-[800px] items-center justify-center rounded-lg p-20">
|
||||
<Card className="max-w-sm">
|
||||
<CardContent>
|
||||
<Empty className="p-4">
|
||||
<EmptyHeader>
|
||||
<EmptyMedia variant="icon">
|
||||
<IconFolderCode />
|
||||
</EmptyMedia>
|
||||
<EmptyTitle>No projects yet</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
You haven't created any projects yet. Get started by
|
||||
creating your first project.
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
<EmptyContent>
|
||||
<div className="flex gap-2">
|
||||
<Button asChild>
|
||||
<a href="#">Create project</a>
|
||||
</Button>
|
||||
<Button variant="outline">Import project</Button>
|
||||
</div>
|
||||
<Button
|
||||
variant="link"
|
||||
asChild
|
||||
className="text-muted-foreground"
|
||||
>
|
||||
<a href="#">
|
||||
Learn more <IconArrowUpRight />
|
||||
</a>
|
||||
</Button>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-6">
|
||||
<div className="flex gap-4">
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button>Open Dialog</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader className="sr-only">
|
||||
<DialogTitle>Dialog Title</DialogTitle>
|
||||
<DialogDescription>Dialog Description</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Empty className="p-4">
|
||||
<EmptyHeader>
|
||||
<EmptyMedia variant="icon">
|
||||
<IconFolderCode />
|
||||
</EmptyMedia>
|
||||
<EmptyTitle>No projects yet</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
You haven't created any projects yet. Get started by
|
||||
creating your first project.
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
<EmptyContent>
|
||||
<div className="flex gap-2">
|
||||
<Button asChild>
|
||||
<a href="#">Create project</a>
|
||||
</Button>
|
||||
<Button variant="outline">Import project</Button>
|
||||
</div>
|
||||
<Button
|
||||
variant="link"
|
||||
asChild
|
||||
className="text-muted-foreground"
|
||||
>
|
||||
<a href="#">
|
||||
Learn more <IconArrowUpRight />
|
||||
</a>
|
||||
</Button>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline">Open Popover</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="rounded-2xl p-2">
|
||||
<Empty className="rounded-sm p-6">
|
||||
<EmptyHeader>
|
||||
<EmptyTitle>Nothing to see here</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
No posts have been created yet.{" "}
|
||||
<a href="#">Create your first post</a> to share with the
|
||||
community.
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
<EmptyContent>
|
||||
<Button variant="outline">
|
||||
<PlusIcon />
|
||||
New Post
|
||||
</Button>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
4957
apps/v4/app/(internal)/sink/components/field-demo.tsx
Normal file
4957
apps/v4/app/(internal)/sink/components/field-demo.tsx
Normal file
File diff suppressed because it is too large
Load Diff
663
apps/v4/app/(internal)/sink/components/input-group-demo.tsx
Normal file
663
apps/v4/app/(internal)/sink/components/input-group-demo.tsx
Normal file
@@ -0,0 +1,663 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import {
|
||||
IconBrandJavascript,
|
||||
IconCheck,
|
||||
IconChevronDown,
|
||||
IconCopy,
|
||||
IconInfoCircle,
|
||||
IconLoader2,
|
||||
IconMicrophone,
|
||||
IconPlayerRecordFilled,
|
||||
IconPlus,
|
||||
IconRefresh,
|
||||
IconSearch,
|
||||
IconServerSpark,
|
||||
IconStar,
|
||||
IconTrash,
|
||||
} from "@tabler/icons-react"
|
||||
import {
|
||||
ArrowRightIcon,
|
||||
ArrowUpIcon,
|
||||
ChevronDownIcon,
|
||||
EyeClosedIcon,
|
||||
FlipVerticalIcon,
|
||||
SearchIcon,
|
||||
} from "lucide-react"
|
||||
import { toast } from "sonner"
|
||||
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import {
|
||||
ButtonGroup,
|
||||
ButtonGroupText,
|
||||
} from "@/registry/new-york-v4/ui/button-group"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/registry/new-york-v4/ui/dropdown-menu"
|
||||
import {
|
||||
Field,
|
||||
FieldDescription,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
} from "@/registry/new-york-v4/ui/field"
|
||||
import { Input } from "@/registry/new-york-v4/ui/input"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupInput,
|
||||
InputGroupText,
|
||||
InputGroupTextarea,
|
||||
} from "@/registry/new-york-v4/ui/input-group"
|
||||
import { Kbd, KbdGroup } from "@/registry/new-york-v4/ui/kbd"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/registry/new-york-v4/ui/popover"
|
||||
import { Spinner } from "@/registry/new-york-v4/ui/spinner"
|
||||
import { Textarea } from "@/registry/new-york-v4/ui/textarea"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/registry/new-york-v4/ui/tooltip"
|
||||
|
||||
export function InputGroupDemo() {
|
||||
const [country, setCountry] = useState("+1")
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-wrap gap-12 pb-72 *:[div]:w-full *:[div]:max-w-sm">
|
||||
<div className="flex flex-col gap-10">
|
||||
<Field>
|
||||
<FieldLabel htmlFor="input-default-01">
|
||||
Default (No Input Group)
|
||||
</FieldLabel>
|
||||
<Input placeholder="Default" id="input-default-01" />
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="input-group-02">Input Group</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupInput id="input-group-02" placeholder="Default" />
|
||||
</InputGroup>
|
||||
</Field>
|
||||
<Field data-disabled="true">
|
||||
<FieldLabel htmlFor="input-disabled-03">Disabled</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupInput
|
||||
id="input-disabled-03"
|
||||
placeholder="This field is disabled"
|
||||
disabled
|
||||
/>
|
||||
</InputGroup>
|
||||
</Field>
|
||||
<Field data-invalid="true">
|
||||
<FieldLabel htmlFor="input-invalid-04">Invalid</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupInput
|
||||
id="input-invalid-04"
|
||||
placeholder="This field is invalid"
|
||||
aria-invalid="true"
|
||||
/>
|
||||
</InputGroup>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="input-icon-left-05">Icon (left)</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupInput id="input-icon-left-05" />
|
||||
<InputGroupAddon>
|
||||
<SearchIcon className="text-muted-foreground" />
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<InputGroup>
|
||||
<InputGroupInput id="input-icon-left-06" />
|
||||
<InputGroupAddon>
|
||||
<FlipVerticalIcon className="text-muted-foreground" />
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="input-icon-right-07">Icon (right)</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupInput id="input-icon-right-07" />
|
||||
<InputGroupAddon align="inline-end">
|
||||
<EyeClosedIcon />
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<InputGroup>
|
||||
<InputGroupInput id="input-icon-right-08" />
|
||||
<InputGroupAddon align="inline-end">
|
||||
<IconLoader2 className="text-muted-foreground animate-spin" />
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="input-icon-both-09">Icon (both)</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupInput id="input-icon-both-09" />
|
||||
<InputGroupAddon>
|
||||
<IconMicrophone className="text-muted-foreground" />
|
||||
</InputGroupAddon>
|
||||
<InputGroupAddon align="inline-end">
|
||||
<IconPlayerRecordFilled className="animate-pulse text-red-500" />
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="input-icon-both-10">Multiple Icons</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupInput id="input-icon-both-10" />
|
||||
<InputGroupAddon align="inline-end">
|
||||
<IconStar />
|
||||
<InputGroupButton
|
||||
size="icon-xs"
|
||||
onClick={() => toast.success("Copied to clipboard")}
|
||||
>
|
||||
<IconCopy />
|
||||
</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
<InputGroupAddon>
|
||||
<IconPlayerRecordFilled className="animate-pulse text-red-500" />
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="input-description-10">Description</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupInput id="input-description-10" />
|
||||
<InputGroupAddon align="inline-end">
|
||||
<IconInfoCircle />
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<FieldDescription>
|
||||
This is a description of the input group.
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
<FieldGroup className="grid grid-cols-2 gap-4">
|
||||
<Field>
|
||||
<FieldLabel htmlFor="input-group-11">First Name</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupInput id="input-group-11" placeholder="First Name" />
|
||||
<InputGroupAddon align="inline-end">
|
||||
<IconInfoCircle />
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="input-group-12">Last Name</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupInput id="input-group-12" placeholder="Last Name" />
|
||||
<InputGroupAddon align="inline-end">
|
||||
<IconInfoCircle />
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</div>
|
||||
<div className="flex flex-col gap-10">
|
||||
<Field>
|
||||
<FieldLabel htmlFor="input-tooltip-20">Tooltip</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupInput id="input-tooltip-20" />
|
||||
<InputGroupAddon align="inline-end">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<InputGroupButton className="rounded-full" size="icon-xs">
|
||||
<IconInfoCircle />
|
||||
</InputGroupButton>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>This is content in a tooltip.</TooltipContent>
|
||||
</Tooltip>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="input-dropdown-21">Dropdown</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupInput id="input-dropdown-21" />
|
||||
<InputGroupAddon>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<InputGroupButton className="text-muted-foreground tabular-nums">
|
||||
{country} <ChevronDownIcon />
|
||||
</InputGroupButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="start"
|
||||
className="min-w-16"
|
||||
sideOffset={10}
|
||||
alignOffset={-8}
|
||||
>
|
||||
<DropdownMenuItem onClick={() => setCountry("+1")}>
|
||||
+1
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setCountry("+44")}>
|
||||
+44
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setCountry("+46")}>
|
||||
+46
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="input-label-10">Label</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupAddon>
|
||||
<FieldLabel htmlFor="input-label-10">Label</FieldLabel>
|
||||
</InputGroupAddon>
|
||||
<InputGroupInput id="input-label-10" />
|
||||
</InputGroup>
|
||||
<InputGroup className="gap-0">
|
||||
<InputGroupAddon>
|
||||
<FieldLabel
|
||||
htmlFor="input-prefix-11"
|
||||
className="text-muted-foreground"
|
||||
>
|
||||
example.com/
|
||||
</FieldLabel>
|
||||
</InputGroupAddon>
|
||||
<InputGroupInput id="input-prefix-11" />
|
||||
</InputGroup>
|
||||
<InputGroup>
|
||||
<InputGroupInput id="input-optional-12" />
|
||||
<InputGroupAddon align="inline-end">
|
||||
<InputGroupText>(optional)</InputGroupText>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="input-button-13">Button</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupInput id="input-button-13" />
|
||||
<InputGroupAddon>
|
||||
<InputGroupButton>Button</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<InputGroup>
|
||||
<InputGroupInput id="input-button-14" />
|
||||
<InputGroupAddon>
|
||||
<InputGroupButton variant="outline">Button</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<InputGroup>
|
||||
<InputGroupInput id="input-button-15" />
|
||||
<InputGroupAddon>
|
||||
<InputGroupButton variant="secondary">Button</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<InputGroup>
|
||||
<InputGroupInput id="input-button-16" />
|
||||
<InputGroupAddon align="inline-end">
|
||||
<InputGroupButton variant="secondary">Button</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<InputGroup>
|
||||
<InputGroupInput id="input-button-17" />
|
||||
<InputGroupAddon align="inline-end">
|
||||
<InputGroupButton size="icon-xs">
|
||||
<IconCopy />
|
||||
</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<InputGroup>
|
||||
<InputGroupInput id="input-button-18" />
|
||||
<InputGroupAddon align="inline-end">
|
||||
<InputGroupButton variant="secondary" size="icon-xs">
|
||||
<IconTrash />
|
||||
</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<InputGroup className="[--radius:9999px]">
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<InputGroupAddon>
|
||||
<InputGroupButton variant="secondary" size="icon-xs">
|
||||
<IconInfoCircle />
|
||||
</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
align="start"
|
||||
className="flex flex-col gap-1 rounded-xl text-sm"
|
||||
>
|
||||
<p className="font-medium">Your connection is not secure.</p>
|
||||
<p>
|
||||
You should not enter any sensitive information on this site.
|
||||
</p>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<InputGroupAddon className="text-muted-foreground">
|
||||
https://
|
||||
</InputGroupAddon>
|
||||
<InputGroupInput id="input-secure-19" />
|
||||
<InputGroupAddon align="inline-end">
|
||||
<InputGroupButton
|
||||
size="icon-xs"
|
||||
onClick={() => toast.success("Added to favorites")}
|
||||
>
|
||||
<IconStar />
|
||||
</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="input-addon-20">Addon (block-start)</FieldLabel>
|
||||
<InputGroup className="h-auto">
|
||||
<InputGroupInput id="input-addon-20" />
|
||||
<InputGroupAddon align="block-start">
|
||||
<InputGroupText>First Name</InputGroupText>
|
||||
<IconInfoCircle className="text-muted-foreground ml-auto" />
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="input-addon-21">Addon (block-end)</FieldLabel>
|
||||
<InputGroup className="h-auto">
|
||||
<InputGroupInput id="input-addon-21" />
|
||||
<InputGroupAddon align="block-end">
|
||||
<InputGroupText>20/240 characters</InputGroupText>
|
||||
<IconInfoCircle className="text-muted-foreground ml-auto" />
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="textarea-comment-33">Default Button</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupTextarea
|
||||
id="textarea-comment-33"
|
||||
placeholder="Share your thoughts..."
|
||||
className="py-2.5"
|
||||
/>
|
||||
<InputGroupAddon align="block-end">
|
||||
<ButtonGroup>
|
||||
<Button variant="outline" size="sm">
|
||||
Button
|
||||
</Button>
|
||||
<Button variant="outline" size="icon" className="size-8">
|
||||
<IconChevronDown />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<Button variant="ghost" className="ml-auto" size="sm">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="default" size="sm">
|
||||
Post <ArrowRightIcon />
|
||||
</Button>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</Field>
|
||||
</div>
|
||||
<div className="flex flex-col gap-10">
|
||||
<Field>
|
||||
<FieldLabel htmlFor="input-kbd-22">Input Group with Kbd</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupInput id="input-kbd-22" />
|
||||
<InputGroupAddon>
|
||||
<Kbd>⌘K</Kbd>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<InputGroup>
|
||||
<InputGroupInput id="input-kbd-23" />
|
||||
<InputGroupAddon align="inline-end">
|
||||
<Kbd>⌘K</Kbd>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<InputGroup>
|
||||
<InputGroupInput
|
||||
id="input-search-apps-24"
|
||||
placeholder="Search for Apps..."
|
||||
/>
|
||||
<InputGroupAddon align="inline-end">Ask AI</InputGroupAddon>
|
||||
<InputGroupAddon align="inline-end">
|
||||
<Kbd>Tab</Kbd>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<InputGroup>
|
||||
<InputGroupInput
|
||||
id="input-search-type-25"
|
||||
placeholder="Type to search..."
|
||||
/>
|
||||
<InputGroupAddon align="inline-start">
|
||||
<IconServerSpark />
|
||||
</InputGroupAddon>
|
||||
<InputGroupAddon align="inline-end">
|
||||
<KbdGroup>
|
||||
<Kbd>Ctrl</Kbd>
|
||||
<Kbd>C</Kbd>
|
||||
</KbdGroup>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="input-username-26">Username</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupInput id="input-username-26" defaultValue="shadcn" />
|
||||
<InputGroupAddon align="inline-end">
|
||||
<div className="flex size-4 items-center justify-center rounded-full bg-green-500 dark:bg-green-800">
|
||||
<IconCheck className="size-3 text-white" />
|
||||
</div>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<FieldDescription className="text-green-700">
|
||||
This username is available.
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
<InputGroup>
|
||||
<InputGroupInput
|
||||
id="input-search-docs-27"
|
||||
placeholder="Search documentation..."
|
||||
/>
|
||||
<InputGroupAddon>
|
||||
<IconSearch />
|
||||
</InputGroupAddon>
|
||||
<InputGroupAddon align="inline-end">12 results</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<InputGroup data-disabled="true">
|
||||
<InputGroupInput
|
||||
id="input-search-disabled-28"
|
||||
placeholder="Search documentation..."
|
||||
disabled
|
||||
/>
|
||||
<InputGroupAddon>
|
||||
<IconSearch />
|
||||
</InputGroupAddon>
|
||||
<InputGroupAddon align="inline-end">Disabled</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="url">With Button Group</FieldLabel>
|
||||
<ButtonGroup>
|
||||
<ButtonGroupText>https://</ButtonGroupText>
|
||||
<InputGroup>
|
||||
<InputGroupInput id="url" />
|
||||
<InputGroupAddon align="inline-end">
|
||||
<IconInfoCircle />
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<ButtonGroupText>.com</ButtonGroupText>
|
||||
</ButtonGroup>
|
||||
<FieldDescription>
|
||||
This is a description of the input group.
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
<Field data-disabled="true">
|
||||
<FieldLabel htmlFor="input-group-29">Loading</FieldLabel>
|
||||
<FieldDescription>
|
||||
This is a description of the input group.
|
||||
</FieldDescription>
|
||||
<InputGroup>
|
||||
<InputGroupInput
|
||||
id="input-group-29"
|
||||
disabled
|
||||
defaultValue="shadcn"
|
||||
/>
|
||||
<InputGroupAddon align="inline-end">
|
||||
<Spinner />
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
<FieldLabel htmlFor="textarea-code-32">Code Editor</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupTextarea
|
||||
id="textarea-code-32"
|
||||
placeholder="console.log('Hello, world!');"
|
||||
className="min-h-[300px] py-3"
|
||||
/>
|
||||
<InputGroupAddon align="block-start" className="border-b">
|
||||
<InputGroupText className="font-mono font-medium">
|
||||
<IconBrandJavascript />
|
||||
script.js
|
||||
</InputGroupText>
|
||||
<InputGroupButton size="icon-xs" className="ml-auto">
|
||||
<IconRefresh />
|
||||
</InputGroupButton>
|
||||
<InputGroupButton size="icon-xs" variant="ghost">
|
||||
<IconCopy />
|
||||
</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
<InputGroupAddon align="block-end" className="border-t">
|
||||
<InputGroupText>Line 1, Column 1</InputGroupText>
|
||||
<InputGroupText className="ml-auto">JavaScript</InputGroupText>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</Field>
|
||||
</div>
|
||||
<div className="flex flex-col gap-10">
|
||||
<Field>
|
||||
<FieldLabel htmlFor="textarea-header-footer-12">Default</FieldLabel>
|
||||
<Textarea
|
||||
id="textarea-header-footer-12"
|
||||
placeholder="Enter your text here..."
|
||||
/>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="textarea-header-footer-13">
|
||||
Input Group
|
||||
</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupTextarea
|
||||
id="textarea-header-footer-13"
|
||||
placeholder="Enter your text here..."
|
||||
/>
|
||||
</InputGroup>
|
||||
<FieldDescription>
|
||||
This is a description of the input group.
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
<Field data-invalid="true">
|
||||
<FieldLabel htmlFor="textarea-header-footer-14">Invalid</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupTextarea
|
||||
id="textarea-header-footer-14"
|
||||
placeholder="Enter your text here..."
|
||||
aria-invalid="true"
|
||||
/>
|
||||
</InputGroup>
|
||||
<FieldDescription>
|
||||
This is a description of the input group.
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
<Field data-disabled="true">
|
||||
<FieldLabel htmlFor="textarea-header-footer-15">Disabled</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupTextarea
|
||||
id="textarea-header-footer-15"
|
||||
placeholder="Enter your text here..."
|
||||
disabled
|
||||
/>
|
||||
</InputGroup>
|
||||
<FieldDescription>
|
||||
This is a description of the input group.
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="textarea-header-footer-30">Textarea</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupTextarea
|
||||
id="textarea-header-footer-30"
|
||||
placeholder="Enter your text here..."
|
||||
/>
|
||||
<InputGroupAddon align="block-end">
|
||||
<InputGroupText>0/280 characters</InputGroupText>
|
||||
<InputGroupButton
|
||||
variant="default"
|
||||
size="icon-xs"
|
||||
className="ml-auto rounded-full"
|
||||
>
|
||||
<ArrowUpIcon />
|
||||
<span className="sr-only">Send</span>
|
||||
</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="prompt-31">Enter your prompt</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupTextarea
|
||||
id="prompt-31"
|
||||
placeholder="Ask, Search or Chat..."
|
||||
/>
|
||||
<InputGroupAddon align="block-end">
|
||||
<InputGroupButton
|
||||
variant="outline"
|
||||
className="rounded-full"
|
||||
size="icon-xs"
|
||||
>
|
||||
<IconPlus />
|
||||
</InputGroupButton>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<InputGroupButton variant="ghost">Auto</InputGroupButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent side="top" align="start">
|
||||
<DropdownMenuItem>Auto</DropdownMenuItem>
|
||||
<DropdownMenuItem>Agent</DropdownMenuItem>
|
||||
<DropdownMenuItem>Manual</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<InputGroupText className="ml-auto">
|
||||
12 messages left
|
||||
</InputGroupText>
|
||||
<InputGroupButton
|
||||
variant="default"
|
||||
className="rounded-full"
|
||||
size="icon-xs"
|
||||
>
|
||||
<ArrowUpIcon />
|
||||
<span className="sr-only">Send</span>
|
||||
</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<FieldDescription>
|
||||
This is a description of the input group.
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="textarea-comment-31">Comment Box</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupTextarea
|
||||
id="textarea-comment-31"
|
||||
placeholder="Share your thoughts..."
|
||||
/>
|
||||
<InputGroupAddon align="block-end">
|
||||
<InputGroupButton variant="ghost" className="ml-auto" size="sm">
|
||||
Cancel
|
||||
</InputGroupButton>
|
||||
<InputGroupButton variant="default" size="sm">
|
||||
Post Comment
|
||||
</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</Field>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
392
apps/v4/app/(internal)/sink/components/item-demo.tsx
Normal file
392
apps/v4/app/(internal)/sink/components/item-demo.tsx
Normal file
@@ -0,0 +1,392 @@
|
||||
import * as React from "react"
|
||||
import Image from "next/image"
|
||||
import { IconChevronRight, IconDownload } from "@tabler/icons-react"
|
||||
import { PlusIcon, TicketIcon } from "lucide-react"
|
||||
|
||||
import {
|
||||
Avatar,
|
||||
AvatarFallback,
|
||||
AvatarImage,
|
||||
} from "@/registry/new-york-v4/ui/avatar"
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import {
|
||||
Field,
|
||||
FieldContent,
|
||||
FieldDescription,
|
||||
FieldLabel,
|
||||
FieldTitle,
|
||||
} from "@/registry/new-york-v4/ui/field"
|
||||
import {
|
||||
Item,
|
||||
ItemActions,
|
||||
ItemContent,
|
||||
ItemDescription,
|
||||
ItemFooter,
|
||||
ItemGroup,
|
||||
ItemHeader,
|
||||
ItemMedia,
|
||||
ItemSeparator,
|
||||
ItemTitle,
|
||||
} from "@/registry/new-york-v4/ui/item"
|
||||
import { Progress } from "@/registry/new-york-v4/ui/progress"
|
||||
import { Spinner } from "@/registry/new-york-v4/ui/spinner"
|
||||
|
||||
const people = [
|
||||
{
|
||||
username: "shadcn",
|
||||
avatar: "https://github.com/shadcn.png",
|
||||
message: "Just shipped a component that fixes itself",
|
||||
},
|
||||
{
|
||||
username: "pranathip",
|
||||
avatar: "https://github.com/pranathip.png",
|
||||
message: "My code is so clean, it does its own laundry",
|
||||
},
|
||||
{
|
||||
username: "evilrabbit",
|
||||
avatar: "https://github.com/evilrabbit.png",
|
||||
message:
|
||||
"Debugging is like being a detective in a crime movie where you're also the murderer",
|
||||
},
|
||||
{
|
||||
username: "maxleiter",
|
||||
avatar: "https://github.com/maxleiter.png",
|
||||
message:
|
||||
"I don't always test my code, but when I do, I test it in production",
|
||||
},
|
||||
]
|
||||
|
||||
const music = [
|
||||
{
|
||||
title: "Midnight City Lights",
|
||||
artist: "Neon Dreams",
|
||||
album: "Electric Nights",
|
||||
duration: "3:45",
|
||||
},
|
||||
{
|
||||
title: "Coffee Shop Conversations",
|
||||
artist: "The Morning Brew",
|
||||
album: "Urban Stories",
|
||||
duration: "4:05",
|
||||
},
|
||||
{
|
||||
title: "Digital Rain",
|
||||
artist: "Cyber Symphony",
|
||||
album: "Binary Beats",
|
||||
duration: "3:30",
|
||||
},
|
||||
{
|
||||
title: "Sunset Boulevard",
|
||||
artist: "Golden Hour",
|
||||
album: "California Dreams",
|
||||
duration: "3:55",
|
||||
},
|
||||
{
|
||||
title: "Neon Sign Romance",
|
||||
artist: "Retro Wave",
|
||||
album: "80s Forever",
|
||||
duration: "4:10",
|
||||
},
|
||||
{
|
||||
title: "Ocean Depths",
|
||||
artist: "Deep Blue",
|
||||
album: "Underwater Symphony",
|
||||
duration: "3:40",
|
||||
},
|
||||
{
|
||||
title: "Space Station Alpha",
|
||||
artist: "Cosmic Explorers",
|
||||
album: "Galactic Journey",
|
||||
duration: "3:50",
|
||||
},
|
||||
{
|
||||
title: "Forest Whispers",
|
||||
artist: "Nature's Choir",
|
||||
album: "Woodland Tales",
|
||||
duration: "3:35",
|
||||
},
|
||||
]
|
||||
|
||||
const issues = [
|
||||
{
|
||||
number: 1247,
|
||||
date: "March 15, 2024",
|
||||
title:
|
||||
"Button component doesn't respect disabled state when using custom variants",
|
||||
description:
|
||||
"When applying custom variants to the Button component, the disabled prop is ignored and the button remains clickable. This affects accessibility and user experience.",
|
||||
},
|
||||
{
|
||||
number: 892,
|
||||
date: "February 8, 2024",
|
||||
title: "Dialog component causes scroll lock on mobile devices",
|
||||
description:
|
||||
"The Dialog component prevents scrolling on the background content but doesn't restore scroll position properly on mobile Safari and Chrome, causing layout shifts.",
|
||||
},
|
||||
{
|
||||
number: 1156,
|
||||
date: "January 22, 2024",
|
||||
title: "TypeScript errors with Select component in strict mode",
|
||||
description:
|
||||
"Using the Select component with TypeScript strict mode enabled throws type errors related to generic constraints and value prop typing.",
|
||||
},
|
||||
{
|
||||
number: 734,
|
||||
date: "December 3, 2023",
|
||||
title: "Dark mode toggle causes flash of unstyled content",
|
||||
description:
|
||||
"When switching between light and dark themes, there's a brief moment where components render with incorrect styling before the theme transition completes.",
|
||||
},
|
||||
{
|
||||
number: 1389,
|
||||
date: "April 2, 2024",
|
||||
title: "Form validation messages overlap with floating labels",
|
||||
description:
|
||||
"Error messages in Form components with floating labels appear underneath the label text, making them difficult to read. Need better positioning logic for validation feedback.",
|
||||
},
|
||||
]
|
||||
|
||||
export function ItemDemo() {
|
||||
return (
|
||||
<div className="@container w-full">
|
||||
<div className="flex flex-wrap gap-6 2xl:gap-12">
|
||||
<div className="flex max-w-sm flex-col gap-6">
|
||||
<Item>
|
||||
<ItemContent>
|
||||
<ItemTitle>Item Title</ItemTitle>
|
||||
</ItemContent>
|
||||
<ItemActions>
|
||||
<Button variant="outline">Button</Button>
|
||||
</ItemActions>
|
||||
</Item>
|
||||
<Item variant="outline">
|
||||
<ItemContent>
|
||||
<ItemTitle>Item Title</ItemTitle>
|
||||
</ItemContent>
|
||||
<ItemActions>
|
||||
<Button variant="outline">Button</Button>
|
||||
</ItemActions>
|
||||
</Item>
|
||||
<Item>
|
||||
<ItemContent>
|
||||
<ItemTitle>Item Title</ItemTitle>
|
||||
<ItemDescription>Item Description</ItemDescription>
|
||||
</ItemContent>
|
||||
<ItemActions>
|
||||
<Button variant="outline">Button</Button>
|
||||
</ItemActions>
|
||||
</Item>
|
||||
<Item variant="outline">
|
||||
<ItemContent>
|
||||
<ItemTitle>Item Title</ItemTitle>
|
||||
<ItemDescription>Item Description</ItemDescription>
|
||||
</ItemContent>
|
||||
</Item>
|
||||
<Item variant="muted">
|
||||
<ItemContent>
|
||||
<ItemTitle>Item Title</ItemTitle>
|
||||
<ItemDescription>Item Description</ItemDescription>
|
||||
</ItemContent>
|
||||
</Item>
|
||||
<Item variant="muted">
|
||||
<ItemContent>
|
||||
<ItemTitle>Item Title</ItemTitle>
|
||||
<ItemDescription>Item Description</ItemDescription>
|
||||
</ItemContent>
|
||||
<ItemActions>
|
||||
<Button variant="outline">Button</Button>
|
||||
<Button variant="outline">Button</Button>
|
||||
</ItemActions>
|
||||
</Item>
|
||||
<Item variant="outline">
|
||||
<ItemMedia variant="icon">
|
||||
<TicketIcon />
|
||||
</ItemMedia>
|
||||
<ItemContent>
|
||||
<ItemTitle>Item Title</ItemTitle>
|
||||
</ItemContent>
|
||||
<ItemActions>
|
||||
<Button size="sm">Purchase</Button>
|
||||
</ItemActions>
|
||||
</Item>
|
||||
<Item variant="muted">
|
||||
<ItemMedia variant="icon">
|
||||
<TicketIcon />
|
||||
</ItemMedia>
|
||||
<ItemContent>
|
||||
<ItemTitle>Item Title</ItemTitle>
|
||||
<ItemDescription>Item Description</ItemDescription>
|
||||
</ItemContent>
|
||||
<ItemActions>
|
||||
<Button size="sm">Upgrade</Button>
|
||||
</ItemActions>
|
||||
</Item>
|
||||
<FieldLabel>
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldTitle>Field Title</FieldTitle>
|
||||
<FieldDescription>Field Description</FieldDescription>
|
||||
</FieldContent>
|
||||
<Button variant="outline">Button</Button>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
</div>
|
||||
<div className="flex max-w-sm flex-col gap-6">
|
||||
<ItemGroup>
|
||||
{people.map((person, index) => (
|
||||
<React.Fragment key={person.username}>
|
||||
<Item>
|
||||
<ItemMedia>
|
||||
<Avatar>
|
||||
<AvatarImage src={person.avatar} />
|
||||
<AvatarFallback>
|
||||
{person.username.charAt(0)}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
</ItemMedia>
|
||||
<ItemContent>
|
||||
<ItemTitle>{person.username}</ItemTitle>
|
||||
<ItemDescription>{person.message}</ItemDescription>
|
||||
</ItemContent>
|
||||
<ItemActions>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="size-8 rounded-full"
|
||||
>
|
||||
<PlusIcon />
|
||||
</Button>
|
||||
</ItemActions>
|
||||
</Item>
|
||||
{index !== people.length - 1 && <ItemSeparator />}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</ItemGroup>
|
||||
<Item variant="outline">
|
||||
<ItemMedia>
|
||||
<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/maxleiter.png"
|
||||
alt="@maxleiter"
|
||||
/>
|
||||
<AvatarFallback>LR</AvatarFallback>
|
||||
</Avatar>
|
||||
<Avatar>
|
||||
<AvatarImage
|
||||
src="https://github.com/evilrabbit.png"
|
||||
alt="@evilrabbit"
|
||||
/>
|
||||
<AvatarFallback>ER</AvatarFallback>
|
||||
</Avatar>
|
||||
</div>
|
||||
</ItemMedia>
|
||||
<ItemContent>
|
||||
<ItemTitle>Design Department</ItemTitle>
|
||||
<ItemDescription>
|
||||
Meet our team of designers, engineers, and researchers.
|
||||
</ItemDescription>
|
||||
</ItemContent>
|
||||
<ItemActions className="self-start">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="size-8 rounded-full"
|
||||
>
|
||||
<IconChevronRight />
|
||||
</Button>
|
||||
</ItemActions>
|
||||
</Item>
|
||||
<Item variant="outline">
|
||||
<ItemHeader>Your download has started.</ItemHeader>
|
||||
<ItemMedia variant="icon">
|
||||
<Spinner />
|
||||
</ItemMedia>
|
||||
<ItemContent>
|
||||
<ItemTitle>Downloading...</ItemTitle>
|
||||
<ItemDescription>129 MB / 1000 MB</ItemDescription>
|
||||
</ItemContent>
|
||||
<ItemActions>
|
||||
<Button variant="outline" size="sm">
|
||||
Cancel
|
||||
</Button>
|
||||
</ItemActions>
|
||||
<ItemFooter>
|
||||
<Progress value={50} />
|
||||
</ItemFooter>
|
||||
</Item>
|
||||
</div>
|
||||
<div className="flex max-w-lg flex-col gap-6">
|
||||
<ItemGroup className="gap-4">
|
||||
{music.map((song) => (
|
||||
<Item key={song.title} variant="outline" asChild role="listitem">
|
||||
<a href="#">
|
||||
<ItemMedia variant="image">
|
||||
<Image
|
||||
src={`https://avatar.vercel.sh/${song.title}`}
|
||||
alt={song.title}
|
||||
width={32}
|
||||
height={32}
|
||||
className="grayscale"
|
||||
/>
|
||||
</ItemMedia>
|
||||
<ItemContent>
|
||||
<ItemTitle className="line-clamp-1">
|
||||
{song.title} -{" "}
|
||||
<span className="text-muted-foreground">
|
||||
{song.album}
|
||||
</span>
|
||||
</ItemTitle>
|
||||
<ItemDescription>{song.artist}</ItemDescription>
|
||||
</ItemContent>
|
||||
<ItemContent className="flex-none text-center">
|
||||
<ItemDescription>{song.duration}</ItemDescription>
|
||||
</ItemContent>
|
||||
<ItemActions>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-8 rounded-full"
|
||||
aria-label="Download"
|
||||
>
|
||||
<IconDownload />
|
||||
</Button>
|
||||
</ItemActions>
|
||||
</a>
|
||||
</Item>
|
||||
))}
|
||||
</ItemGroup>
|
||||
</div>
|
||||
<div className="flex max-w-lg flex-col gap-6">
|
||||
<ItemGroup>
|
||||
{issues.map((issue) => (
|
||||
<React.Fragment key={issue.number}>
|
||||
<Item asChild className="rounded-none">
|
||||
<a href="#">
|
||||
<ItemContent>
|
||||
<ItemTitle className="line-clamp-1">
|
||||
{issue.title}
|
||||
</ItemTitle>
|
||||
<ItemDescription>{issue.description}</ItemDescription>
|
||||
</ItemContent>
|
||||
<ItemContent className="self-start">
|
||||
#{issue.number}
|
||||
</ItemContent>
|
||||
</a>
|
||||
</Item>
|
||||
<ItemSeparator />
|
||||
</React.Fragment>
|
||||
))}
|
||||
</ItemGroup>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
103
apps/v4/app/(internal)/sink/components/kbd-demo.tsx
Normal file
103
apps/v4/app/(internal)/sink/components/kbd-demo.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
import { IconArrowLeft, IconArrowRight } from "@tabler/icons-react"
|
||||
import { CommandIcon, WavesIcon } from "lucide-react"
|
||||
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import { ButtonGroup } from "@/registry/new-york-v4/ui/button-group"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupInput,
|
||||
} from "@/registry/new-york-v4/ui/input-group"
|
||||
import { Kbd, KbdGroup } from "@/registry/new-york-v4/ui/kbd"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/registry/new-york-v4/ui/tooltip"
|
||||
|
||||
export function KbdDemo() {
|
||||
return (
|
||||
<div className="flex max-w-xs flex-col items-start gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Kbd>Ctrl</Kbd>
|
||||
<Kbd>⌘K</Kbd>
|
||||
<Kbd>Ctrl + B</Kbd>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Kbd>⌘</Kbd>
|
||||
<Kbd>C</Kbd>
|
||||
</div>
|
||||
<KbdGroup>
|
||||
<Kbd>Ctrl</Kbd>
|
||||
<Kbd>Shift</Kbd>
|
||||
<Kbd>P</Kbd>
|
||||
</KbdGroup>
|
||||
<div className="flex items-center gap-2">
|
||||
<Kbd>↑</Kbd>
|
||||
<Kbd>↓</Kbd>
|
||||
<Kbd>←</Kbd>
|
||||
<Kbd>→</Kbd>
|
||||
</div>
|
||||
<KbdGroup>
|
||||
<Kbd>
|
||||
<CommandIcon />
|
||||
</Kbd>
|
||||
<Kbd>
|
||||
<IconArrowLeft />
|
||||
</Kbd>
|
||||
<Kbd>
|
||||
<IconArrowRight />
|
||||
</Kbd>
|
||||
</KbdGroup>
|
||||
<KbdGroup>
|
||||
<Kbd>
|
||||
<IconArrowLeft />
|
||||
Left
|
||||
</Kbd>
|
||||
<Kbd>
|
||||
<WavesIcon />
|
||||
Voice Enabled
|
||||
</Kbd>
|
||||
</KbdGroup>
|
||||
<InputGroup>
|
||||
<InputGroupInput />
|
||||
<InputGroupAddon>
|
||||
<Kbd>Space</Kbd>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<ButtonGroup>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button size="sm" variant="outline">
|
||||
Save
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<div className="flex items-center gap-2">
|
||||
Save Changes <Kbd>S</Kbd>
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button size="sm" variant="outline">
|
||||
Print
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<div className="flex items-center gap-2">
|
||||
Print Document{" "}
|
||||
<KbdGroup>
|
||||
<Kbd>Ctrl</Kbd>
|
||||
<Kbd>P</Kbd>
|
||||
</KbdGroup>
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</ButtonGroup>
|
||||
<Kbd>
|
||||
<samp>File</samp>
|
||||
</Kbd>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
135
apps/v4/app/(internal)/sink/components/native-select-demo.tsx
Normal file
135
apps/v4/app/(internal)/sink/components/native-select-demo.tsx
Normal file
@@ -0,0 +1,135 @@
|
||||
import {
|
||||
NativeSelect,
|
||||
NativeSelectOptGroup,
|
||||
NativeSelectOption,
|
||||
} from "@/registry/new-york-v4/ui/native-select"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/registry/new-york-v4/ui/select"
|
||||
|
||||
export function NativeSelectDemo() {
|
||||
return (
|
||||
<div className="flex flex-col gap-8">
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="text-muted-foreground text-sm font-medium">
|
||||
Basic Select
|
||||
</div>
|
||||
<div className="flex flex-col gap-4">
|
||||
<NativeSelect>
|
||||
<NativeSelectOption value="">Select a fruit</NativeSelectOption>
|
||||
<NativeSelectOption value="apple">Apple</NativeSelectOption>
|
||||
<NativeSelectOption value="banana">Banana</NativeSelectOption>
|
||||
<NativeSelectOption value="blueberry">Blueberry</NativeSelectOption>
|
||||
<NativeSelectOption value="grapes" disabled>
|
||||
Grapes
|
||||
</NativeSelectOption>
|
||||
<NativeSelectOption value="pineapple">Pineapple</NativeSelectOption>
|
||||
</NativeSelect>
|
||||
<Select>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a fruit" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="apple">Apple</SelectItem>
|
||||
<SelectItem value="banana">Banana</SelectItem>
|
||||
<SelectItem value="blueberry">Blueberry</SelectItem>
|
||||
<SelectItem value="grapes" disabled>
|
||||
Grapes
|
||||
</SelectItem>
|
||||
<SelectItem value="pineapple">Pineapple</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="text-muted-foreground text-sm font-medium">
|
||||
With Groups
|
||||
</div>
|
||||
<div className="flex flex-col gap-4">
|
||||
<NativeSelect>
|
||||
<NativeSelectOption value="">Select a food</NativeSelectOption>
|
||||
<NativeSelectOptGroup label="Fruits">
|
||||
<NativeSelectOption value="apple">Apple</NativeSelectOption>
|
||||
<NativeSelectOption value="banana">Banana</NativeSelectOption>
|
||||
<NativeSelectOption value="blueberry">
|
||||
Blueberry
|
||||
</NativeSelectOption>
|
||||
</NativeSelectOptGroup>
|
||||
<NativeSelectOptGroup label="Vegetables">
|
||||
<NativeSelectOption value="carrot">Carrot</NativeSelectOption>
|
||||
<NativeSelectOption value="broccoli">Broccoli</NativeSelectOption>
|
||||
<NativeSelectOption value="spinach">Spinach</NativeSelectOption>
|
||||
</NativeSelectOptGroup>
|
||||
</NativeSelect>
|
||||
<Select>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a food" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectLabel>Fruits</SelectLabel>
|
||||
<SelectItem value="apple">Apple</SelectItem>
|
||||
<SelectItem value="banana">Banana</SelectItem>
|
||||
<SelectItem value="blueberry">Blueberry</SelectItem>
|
||||
</SelectGroup>
|
||||
<SelectGroup>
|
||||
<SelectLabel>Vegetables</SelectLabel>
|
||||
<SelectItem value="carrot">Carrot</SelectItem>
|
||||
<SelectItem value="broccoli">Broccoli</SelectItem>
|
||||
<SelectItem value="spinach">Spinach</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="text-muted-foreground text-sm font-medium">
|
||||
Disabled State
|
||||
</div>
|
||||
<div className="flex flex-col gap-4">
|
||||
<NativeSelect disabled>
|
||||
<NativeSelectOption value="">Disabled</NativeSelectOption>
|
||||
<NativeSelectOption value="apple">Apple</NativeSelectOption>
|
||||
<NativeSelectOption value="banana">Banana</NativeSelectOption>
|
||||
</NativeSelect>
|
||||
<Select disabled>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Disabled" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="apple">Apple</SelectItem>
|
||||
<SelectItem value="banana">Banana</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="text-muted-foreground text-sm font-medium">
|
||||
Error State
|
||||
</div>
|
||||
<div className="flex flex-col gap-4">
|
||||
<NativeSelect aria-invalid="true">
|
||||
<NativeSelectOption value="">Error state</NativeSelectOption>
|
||||
<NativeSelectOption value="apple">Apple</NativeSelectOption>
|
||||
<NativeSelectOption value="banana">Banana</NativeSelectOption>
|
||||
</NativeSelect>
|
||||
<Select>
|
||||
<SelectTrigger aria-invalid="true">
|
||||
<SelectValue placeholder="Error state" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="apple">Apple</SelectItem>
|
||||
<SelectItem value="banana">Banana</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
106
apps/v4/app/(internal)/sink/components/spinner-demo.tsx
Normal file
106
apps/v4/app/(internal)/sink/components/spinner-demo.tsx
Normal file
@@ -0,0 +1,106 @@
|
||||
import { ArrowRightIcon } from "lucide-react"
|
||||
|
||||
import { Badge } from "@/registry/new-york-v4/ui/badge"
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import {
|
||||
Empty,
|
||||
EmptyContent,
|
||||
EmptyDescription,
|
||||
EmptyHeader,
|
||||
EmptyMedia,
|
||||
EmptyTitle,
|
||||
} from "@/registry/new-york-v4/ui/empty"
|
||||
import { Field, FieldLabel } from "@/registry/new-york-v4/ui/field"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupInput,
|
||||
} from "@/registry/new-york-v4/ui/input-group"
|
||||
import { Spinner } from "@/registry/new-york-v4/ui/spinner"
|
||||
|
||||
export function SpinnerDemo() {
|
||||
return (
|
||||
<div className="flex w-full flex-col gap-12">
|
||||
<div className="flex items-center gap-6">
|
||||
<Spinner />
|
||||
<Spinner className="size-8" />
|
||||
</div>
|
||||
<div className="flex items-center gap-6">
|
||||
<Button>
|
||||
<Spinner /> Submit
|
||||
</Button>
|
||||
<Button disabled>
|
||||
<Spinner /> Disabled
|
||||
</Button>
|
||||
<Button size="sm">
|
||||
<Spinner /> Small
|
||||
</Button>
|
||||
<Button variant="outline" disabled>
|
||||
<Spinner /> Outline
|
||||
</Button>
|
||||
<Button variant="outline" size="icon" disabled>
|
||||
<Spinner />
|
||||
<span className="sr-only">Loading...</span>
|
||||
</Button>
|
||||
<Button variant="destructive" disabled>
|
||||
<Spinner />
|
||||
Remove
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex items-center gap-6">
|
||||
<Badge>
|
||||
<Spinner />
|
||||
Badge
|
||||
</Badge>
|
||||
<Badge variant="secondary">
|
||||
<Spinner />
|
||||
Badge
|
||||
</Badge>
|
||||
<Badge variant="destructive">
|
||||
<Spinner />
|
||||
Badge
|
||||
</Badge>
|
||||
<Badge variant="outline">
|
||||
<Spinner />
|
||||
Badge
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex max-w-xs items-center gap-6">
|
||||
<Field>
|
||||
<FieldLabel htmlFor="input-group-spinner">Input Group</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupInput id="input-group-spinner" />
|
||||
<InputGroupAddon>
|
||||
<Spinner />
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</Field>
|
||||
</div>
|
||||
<Empty className="min-h-[80svh]">
|
||||
<EmptyHeader>
|
||||
<EmptyMedia variant="icon">
|
||||
<Spinner />
|
||||
</EmptyMedia>
|
||||
<EmptyTitle>No projects yet</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
You haven't created any projects yet. Get started by creating
|
||||
your first project.
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
<EmptyContent>
|
||||
<div className="flex gap-2">
|
||||
<Button asChild>
|
||||
<a href="#">Create project</a>
|
||||
</Button>
|
||||
<Button variant="outline">Import project</Button>
|
||||
</div>
|
||||
<Button variant="link" asChild className="text-muted-foreground">
|
||||
<a href="#">
|
||||
Learn more <ArrowRightIcon />
|
||||
</a>
|
||||
</Button>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
147
apps/v4/app/(internal)/sink/components/theme-selector.tsx
Normal file
147
apps/v4/app/(internal)/sink/components/theme-selector.tsx
Normal file
@@ -0,0 +1,147 @@
|
||||
"use client"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
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 THEMES = {
|
||||
sizes: [
|
||||
{
|
||||
name: "Default",
|
||||
value: "default",
|
||||
},
|
||||
{
|
||||
name: "Scaled",
|
||||
value: "scaled",
|
||||
},
|
||||
{
|
||||
name: "Mono",
|
||||
value: "mono",
|
||||
},
|
||||
],
|
||||
colors: [
|
||||
{
|
||||
name: "Blue",
|
||||
value: "blue",
|
||||
},
|
||||
{
|
||||
name: "Green",
|
||||
value: "green",
|
||||
},
|
||||
{
|
||||
name: "Amber",
|
||||
value: "amber",
|
||||
},
|
||||
{
|
||||
name: "Rose",
|
||||
value: "rose",
|
||||
},
|
||||
{
|
||||
name: "Purple",
|
||||
value: "purple",
|
||||
},
|
||||
{
|
||||
name: "Orange",
|
||||
value: "orange",
|
||||
},
|
||||
{
|
||||
name: "Teal",
|
||||
value: "teal",
|
||||
},
|
||||
],
|
||||
fonts: [
|
||||
{
|
||||
name: "Inter",
|
||||
value: "inter",
|
||||
},
|
||||
{
|
||||
name: "Noto Sans",
|
||||
value: "noto-sans",
|
||||
},
|
||||
{
|
||||
name: "Nunito Sans",
|
||||
value: "nunito-sans",
|
||||
},
|
||||
{
|
||||
name: "Figtree",
|
||||
value: "figtree",
|
||||
},
|
||||
],
|
||||
radius: [
|
||||
{
|
||||
name: "None",
|
||||
value: "rounded-none",
|
||||
},
|
||||
{
|
||||
name: "Small",
|
||||
value: "rounded-small",
|
||||
},
|
||||
{
|
||||
name: "Medium",
|
||||
value: "rounded-medium",
|
||||
},
|
||||
{
|
||||
name: "Large",
|
||||
value: "rounded-large",
|
||||
},
|
||||
{
|
||||
name: "Full",
|
||||
value: "rounded-full",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export function ThemeSelector({ className }: React.ComponentProps<"div">) {
|
||||
const { activeTheme, setActiveTheme } = useThemeConfig()
|
||||
|
||||
return (
|
||||
<div className={cn("flex items-center gap-2", className)}>
|
||||
<Label htmlFor="theme-selector" className="sr-only">
|
||||
Theme
|
||||
</Label>
|
||||
<Select value={activeTheme} onValueChange={setActiveTheme}>
|
||||
<SelectTrigger
|
||||
id="theme-selector"
|
||||
size="sm"
|
||||
className="bg-secondary text-secondary-foreground border-secondary justify-start shadow-none *:data-[slot=select-value]:w-16"
|
||||
>
|
||||
<SelectValue placeholder="Select" />
|
||||
</SelectTrigger>
|
||||
<SelectContent align="end">
|
||||
{Object.entries(THEMES).map(
|
||||
([key, themes], index) =>
|
||||
themes.length > 0 && (
|
||||
<div key={key}>
|
||||
{index > 0 && <SelectSeparator />}
|
||||
<SelectGroup>
|
||||
<SelectLabel>
|
||||
{key.charAt(0).toUpperCase() + key.slice(1)}
|
||||
</SelectLabel>
|
||||
{themes.map((theme) => (
|
||||
<SelectItem
|
||||
key={theme.name}
|
||||
value={theme.value}
|
||||
className="data-[state=checked]:opacity-50"
|
||||
>
|
||||
{theme.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,4 +1,11 @@
|
||||
import { BoldIcon, ItalicIcon, UnderlineIcon } from "lucide-react"
|
||||
import {
|
||||
BoldIcon,
|
||||
BookmarkIcon,
|
||||
HeartIcon,
|
||||
ItalicIcon,
|
||||
StarIcon,
|
||||
UnderlineIcon,
|
||||
} from "lucide-react"
|
||||
|
||||
import {
|
||||
ToggleGroup,
|
||||
@@ -8,7 +15,7 @@ import {
|
||||
export function ToggleGroupDemo() {
|
||||
return (
|
||||
<div className="flex flex-wrap items-start gap-4">
|
||||
<ToggleGroup type="multiple">
|
||||
<ToggleGroup type="multiple" spacing={2}>
|
||||
<ToggleGroupItem value="bold" aria-label="Toggle bold">
|
||||
<BoldIcon />
|
||||
</ToggleGroupItem>
|
||||
@@ -54,12 +61,7 @@ export function ToggleGroupDemo() {
|
||||
</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
|
||||
<ToggleGroup
|
||||
type="single"
|
||||
size="sm"
|
||||
defaultValue="last-24-hours"
|
||||
className="*:data-[slot=toggle-group-item]:px-3"
|
||||
>
|
||||
<ToggleGroup type="single" size="sm" defaultValue="last-24-hours">
|
||||
<ToggleGroupItem
|
||||
value="last-24-hours"
|
||||
aria-label="Toggle last 24 hours"
|
||||
@@ -70,6 +72,68 @@ export function ToggleGroupDemo() {
|
||||
Last 7 days
|
||||
</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
<ToggleGroup type="single" size="sm" defaultValue="top" variant="outline">
|
||||
<ToggleGroupItem value="top" aria-label="Toggle top">
|
||||
Top
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem value="bottom" aria-label="Toggle bottom">
|
||||
Bottom
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem value="left" aria-label="Toggle left">
|
||||
Left
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem value="right" aria-label="Toggle right">
|
||||
Right
|
||||
</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
|
||||
<ToggleGroup
|
||||
type="single"
|
||||
size="sm"
|
||||
defaultValue="top"
|
||||
variant="outline"
|
||||
spacing={2}
|
||||
>
|
||||
<ToggleGroupItem value="top" aria-label="Toggle top">
|
||||
Top
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem value="bottom" aria-label="Toggle bottom">
|
||||
Bottom
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem value="left" aria-label="Toggle left">
|
||||
Left
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem value="right" aria-label="Toggle right">
|
||||
Right
|
||||
</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
|
||||
<ToggleGroup type="multiple" variant="outline" spacing={2} size="sm">
|
||||
<ToggleGroupItem
|
||||
value="star"
|
||||
aria-label="Toggle star"
|
||||
className="data-[state=on]:bg-transparent data-[state=on]:*:[svg]:fill-yellow-500 data-[state=on]:*:[svg]:stroke-yellow-500"
|
||||
>
|
||||
<StarIcon />
|
||||
Star
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem
|
||||
value="heart"
|
||||
aria-label="Toggle heart"
|
||||
className="data-[state=on]:bg-transparent data-[state=on]:*:[svg]:fill-red-500 data-[state=on]:*:[svg]:stroke-red-500"
|
||||
>
|
||||
<HeartIcon />
|
||||
Heart
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem
|
||||
value="bookmark"
|
||||
aria-label="Toggle bookmark"
|
||||
className="data-[state=on]:bg-transparent data-[state=on]:*:[svg]:fill-blue-500 data-[state=on]:*:[svg]:stroke-blue-500"
|
||||
>
|
||||
<BookmarkIcon />
|
||||
Bookmark
|
||||
</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
66
apps/v4/app/(internal)/sink/layout.tsx
Normal file
66
apps/v4/app/(internal)/sink/layout.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import { Figtree, Inter, Noto_Sans, Nunito_Sans } from "next/font/google"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { ModeSwitcher } from "@/components/mode-switcher"
|
||||
import { Separator } from "@/registry/new-york-v4/ui/separator"
|
||||
import {
|
||||
SidebarInset,
|
||||
SidebarProvider,
|
||||
SidebarTrigger,
|
||||
} from "@/registry/new-york-v4/ui/sidebar"
|
||||
import { AppBreadcrumbs } from "@/app/(internal)/sink/components/app-breadcrumbs"
|
||||
import { AppSidebar } from "@/app/(internal)/sink/components/app-sidebar"
|
||||
import { ThemeSelector } from "@/app/(internal)/sink/components/theme-selector"
|
||||
|
||||
const inter = Inter({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-inter",
|
||||
})
|
||||
|
||||
const notoSans = Noto_Sans({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-noto-sans",
|
||||
})
|
||||
|
||||
const nunitoSans = Nunito_Sans({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-nunito-sans",
|
||||
})
|
||||
|
||||
const figtree = Figtree({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-figtree",
|
||||
})
|
||||
|
||||
export default async function SinkLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<SidebarProvider
|
||||
defaultOpen={true}
|
||||
className={cn(
|
||||
"theme-container",
|
||||
inter.variable,
|
||||
notoSans.variable,
|
||||
nunitoSans.variable,
|
||||
figtree.variable
|
||||
)}
|
||||
>
|
||||
<AppSidebar />
|
||||
<SidebarInset>
|
||||
<header className="bg-background sticky top-0 z-10 flex h-14 items-center border-b p-4">
|
||||
<SidebarTrigger />
|
||||
<Separator orientation="vertical" className="mr-4 ml-2 !h-4" />
|
||||
<AppBreadcrumbs />
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
<ModeSwitcher />
|
||||
<ThemeSelector />
|
||||
</div>
|
||||
</header>
|
||||
{children}
|
||||
</SidebarInset>
|
||||
</SidebarProvider>
|
||||
)
|
||||
}
|
||||
@@ -1,62 +1,7 @@
|
||||
import { Metadata } from "next"
|
||||
import { cookies } from "next/headers"
|
||||
|
||||
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"
|
||||
import { AccordionDemo } from "@/app/(internal)/sink/components/accordion-demo"
|
||||
import { AlertDemo } from "@/app/(internal)/sink/components/alert-demo"
|
||||
import { AlertDialogDemo } from "@/app/(internal)/sink/components/alert-dialog-demo"
|
||||
import { AppSidebar } from "@/app/(internal)/sink/components/app-sidebar"
|
||||
import { AspectRatioDemo } from "@/app/(internal)/sink/components/aspect-ratio-demo"
|
||||
import { AvatarDemo } from "@/app/(internal)/sink/components/avatar-demo"
|
||||
import { BadgeDemo } from "@/app/(internal)/sink/components/badge-demo"
|
||||
import { BreadcrumbDemo } from "@/app/(internal)/sink/components/breadcrumb-demo"
|
||||
import { ButtonDemo } from "@/app/(internal)/sink/components/button-demo"
|
||||
import { CalendarDemo } from "@/app/(internal)/sink/components/calendar-demo"
|
||||
import { CardDemo } from "@/app/(internal)/sink/components/card-demo"
|
||||
import { CarouselDemo } from "@/app/(internal)/sink/components/carousel-demo"
|
||||
import { ChartDemo } from "@/app/(internal)/sink/components/chart-demo"
|
||||
import { CheckboxDemo } from "@/app/(internal)/sink/components/checkbox-demo"
|
||||
import { CollapsibleDemo } from "@/app/(internal)/sink/components/collapsible-demo"
|
||||
import { ComboboxDemo } from "@/app/(internal)/sink/components/combobox-demo"
|
||||
import { CommandDemo } from "@/app/(internal)/sink/components/command-demo"
|
||||
import { componentRegistry } from "@/app/(internal)/sink/component-registry"
|
||||
import { ComponentWrapper } from "@/app/(internal)/sink/components/component-wrapper"
|
||||
import { ContextMenuDemo } from "@/app/(internal)/sink/components/context-menu-demo"
|
||||
import { DatePickerDemo } from "@/app/(internal)/sink/components/date-picker-demo"
|
||||
import { DialogDemo } from "@/app/(internal)/sink/components/dialog-demo"
|
||||
import { DrawerDemo } from "@/app/(internal)/sink/components/drawer-demo"
|
||||
import { DropdownMenuDemo } from "@/app/(internal)/sink/components/dropdown-menu-demo"
|
||||
import { FormDemo } from "@/app/(internal)/sink/components/form-demo"
|
||||
import { HoverCardDemo } from "@/app/(internal)/sink/components/hover-card-demo"
|
||||
import { InputDemo } from "@/app/(internal)/sink/components/input-demo"
|
||||
import { InputOTPDemo } from "@/app/(internal)/sink/components/input-otp-demo"
|
||||
import { LabelDemo } from "@/app/(internal)/sink/components/label-demo"
|
||||
import { MenubarDemo } from "@/app/(internal)/sink/components/menubar-demo"
|
||||
import { NavigationMenuDemo } from "@/app/(internal)/sink/components/navigation-menu-demo"
|
||||
import { PaginationDemo } from "@/app/(internal)/sink/components/pagination-demo"
|
||||
import { PopoverDemo } from "@/app/(internal)/sink/components/popover-demo"
|
||||
import { ProgressDemo } from "@/app/(internal)/sink/components/progress-demo"
|
||||
import { RadioGroupDemo } from "@/app/(internal)/sink/components/radio-group-demo"
|
||||
import { ResizableDemo } from "@/app/(internal)/sink/components/resizable-demo"
|
||||
import { ScrollAreaDemo } from "@/app/(internal)/sink/components/scroll-area-demo"
|
||||
import { SelectDemo } from "@/app/(internal)/sink/components/select-demo"
|
||||
import { SeparatorDemo } from "@/app/(internal)/sink/components/separator-demo"
|
||||
import { SheetDemo } from "@/app/(internal)/sink/components/sheet-demo"
|
||||
import { SkeletonDemo } from "@/app/(internal)/sink/components/skeleton-demo"
|
||||
import { SliderDemo } from "@/app/(internal)/sink/components/slider-demo"
|
||||
import { SonnerDemo } from "@/app/(internal)/sink/components/sonner-demo"
|
||||
import { SwitchDemo } from "@/app/(internal)/sink/components/switch-demo"
|
||||
import { TableDemo } from "@/app/(internal)/sink/components/table-demo"
|
||||
import { TabsDemo } from "@/app/(internal)/sink/components/tabs-demo"
|
||||
import { TextareaDemo } from "@/app/(internal)/sink/components/textarea-demo"
|
||||
import { ToggleDemo } from "@/app/(internal)/sink/components/toggle-demo"
|
||||
import { ToggleGroupDemo } from "@/app/(internal)/sink/components/toggle-group-demo"
|
||||
import { TooltipDemo } from "@/app/(internal)/sink/components/tooltip-demo"
|
||||
|
||||
export const dynamic = "force-static"
|
||||
export const revalidate = false
|
||||
@@ -66,164 +11,25 @@ export const metadata: Metadata = {
|
||||
description: "A page with all components for testing purposes.",
|
||||
}
|
||||
|
||||
export default async function SinkPage() {
|
||||
const cookieStore = await cookies()
|
||||
const defaultOpen = cookieStore.get("sidebar_state")?.value === "true"
|
||||
|
||||
export default function SinkPage() {
|
||||
return (
|
||||
<SidebarProvider defaultOpen={defaultOpen} className="theme-container">
|
||||
<AppSidebar />
|
||||
<SidebarInset>
|
||||
<header className="bg-background sticky top-0 z-10 flex h-14 items-center border-b p-4">
|
||||
<SidebarTrigger />
|
||||
<Separator orientation="vertical" className="mr-4 ml-2 !h-4" />
|
||||
<h1 className="text-base font-medium">Kitchen Sink</h1>
|
||||
<ThemeSelector className="ml-auto" />
|
||||
</header>
|
||||
<div className="@container grid flex-1 gap-4 p-4">
|
||||
<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="chart" className="w-full">
|
||||
<ChartDemo />
|
||||
</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>
|
||||
</SidebarInset>
|
||||
</SidebarProvider>
|
||||
<div className="@container grid flex-1 gap-4 p-4">
|
||||
{Object.entries(componentRegistry)
|
||||
.filter(([, component]) => {
|
||||
return component.type === "registry:ui"
|
||||
})
|
||||
.map(([key, component]) => {
|
||||
const Component = component.component
|
||||
return (
|
||||
<ComponentWrapper
|
||||
key={key}
|
||||
name={key}
|
||||
className={component.className || ""}
|
||||
>
|
||||
<Component />
|
||||
</ComponentWrapper>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
105
apps/v4/app/(sandbox)/sandbox/[style]/page.tsx
Normal file
105
apps/v4/app/(sandbox)/sandbox/[style]/page.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
import { Metadata } from "next"
|
||||
import { notFound } from "next/navigation"
|
||||
|
||||
import { siteConfig } from "@/lib/config"
|
||||
import { getRegistryComponent, getRegistryItems } from "@/lib/registry"
|
||||
import { absoluteUrl, cn } from "@/lib/utils"
|
||||
import { getStyle, STYLES } from "@/registry/styles"
|
||||
|
||||
export const revalidate = false
|
||||
export const dynamic = "force-static"
|
||||
export const dynamicParams = false
|
||||
|
||||
const allowedTypes = ["registry:example"]
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{
|
||||
style: string
|
||||
}>
|
||||
}): Promise<Metadata> {
|
||||
const { style: styleName } = await params
|
||||
const style = getStyle(styleName)
|
||||
|
||||
if (!style) {
|
||||
return {}
|
||||
}
|
||||
|
||||
const title = style.title
|
||||
|
||||
return {
|
||||
title,
|
||||
openGraph: {
|
||||
title,
|
||||
type: "article",
|
||||
url: absoluteUrl(`/sandbox/${style.name}`),
|
||||
images: [
|
||||
{
|
||||
url: siteConfig.ogImage,
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: siteConfig.name,
|
||||
},
|
||||
],
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title,
|
||||
images: [siteConfig.ogImage],
|
||||
creator: "@shadcn",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export async function generateStaticParams() {
|
||||
return STYLES.map((style) => ({
|
||||
style: style.name,
|
||||
}))
|
||||
}
|
||||
|
||||
export default async function BlockPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{
|
||||
style: string
|
||||
}>
|
||||
}) {
|
||||
const { style: styleName } = await params
|
||||
const style = getStyle(styleName)
|
||||
|
||||
if (!style) {
|
||||
return notFound()
|
||||
}
|
||||
|
||||
const items = await getRegistryItems(style.name, (item) =>
|
||||
allowedTypes.includes(item.type)
|
||||
)
|
||||
|
||||
if (items.length === 0) {
|
||||
return notFound()
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={cn("grid gap-6")}>
|
||||
{items
|
||||
.filter((item) => item !== null)
|
||||
.map((item) => {
|
||||
const Component = getRegistryComponent(item.name, style.name)
|
||||
if (!Component) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<div
|
||||
key={item.name}
|
||||
className={cn("bg-background", item.meta?.container)}
|
||||
>
|
||||
<Component />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,30 +1,39 @@
|
||||
/* eslint-disable react-hooks/static-components */
|
||||
import * as React from "react"
|
||||
import { Metadata } from "next"
|
||||
import { notFound } from "next/navigation"
|
||||
import { registryItemSchema } from "shadcn/schema"
|
||||
import { z } from "zod"
|
||||
|
||||
import { siteConfig } from "@/lib/config"
|
||||
import { getRegistryComponent, getRegistryItem } from "@/lib/registry"
|
||||
import { absoluteUrl, cn } from "@/lib/utils"
|
||||
import { getStyle, STYLES, type Style } from "@/registry/styles"
|
||||
|
||||
export const revalidate = false
|
||||
export const dynamic = "force-static"
|
||||
export const dynamicParams = false
|
||||
|
||||
const getCachedRegistryItem = React.cache(async (name: string) => {
|
||||
return await getRegistryItem(name)
|
||||
})
|
||||
const getCachedRegistryItem = React.cache(
|
||||
async (name: string, styleName: Style["name"]) => {
|
||||
return await getRegistryItem(name, styleName)
|
||||
}
|
||||
)
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{
|
||||
style: string
|
||||
name: string
|
||||
}>
|
||||
}): Promise<Metadata> {
|
||||
const { name } = await params
|
||||
const item = await getCachedRegistryItem(name)
|
||||
const { style: styleName, name } = await params
|
||||
const style = getStyle(styleName)
|
||||
|
||||
if (!style) {
|
||||
return {}
|
||||
}
|
||||
|
||||
const item = await getCachedRegistryItem(name, style.name)
|
||||
|
||||
if (!item) {
|
||||
return {}
|
||||
@@ -34,13 +43,13 @@ export async function generateMetadata({
|
||||
const description = item.description
|
||||
|
||||
return {
|
||||
title: item.description,
|
||||
title: item.name,
|
||||
description,
|
||||
openGraph: {
|
||||
title,
|
||||
description,
|
||||
type: "article",
|
||||
url: absoluteUrl(`/view/${item.name}`),
|
||||
url: absoluteUrl(`/view/${style.name}/${item.name}`),
|
||||
images: [
|
||||
{
|
||||
url: siteConfig.ogImage,
|
||||
@@ -62,32 +71,52 @@ export async function generateMetadata({
|
||||
|
||||
export async function generateStaticParams() {
|
||||
const { Index } = await import("@/registry/__index__")
|
||||
const index = z.record(registryItemSchema).parse(Index)
|
||||
const params: Array<{ style: string; name: string }> = []
|
||||
|
||||
return Object.values(index)
|
||||
.filter((block) =>
|
||||
[
|
||||
"registry:block",
|
||||
"registry:component",
|
||||
"registry:example",
|
||||
"registry:internal",
|
||||
].includes(block.type)
|
||||
)
|
||||
.map((block) => ({
|
||||
name: block.name,
|
||||
}))
|
||||
for (const style of STYLES) {
|
||||
if (!Index[style.name]) {
|
||||
continue
|
||||
}
|
||||
|
||||
const styleIndex = Index[style.name]
|
||||
for (const itemName in styleIndex) {
|
||||
const item = styleIndex[itemName]
|
||||
if (
|
||||
[
|
||||
"registry:block",
|
||||
"registry:component",
|
||||
"registry:example",
|
||||
"registry:internal",
|
||||
].includes(item.type)
|
||||
) {
|
||||
params.push({
|
||||
style: style.name,
|
||||
name: item.name,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return params
|
||||
}
|
||||
|
||||
export default async function BlockPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{
|
||||
style: string
|
||||
name: string
|
||||
}>
|
||||
}) {
|
||||
const { name } = await params
|
||||
const item = await getCachedRegistryItem(name)
|
||||
const Component = getRegistryComponent(name)
|
||||
const { style: styleName, name } = await params
|
||||
const style = getStyle(styleName)
|
||||
|
||||
if (!style) {
|
||||
return notFound()
|
||||
}
|
||||
|
||||
const item = await getCachedRegistryItem(name, style.name)
|
||||
const Component = getRegistryComponent(name, style.name)
|
||||
|
||||
if (!item || !Component) {
|
||||
return notFound()
|
||||
5
apps/v4/app/api/search/route.ts
Normal file
5
apps/v4/app/api/search/route.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { createFromSource } from "fumadocs-core/search/server"
|
||||
|
||||
import { source } from "@/lib/source"
|
||||
|
||||
export const { GET } = createFromSource(source)
|
||||
@@ -84,13 +84,13 @@ export default function RootLayout({
|
||||
</head>
|
||||
<body
|
||||
className={cn(
|
||||
"text-foreground group/body overscroll-none font-sans antialiased [--footer-height:calc(var(--spacing)*14)] [--header-height:calc(var(--spacing)*14)] xl:[--footer-height:calc(var(--spacing)*24)]",
|
||||
"group/body theme-blue overscroll-none antialiased [--footer-height:calc(var(--spacing)*14)] [--header-height:calc(var(--spacing)*14)] xl:[--footer-height:calc(var(--spacing)*24)]",
|
||||
fontVariables
|
||||
)}
|
||||
>
|
||||
<ThemeProvider>
|
||||
<LayoutProvider>
|
||||
<ActiveThemeProvider>
|
||||
<ActiveThemeProvider initialTheme="blue">
|
||||
{children}
|
||||
<TailwindIndicator />
|
||||
<Toaster position="top-center" />
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
useState,
|
||||
} from "react"
|
||||
|
||||
const DEFAULT_THEME = "default"
|
||||
const DEFAULT_THEME = "blue"
|
||||
|
||||
type ThemeContextType = {
|
||||
activeTheme: string
|
||||
|
||||
@@ -5,9 +5,10 @@ import { Badge } from "@/registry/new-york-v4/ui/badge"
|
||||
|
||||
export function Announcement() {
|
||||
return (
|
||||
<Badge asChild variant="secondary" className="rounded-full">
|
||||
<Badge asChild variant="secondary" className="bg-transparent">
|
||||
<Link href="/docs/changelog">
|
||||
Now available: shadcn CLI 3.0 and MCP Server <ArrowRightIcon />
|
||||
<span className="flex size-2 rounded-full bg-blue-500" title="New" />
|
||||
New Components: Field, Input Group, Item and more <ArrowRightIcon />
|
||||
</Link>
|
||||
</Badge>
|
||||
)
|
||||
|
||||
@@ -10,9 +10,16 @@ import {
|
||||
import { cn } from "@/lib/utils"
|
||||
import { BlockViewer } from "@/components/block-viewer"
|
||||
import { ComponentPreview } from "@/components/component-preview"
|
||||
import { type Style } from "@/registry/styles"
|
||||
|
||||
export async function BlockDisplay({ name }: { name: string }) {
|
||||
const item = await getCachedRegistryItem(name)
|
||||
export async function BlockDisplay({
|
||||
name,
|
||||
styleName,
|
||||
}: {
|
||||
name: string
|
||||
styleName: Style["name"]
|
||||
}) {
|
||||
const item = await getCachedRegistryItem(name, styleName)
|
||||
|
||||
if (!item?.files) {
|
||||
return null
|
||||
@@ -24,9 +31,15 @@ export async function BlockDisplay({ name }: { name: string }) {
|
||||
])
|
||||
|
||||
return (
|
||||
<BlockViewer item={item} tree={tree} highlightedFiles={highlightedFiles}>
|
||||
<BlockViewer
|
||||
item={item}
|
||||
tree={tree}
|
||||
highlightedFiles={highlightedFiles}
|
||||
styleName={styleName}
|
||||
>
|
||||
<ComponentPreview
|
||||
name={item.name}
|
||||
styleName={styleName}
|
||||
hideCode
|
||||
className={cn(
|
||||
"my-0 **:[.preview]:h-auto **:[.preview]:p-4 **:[.preview>.p-6]:p-0",
|
||||
@@ -37,9 +50,11 @@ export async function BlockDisplay({ name }: { name: string }) {
|
||||
)
|
||||
}
|
||||
|
||||
const getCachedRegistryItem = React.cache(async (name: string) => {
|
||||
return await getRegistryItem(name)
|
||||
})
|
||||
const getCachedRegistryItem = React.cache(
|
||||
async (name: string, styleName: Style["name"]) => {
|
||||
return await getRegistryItem(name, styleName)
|
||||
}
|
||||
)
|
||||
|
||||
const getCachedFileTree = React.cache(
|
||||
async (files: Array<{ path: string; target?: string }>) => {
|
||||
|
||||
@@ -54,6 +54,7 @@ import {
|
||||
ToggleGroup,
|
||||
ToggleGroupItem,
|
||||
} from "@/registry/new-york-v4/ui/toggle-group"
|
||||
import { type Style } from "@/registry/styles"
|
||||
|
||||
type BlockViewerContext = {
|
||||
item: z.infer<typeof registryItemSchema>
|
||||
@@ -128,7 +129,15 @@ function BlockViewerProvider({
|
||||
)
|
||||
}
|
||||
|
||||
function BlockViewerToolbar() {
|
||||
type BlockViewerProps = Pick<
|
||||
BlockViewerContext,
|
||||
"item" | "tree" | "highlightedFiles"
|
||||
> & {
|
||||
children: React.ReactNode
|
||||
styleName: Style["name"]
|
||||
}
|
||||
|
||||
function BlockViewerToolbar({ styleName }: { styleName: Style["name"] }) {
|
||||
const { setView, view, item, resizablePanelRef, setIframeKey } =
|
||||
useBlockViewer()
|
||||
const { copyToClipboard, isCopied } = useCopyToClipboard()
|
||||
@@ -181,7 +190,7 @@ function BlockViewerToolbar() {
|
||||
asChild
|
||||
title="Open in New Tab"
|
||||
>
|
||||
<Link href={`/view/${item.name}`} target="_blank">
|
||||
<Link href={`/view/${styleName}/${item.name}`} target="_blank">
|
||||
<span className="sr-only">Open in New Tab</span>
|
||||
<Fullscreen />
|
||||
</Link>
|
||||
@@ -222,13 +231,19 @@ function BlockViewerToolbar() {
|
||||
)
|
||||
}
|
||||
|
||||
function BlockViewerIframe({ className }: { className?: string }) {
|
||||
function BlockViewerIframe({
|
||||
className,
|
||||
styleName,
|
||||
}: {
|
||||
className?: string
|
||||
styleName: Style["name"]
|
||||
}) {
|
||||
const { item, iframeKey } = useBlockViewer()
|
||||
|
||||
return (
|
||||
<iframe
|
||||
key={iframeKey}
|
||||
src={`/view/${item.name}`}
|
||||
src={`/view/${styleName}/${item.name}`}
|
||||
height={item.meta?.iframeHeight ?? 930}
|
||||
loading="lazy"
|
||||
className={cn(
|
||||
@@ -239,7 +254,7 @@ function BlockViewerIframe({ className }: { className?: string }) {
|
||||
)
|
||||
}
|
||||
|
||||
function BlockViewerView() {
|
||||
function BlockViewerView({ styleName }: { styleName: Style["name"] }) {
|
||||
const { resizablePanelRef } = useBlockViewer()
|
||||
|
||||
return (
|
||||
@@ -256,7 +271,7 @@ function BlockViewerView() {
|
||||
defaultSize={100}
|
||||
minSize={30}
|
||||
>
|
||||
<BlockViewerIframe />
|
||||
<BlockViewerIframe styleName={styleName} />
|
||||
</ResizablePanel>
|
||||
<ResizableHandle className="after:bg-border relative hidden w-3 bg-transparent p-0 after:absolute after:top-1/2 after:right-0 after:h-8 after:w-[6px] after:translate-x-[-1px] after:-translate-y-1/2 after:rounded-full after:transition-all after:hover:h-10 md:block" />
|
||||
<ResizablePanel defaultSize={0} minSize={0} />
|
||||
@@ -471,10 +486,9 @@ function BlockViewer({
|
||||
tree,
|
||||
highlightedFiles,
|
||||
children,
|
||||
styleName,
|
||||
...props
|
||||
}: Pick<BlockViewerContext, "item" | "tree" | "highlightedFiles"> & {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
}: BlockViewerProps) {
|
||||
return (
|
||||
<BlockViewerProvider
|
||||
item={item}
|
||||
@@ -482,8 +496,8 @@ function BlockViewer({
|
||||
highlightedFiles={highlightedFiles}
|
||||
{...props}
|
||||
>
|
||||
<BlockViewerToolbar />
|
||||
<BlockViewerView />
|
||||
<BlockViewerToolbar styleName={styleName} />
|
||||
<BlockViewerView styleName={styleName} />
|
||||
<BlockViewerCode />
|
||||
<BlockViewerMobile>{children}</BlockViewerMobile>
|
||||
</BlockViewerProvider>
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
import Link from "next/link"
|
||||
import { usePathname } from "next/navigation"
|
||||
|
||||
import { registryCategories } from "@/lib/categories"
|
||||
import { ScrollArea, ScrollBar } from "@/registry/new-york-v4/ui/scroll-area"
|
||||
import { registryCategories } from "@/registry/registry-categories"
|
||||
|
||||
export function BlocksNav() {
|
||||
const pathname = usePathname()
|
||||
|
||||
@@ -10,12 +10,17 @@ export function Callout({
|
||||
children,
|
||||
icon,
|
||||
className,
|
||||
variant = "default",
|
||||
...props
|
||||
}: React.ComponentProps<typeof Alert> & { icon?: React.ReactNode }) {
|
||||
}: React.ComponentProps<typeof Alert> & {
|
||||
icon?: React.ReactNode
|
||||
variant?: "default" | "info" | "warning"
|
||||
}) {
|
||||
return (
|
||||
<Alert
|
||||
data-variant={variant}
|
||||
className={cn(
|
||||
"bg-surface text-surface-foreground mt-6 w-auto border-none md:-mx-1",
|
||||
"bg-background text-foreground mt-6 w-auto border md:-mx-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -32,7 +32,12 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/registry/new-york-v4/ui/dialog"
|
||||
import { Input } from "@/registry/new-york-v4/ui/input"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupInput,
|
||||
} from "@/registry/new-york-v4/ui/input-group"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
@@ -159,23 +164,25 @@ export function CardsChat() {
|
||||
}}
|
||||
className="relative w-full"
|
||||
>
|
||||
<Input
|
||||
id="message"
|
||||
placeholder="Type your message..."
|
||||
className="flex-1 pr-10"
|
||||
autoComplete="off"
|
||||
value={input}
|
||||
onChange={(event) => setInput(event.target.value)}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
size="icon"
|
||||
className="absolute top-1/2 right-2 size-6 -translate-y-1/2 rounded-full"
|
||||
disabled={inputLength === 0}
|
||||
>
|
||||
<ArrowUpIcon className="size-3.5" />
|
||||
<span className="sr-only">Send</span>
|
||||
</Button>
|
||||
<InputGroup>
|
||||
<InputGroupInput
|
||||
id="message"
|
||||
placeholder="Type your message..."
|
||||
autoComplete="off"
|
||||
value={input}
|
||||
onChange={(event) => setInput(event.target.value)}
|
||||
/>
|
||||
<InputGroupAddon align="inline-end">
|
||||
<InputGroupButton
|
||||
type="submit"
|
||||
size="icon-xs"
|
||||
className="rounded-full"
|
||||
>
|
||||
<ArrowUpIcon />
|
||||
<span className="sr-only">Send</span>
|
||||
</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</form>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
|
||||
@@ -5,11 +5,15 @@ import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/new-york-v4/ui/card"
|
||||
import { Label } from "@/registry/new-york-v4/ui/label"
|
||||
import {
|
||||
Field,
|
||||
FieldContent,
|
||||
FieldDescription,
|
||||
FieldLabel,
|
||||
} from "@/registry/new-york-v4/ui/field"
|
||||
import { Switch } from "@/registry/new-york-v4/ui/switch"
|
||||
|
||||
export function CardsCookieSettings() {
|
||||
@@ -20,32 +24,20 @@ export function CardsCookieSettings() {
|
||||
<CardDescription>Manage your cookie settings here.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-6">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<Label htmlFor="necessary" className="flex flex-col items-start">
|
||||
<span>Strictly Necessary</span>
|
||||
<span className="text-muted-foreground leading-snug font-normal">
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor="necessary">Strictly Necessary</FieldLabel>
|
||||
<FieldDescription>
|
||||
These cookies are essential in order to use the website and use
|
||||
its features.
|
||||
</span>
|
||||
</Label>
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<Switch id="necessary" defaultChecked aria-label="Necessary" />
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<Label htmlFor="functional" className="flex flex-col items-start">
|
||||
<span>Functional Cookies</span>
|
||||
<span className="text-muted-foreground leading-snug font-normal">
|
||||
These cookies allow the website to provide personalized
|
||||
functionality.
|
||||
</span>
|
||||
</Label>
|
||||
<Switch id="functional" aria-label="Functional" />
|
||||
</div>
|
||||
</Field>
|
||||
<Field>
|
||||
<Button variant="outline">Save preferences</Button>
|
||||
</Field>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button variant="outline" className="w-full">
|
||||
Save preferences
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,12 +5,16 @@ import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/new-york-v4/ui/card"
|
||||
import {
|
||||
Field,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldSeparator,
|
||||
} from "@/registry/new-york-v4/ui/field"
|
||||
import { Input } from "@/registry/new-york-v4/ui/input"
|
||||
import { Label } from "@/registry/new-york-v4/ui/label"
|
||||
|
||||
export function CardsCreateAccount() {
|
||||
return (
|
||||
@@ -21,53 +25,48 @@ export function CardsCreateAccount() {
|
||||
Enter your email below to create your account
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<Button variant="outline">
|
||||
<svg viewBox="0 0 438.549 438.549">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M409.132 114.573c-19.608-33.596-46.205-60.194-79.798-79.8-33.598-19.607-70.277-29.408-110.063-29.408-39.781 0-76.472 9.804-110.063 29.408-33.596 19.605-60.192 46.204-79.8 79.8C9.803 148.168 0 184.854 0 224.63c0 47.78 13.94 90.745 41.827 128.906 27.884 38.164 63.906 64.572 108.063 79.227 5.14.954 8.945.283 11.419-1.996 2.475-2.282 3.711-5.14 3.711-8.562 0-.571-.049-5.708-.144-15.417a2549.81 2549.81 0 01-.144-25.406l-6.567 1.136c-4.187.767-9.469 1.092-15.846 1-6.374-.089-12.991-.757-19.842-1.999-6.854-1.231-13.229-4.086-19.13-8.559-5.898-4.473-10.085-10.328-12.56-17.556l-2.855-6.57c-1.903-4.374-4.899-9.233-8.992-14.559-4.093-5.331-8.232-8.945-12.419-10.848l-1.999-1.431c-1.332-.951-2.568-2.098-3.711-3.429-1.142-1.331-1.997-2.663-2.568-3.997-.572-1.335-.098-2.43 1.427-3.289 1.525-.859 4.281-1.276 8.28-1.276l5.708.853c3.807.763 8.516 3.042 14.133 6.851 5.614 3.806 10.229 8.754 13.846 14.842 4.38 7.806 9.657 13.754 15.846 17.847 6.184 4.093 12.419 6.136 18.699 6.136 6.28 0 11.704-.476 16.274-1.423 4.565-.952 8.848-2.383 12.847-4.285 1.713-12.758 6.377-22.559 13.988-29.41-10.848-1.14-20.601-2.857-29.264-5.14-8.658-2.286-17.605-5.996-26.835-11.14-9.235-5.137-16.896-11.516-22.985-19.126-6.09-7.614-11.088-17.61-14.987-29.979-3.901-12.374-5.852-26.648-5.852-42.826 0-23.035 7.52-42.637 22.557-58.817-7.044-17.318-6.379-36.732 1.997-58.24 5.52-1.715 13.706-.428 24.554 3.853 10.85 4.283 18.794 7.952 23.84 10.994 5.046 3.041 9.089 5.618 12.135 7.708 17.705-4.947 35.976-7.421 54.818-7.421s37.117 2.474 54.823 7.421l10.849-6.849c7.419-4.57 16.18-8.758 26.262-12.565 10.088-3.805 17.802-4.853 23.134-3.138 8.562 21.509 9.325 40.922 2.279 58.24 15.036 16.18 22.559 35.787 22.559 58.817 0 16.178-1.958 30.497-5.853 42.966-3.9 12.471-8.941 22.457-15.125 29.979-6.191 7.521-13.901 13.85-23.131 18.986-9.232 5.14-18.182 8.85-26.84 11.136-8.662 2.286-18.415 4.004-29.263 5.146 9.894 8.562 14.842 22.077 14.842 40.539v60.237c0 3.422 1.19 6.279 3.572 8.562 2.379 2.279 6.136 2.95 11.276 1.995 44.163-14.653 80.185-41.062 108.068-79.226 27.88-38.161 41.825-81.126 41.825-128.906-.01-39.771-9.818-76.454-29.414-110.049z"
|
||||
></path>
|
||||
</svg>
|
||||
GitHub
|
||||
</Button>
|
||||
<Button variant="outline">
|
||||
<svg role="img" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
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"
|
||||
/>
|
||||
</svg>
|
||||
Google
|
||||
</Button>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
<span className="w-full border-t" />
|
||||
</div>
|
||||
<div className="relative flex justify-center text-xs uppercase">
|
||||
<span className="bg-card text-muted-foreground px-2">
|
||||
Or continue with
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<Label htmlFor="email-create-account">Email</Label>
|
||||
<Input
|
||||
id="email-create-account"
|
||||
type="email"
|
||||
placeholder="m@example.com"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<Label htmlFor="password-create-account">Password</Label>
|
||||
<Input id="password-create-account" type="password" />
|
||||
</div>
|
||||
<CardContent>
|
||||
<FieldGroup>
|
||||
<Field className="grid grid-cols-2 gap-6">
|
||||
<Button variant="outline">
|
||||
<svg viewBox="0 0 438.549 438.549">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M409.132 114.573c-19.608-33.596-46.205-60.194-79.798-79.8-33.598-19.607-70.277-29.408-110.063-29.408-39.781 0-76.472 9.804-110.063 29.408-33.596 19.605-60.192 46.204-79.8 79.8C9.803 148.168 0 184.854 0 224.63c0 47.78 13.94 90.745 41.827 128.906 27.884 38.164 63.906 64.572 108.063 79.227 5.14.954 8.945.283 11.419-1.996 2.475-2.282 3.711-5.14 3.711-8.562 0-.571-.049-5.708-.144-15.417a2549.81 2549.81 0 01-.144-25.406l-6.567 1.136c-4.187.767-9.469 1.092-15.846 1-6.374-.089-12.991-.757-19.842-1.999-6.854-1.231-13.229-4.086-19.13-8.559-5.898-4.473-10.085-10.328-12.56-17.556l-2.855-6.57c-1.903-4.374-4.899-9.233-8.992-14.559-4.093-5.331-8.232-8.945-12.419-10.848l-1.999-1.431c-1.332-.951-2.568-2.098-3.711-3.429-1.142-1.331-1.997-2.663-2.568-3.997-.572-1.335-.098-2.43 1.427-3.289 1.525-.859 4.281-1.276 8.28-1.276l5.708.853c3.807.763 8.516 3.042 14.133 6.851 5.614 3.806 10.229 8.754 13.846 14.842 4.38 7.806 9.657 13.754 15.846 17.847 6.184 4.093 12.419 6.136 18.699 6.136 6.28 0 11.704-.476 16.274-1.423 4.565-.952 8.848-2.383 12.847-4.285 1.713-12.758 6.377-22.559 13.988-29.41-10.848-1.14-20.601-2.857-29.264-5.14-8.658-2.286-17.605-5.996-26.835-11.14-9.235-5.137-16.896-11.516-22.985-19.126-6.09-7.614-11.088-17.61-14.987-29.979-3.901-12.374-5.852-26.648-5.852-42.826 0-23.035 7.52-42.637 22.557-58.817-7.044-17.318-6.379-36.732 1.997-58.24 5.52-1.715 13.706-.428 24.554 3.853 10.85 4.283 18.794 7.952 23.84 10.994 5.046 3.041 9.089 5.618 12.135 7.708 17.705-4.947 35.976-7.421 54.818-7.421s37.117 2.474 54.823 7.421l10.849-6.849c7.419-4.57 16.18-8.758 26.262-12.565 10.088-3.805 17.802-4.853 23.134-3.138 8.562 21.509 9.325 40.922 2.279 58.24 15.036 16.18 22.559 35.787 22.559 58.817 0 16.178-1.958 30.497-5.853 42.966-3.9 12.471-8.941 22.457-15.125 29.979-6.191 7.521-13.901 13.85-23.131 18.986-9.232 5.14-18.182 8.85-26.84 11.136-8.662 2.286-18.415 4.004-29.263 5.146 9.894 8.562 14.842 22.077 14.842 40.539v60.237c0 3.422 1.19 6.279 3.572 8.562 2.379 2.279 6.136 2.95 11.276 1.995 44.163-14.653 80.185-41.062 108.068-79.226 27.88-38.161 41.825-81.126 41.825-128.906-.01-39.771-9.818-76.454-29.414-110.049z"
|
||||
></path>
|
||||
</svg>
|
||||
GitHub
|
||||
</Button>
|
||||
<Button variant="outline">
|
||||
<svg role="img" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
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"
|
||||
/>
|
||||
</svg>
|
||||
Google
|
||||
</Button>
|
||||
</Field>
|
||||
<FieldSeparator className="*:data-[slot=field-separator-content]:bg-card">
|
||||
Or continue with
|
||||
</FieldSeparator>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="email-create-account">Email</FieldLabel>
|
||||
<Input
|
||||
id="email-create-account"
|
||||
type="email"
|
||||
placeholder="m@example.com"
|
||||
/>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="password-create-account">Password</FieldLabel>
|
||||
<Input id="password-create-account" type="password" />
|
||||
</Field>
|
||||
<Field>
|
||||
<Button>Create Account</Button>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button className="w-full">Create account</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,13 +5,21 @@ import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/new-york-v4/ui/card"
|
||||
import { Checkbox } from "@/registry/new-york-v4/ui/checkbox"
|
||||
import {
|
||||
Field,
|
||||
FieldContent,
|
||||
FieldDescription,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldLegend,
|
||||
FieldSet,
|
||||
FieldTitle,
|
||||
} from "@/registry/new-york-v4/ui/field"
|
||||
import { Input } from "@/registry/new-york-v4/ui/input"
|
||||
import { Label } from "@/registry/new-york-v4/ui/label"
|
||||
import {
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
@@ -22,7 +30,7 @@ const plans = [
|
||||
{
|
||||
id: "starter",
|
||||
name: "Starter Plan",
|
||||
description: "Perfect for small businesses.",
|
||||
description: "For small businesses.",
|
||||
price: "$10",
|
||||
},
|
||||
{
|
||||
@@ -37,91 +45,96 @@ export function CardsForms() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">Upgrade your subscription</CardTitle>
|
||||
<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>
|
||||
<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/20 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 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>
|
||||
<form>
|
||||
<FieldGroup>
|
||||
<FieldGroup className="grid grid-cols-2">
|
||||
<Field>
|
||||
<FieldLabel htmlFor="name">Name</FieldLabel>
|
||||
<Input id="name" placeholder="Max Leiter" />
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="email">Email</FieldLabel>
|
||||
<Input id="email" placeholder="mail@acme.com" />
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
<FieldGroup className="grid grid-cols-2 gap-3 md:grid-cols-[1fr_80px_60px]">
|
||||
<Field>
|
||||
<FieldLabel htmlFor="card-number">Card Number</FieldLabel>
|
||||
<Input
|
||||
id="card-number"
|
||||
placeholder="1234 1234 1234 1234"
|
||||
className="col-span-2 md:col-span-1"
|
||||
/>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="card-number-expiry">
|
||||
Expiry Date
|
||||
</FieldLabel>
|
||||
<Input id="card-number-expiry" placeholder="MM/YY" />
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="card-number-cvc">CVC</FieldLabel>
|
||||
<Input id="card-number-cvc" placeholder="CVC" />
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
<FieldSet>
|
||||
<FieldLegend>Plan</FieldLegend>
|
||||
<FieldDescription>
|
||||
Select the plan that best fits your needs.
|
||||
</FieldDescription>
|
||||
<RadioGroup
|
||||
defaultValue="starter"
|
||||
className="grid grid-cols-2 gap-2"
|
||||
>
|
||||
{plans.map((plan) => (
|
||||
<FieldLabel key={plan.id}>
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldTitle>{plan.name}</FieldTitle>
|
||||
<FieldDescription className="text-xs">
|
||||
{plan.description}
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<RadioGroupItem value={plan.id} id={plan.name} />
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</FieldSet>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="notes">Notes</FieldLabel>
|
||||
<Textarea id="notes" placeholder="Enter notes" />
|
||||
</Field>
|
||||
<Field>
|
||||
<Field orientation="horizontal">
|
||||
<Checkbox id="terms" />
|
||||
<FieldLabel htmlFor="terms" className="font-normal">
|
||||
I agree to the terms and conditions
|
||||
</FieldLabel>
|
||||
</Field>
|
||||
<Field orientation="horizontal">
|
||||
<Checkbox id="newsletter" defaultChecked />
|
||||
<FieldLabel htmlFor="newsletter" className="font-normal">
|
||||
Allow us to send you emails
|
||||
</FieldLabel>
|
||||
</Field>
|
||||
</Field>
|
||||
<Field orientation="horizontal">
|
||||
<Button variant="outline" size="sm">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button size="sm">Upgrade Plan</Button>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</form>
|
||||
</CardContent>
|
||||
<CardFooter className="flex justify-between">
|
||||
<Button variant="outline" size="sm">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button size="sm">Upgrade Plan</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -7,12 +7,11 @@ import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/new-york-v4/ui/card"
|
||||
import { Field, FieldGroup, FieldLabel } from "@/registry/new-york-v4/ui/field"
|
||||
import { Input } from "@/registry/new-york-v4/ui/input"
|
||||
import { Label } from "@/registry/new-york-v4/ui/label"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -33,65 +32,69 @@ export function CardsReportIssue() {
|
||||
What area are you having problems with?
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-6">
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<div className="flex flex-col gap-3">
|
||||
<Label htmlFor={`area-${id}`}>Area</Label>
|
||||
<Select defaultValue="billing">
|
||||
<SelectTrigger
|
||||
id={`area-${id}`}
|
||||
aria-label="Area"
|
||||
className="w-full"
|
||||
>
|
||||
<SelectValue placeholder="Select" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="team">Team</SelectItem>
|
||||
<SelectItem value="billing">Billing</SelectItem>
|
||||
<SelectItem value="account">Account</SelectItem>
|
||||
<SelectItem value="deployments">Deployments</SelectItem>
|
||||
<SelectItem value="support">Support</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<Label htmlFor={`security-level-${id}`}>Security Level</Label>
|
||||
<Select defaultValue="2">
|
||||
<SelectTrigger
|
||||
id={`security-level-${id}`}
|
||||
className="w-full [&_span]:!block [&_span]:truncate"
|
||||
aria-label="Security Level"
|
||||
>
|
||||
<SelectValue placeholder="Select level" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="1">Severity 1 (Highest)</SelectItem>
|
||||
<SelectItem value="2">Severity 2</SelectItem>
|
||||
<SelectItem value="3">Severity 3</SelectItem>
|
||||
<SelectItem value="4">Severity 4 (Lowest)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<Label htmlFor={`subject-${id}`}>Subject</Label>
|
||||
<Input id={`subject-${id}`} placeholder="I need help with..." />
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<Label htmlFor={`description-${id}`}>Description</Label>
|
||||
<Textarea
|
||||
id={`description-${id}`}
|
||||
placeholder="Please include all information relevant to your issue."
|
||||
className="min-h-28"
|
||||
/>
|
||||
</div>
|
||||
<CardContent>
|
||||
<FieldGroup>
|
||||
<FieldGroup className="grid gap-4 sm:grid-cols-2">
|
||||
<Field>
|
||||
<FieldLabel htmlFor={`area-${id}`}>Area</FieldLabel>
|
||||
<Select defaultValue="billing">
|
||||
<SelectTrigger
|
||||
id={`area-${id}`}
|
||||
aria-label="Area"
|
||||
className="w-full"
|
||||
>
|
||||
<SelectValue placeholder="Select" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="team">Team</SelectItem>
|
||||
<SelectItem value="billing">Billing</SelectItem>
|
||||
<SelectItem value="account">Account</SelectItem>
|
||||
<SelectItem value="deployments">Deployments</SelectItem>
|
||||
<SelectItem value="support">Support</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor={`security-level-${id}`}>
|
||||
Security Level
|
||||
</FieldLabel>
|
||||
<Select defaultValue="2">
|
||||
<SelectTrigger
|
||||
id={`security-level-${id}`}
|
||||
className="w-full [&_span]:!block [&_span]:truncate"
|
||||
aria-label="Security Level"
|
||||
>
|
||||
<SelectValue placeholder="Select level" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="1">Severity 1 (Highest)</SelectItem>
|
||||
<SelectItem value="2">Severity 2</SelectItem>
|
||||
<SelectItem value="3">Severity 3</SelectItem>
|
||||
<SelectItem value="4">Severity 4 (Lowest)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
<Field>
|
||||
<FieldLabel htmlFor={`subject-${id}`}>Subject</FieldLabel>
|
||||
<Input id={`subject-${id}`} placeholder="I need help with..." />
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor={`description-${id}`}>Description</FieldLabel>
|
||||
<Textarea
|
||||
id={`description-${id}`}
|
||||
placeholder="Please include all information relevant to your issue."
|
||||
className="min-h-24"
|
||||
/>
|
||||
</Field>
|
||||
<Field orientation="horizontal" className="justify-end">
|
||||
<Button variant="ghost" size="sm">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button size="sm">Submit</Button>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</CardContent>
|
||||
<CardFooter className="justify-end gap-2">
|
||||
<Button variant="ghost" size="sm">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button size="sm">Submit</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -14,6 +14,14 @@ import {
|
||||
CardTitle,
|
||||
} from "@/registry/new-york-v4/ui/card"
|
||||
import { Input } from "@/registry/new-york-v4/ui/input"
|
||||
import {
|
||||
Item,
|
||||
ItemActions,
|
||||
ItemContent,
|
||||
ItemDescription,
|
||||
ItemGroup,
|
||||
ItemTitle,
|
||||
} from "@/registry/new-york-v4/ui/item"
|
||||
import { Label } from "@/registry/new-york-v4/ui/label"
|
||||
import {
|
||||
Select,
|
||||
@@ -73,42 +81,35 @@ export function CardsShare() {
|
||||
<Separator className="my-4" />
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="text-sm font-medium">People with access</div>
|
||||
<div className="grid gap-6">
|
||||
<ItemGroup>
|
||||
{people.map((person) => (
|
||||
<div
|
||||
key={person.email}
|
||||
className="flex items-center justify-between gap-4"
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
<Avatar>
|
||||
<AvatarImage src={person.avatar} alt="Image" />
|
||||
<AvatarFallback>{person.name.charAt(0)}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div>
|
||||
<p className="text-sm leading-none font-medium">
|
||||
{person.name}
|
||||
</p>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{person.email}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Select defaultValue="edit">
|
||||
<SelectTrigger
|
||||
className="ml-auto pr-2"
|
||||
aria-label="Edit"
|
||||
size="sm"
|
||||
>
|
||||
<SelectValue placeholder="Select" />
|
||||
</SelectTrigger>
|
||||
<SelectContent align="end">
|
||||
<SelectItem value="edit">Can edit</SelectItem>
|
||||
<SelectItem value="view">Can view</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<Item key={person.email} className="px-0 py-2">
|
||||
<Avatar>
|
||||
<AvatarImage src={person.avatar} alt="Image" />
|
||||
<AvatarFallback>{person.name.charAt(0)}</AvatarFallback>
|
||||
</Avatar>
|
||||
<ItemContent>
|
||||
<ItemTitle>{person.name}</ItemTitle>
|
||||
<ItemDescription>{person.email}</ItemDescription>
|
||||
</ItemContent>
|
||||
<ItemActions>
|
||||
<Select defaultValue="edit">
|
||||
<SelectTrigger
|
||||
className="ml-auto pr-2"
|
||||
aria-label="Edit"
|
||||
size="sm"
|
||||
>
|
||||
<SelectValue placeholder="Select" />
|
||||
</SelectTrigger>
|
||||
<SelectContent align="end">
|
||||
<SelectItem value="edit">Can edit</SelectItem>
|
||||
<SelectItem value="view">Can view</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</ItemActions>
|
||||
</Item>
|
||||
))}
|
||||
</div>
|
||||
</ItemGroup>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -23,6 +23,13 @@ import {
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@/registry/new-york-v4/ui/command"
|
||||
import {
|
||||
Item,
|
||||
ItemActions,
|
||||
ItemContent,
|
||||
ItemDescription,
|
||||
ItemTitle,
|
||||
} from "@/registry/new-york-v4/ui/item"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
@@ -71,63 +78,58 @@ const roles = [
|
||||
|
||||
export function CardsTeamMembers() {
|
||||
return (
|
||||
<Card>
|
||||
<Card className="gap-4">
|
||||
<CardHeader>
|
||||
<CardTitle>Team Members</CardTitle>
|
||||
<CardDescription>
|
||||
Invite your team members to collaborate.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-6">
|
||||
<CardContent>
|
||||
{teamMembers.map((member) => (
|
||||
<div
|
||||
key={member.name}
|
||||
className="flex items-center justify-between gap-4"
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
<Avatar className="border">
|
||||
<AvatarImage src={member.avatar} alt="Image" />
|
||||
<AvatarFallback>{member.name.charAt(0)}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<p className="text-sm leading-none font-medium">
|
||||
{member.name}
|
||||
</p>
|
||||
<p className="text-muted-foreground text-xs">{member.email}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="ml-auto shadow-none"
|
||||
>
|
||||
{member.role} <ChevronDown />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0" align="end">
|
||||
<Command>
|
||||
<CommandInput placeholder="Select role..." />
|
||||
<CommandList>
|
||||
<CommandEmpty>No roles found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{roles.map((role) => (
|
||||
<CommandItem key={role.name}>
|
||||
<div className="flex flex-col">
|
||||
<p className="text-sm font-medium">{role.name}</p>
|
||||
<p className="text-muted-foreground">
|
||||
{role.description}
|
||||
</p>
|
||||
</div>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
<Item key={member.name} size="sm" className="gap-4 px-0">
|
||||
<Avatar className="shrink-0 self-start border">
|
||||
<AvatarImage src={member.avatar} alt="Image" />
|
||||
<AvatarFallback>{member.name.charAt(0)}</AvatarFallback>
|
||||
</Avatar>
|
||||
<ItemContent>
|
||||
<ItemTitle>{member.name}</ItemTitle>
|
||||
<ItemDescription>{member.email}</ItemDescription>
|
||||
</ItemContent>
|
||||
<ItemActions>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="ml-auto shadow-none"
|
||||
>
|
||||
{member.role} <ChevronDown />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0" align="end">
|
||||
<Command>
|
||||
<CommandInput placeholder="Select role..." />
|
||||
<CommandList>
|
||||
<CommandEmpty>No roles found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{roles.map((role) => (
|
||||
<CommandItem key={role.name}>
|
||||
<div className="flex flex-col">
|
||||
<p className="text-sm font-medium">{role.name}</p>
|
||||
<p className="text-muted-foreground">
|
||||
{role.description}
|
||||
</p>
|
||||
</div>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</ItemActions>
|
||||
</Item>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { CheckIcon, ClipboardIcon } from "lucide-react"
|
||||
import { IconCheck, IconCopy } from "@tabler/icons-react"
|
||||
|
||||
import { Event, trackEvent } from "@/lib/events"
|
||||
import { cn } from "@/lib/utils"
|
||||
@@ -54,7 +54,7 @@ export function ChartCopyButton({
|
||||
{...props}
|
||||
>
|
||||
<span className="sr-only">Copy</span>
|
||||
{hasCopied ? <CheckIcon /> : <ClipboardIcon />}
|
||||
{hasCopied ? <IconCheck /> : <IconCopy />}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="bg-black text-white">Copy code</TooltipContent>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { highlightCode } from "@/lib/highlight-code"
|
||||
import { getRegistryItem } from "@/lib/registry"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { ChartToolbar } from "@/components/chart-toolbar"
|
||||
import { type Style } from "@/registry/styles"
|
||||
|
||||
export type Chart = z.infer<typeof registryItemSchema> & {
|
||||
highlightedCode: string
|
||||
@@ -13,10 +14,14 @@ export type Chart = z.infer<typeof registryItemSchema> & {
|
||||
|
||||
export async function ChartDisplay({
|
||||
name,
|
||||
styleName,
|
||||
children,
|
||||
className,
|
||||
}: { name: string } & React.ComponentProps<"div">) {
|
||||
const chart = await getCachedRegistryItem(name)
|
||||
}: {
|
||||
name: string
|
||||
styleName: Style["name"]
|
||||
} & React.ComponentProps<"div">) {
|
||||
const chart = await getCachedRegistryItem(name, styleName)
|
||||
const highlightedCode = await getChartHighlightedCode(
|
||||
chart?.files?.[0]?.content ?? ""
|
||||
)
|
||||
@@ -45,9 +50,11 @@ export async function ChartDisplay({
|
||||
)
|
||||
}
|
||||
|
||||
const getCachedRegistryItem = React.cache(async (name: string) => {
|
||||
return await getRegistryItem(name)
|
||||
})
|
||||
const getCachedRegistryItem = React.cache(
|
||||
async (name: string, styleName: Style["name"]) => {
|
||||
return await getRegistryItem(name, styleName)
|
||||
}
|
||||
)
|
||||
|
||||
const getChartHighlightedCode = React.cache(async (content: string) => {
|
||||
return await highlightCode(content)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { CheckIcon, ClipboardIcon, TerminalIcon } from "lucide-react"
|
||||
import { IconCheck, IconCopy, IconTerminal } from "@tabler/icons-react"
|
||||
|
||||
import { useConfig } from "@/hooks/use-config"
|
||||
import { copyToClipboardWithMeta } from "@/components/copy-button"
|
||||
@@ -80,7 +80,7 @@ export function CodeBlockCommand({
|
||||
>
|
||||
<div className="border-border/50 flex items-center gap-2 border-b px-3 py-1">
|
||||
<div className="bg-foreground flex size-4 items-center justify-center rounded-[1px] opacity-70">
|
||||
<TerminalIcon className="text-code size-3" />
|
||||
<IconTerminal className="text-code size-3" />
|
||||
</div>
|
||||
<TabsList className="rounded-none bg-transparent p-0">
|
||||
{Object.entries(tabs).map(([key]) => {
|
||||
@@ -123,7 +123,7 @@ export function CodeBlockCommand({
|
||||
onClick={copyCommand}
|
||||
>
|
||||
<span className="sr-only">Copy</span>
|
||||
{hasCopied ? <CheckIcon /> : <ClipboardIcon />}
|
||||
{hasCopied ? <IconCheck /> : <IconCopy />}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
|
||||
@@ -4,14 +4,15 @@ import * as React from "react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { type DialogProps } from "@radix-ui/react-dialog"
|
||||
import { IconArrowRight } from "@tabler/icons-react"
|
||||
import { useDocsSearch } from "fumadocs-core/search/client"
|
||||
import { CornerDownLeftIcon, SquareDashedIcon } from "lucide-react"
|
||||
|
||||
import { type Color, type ColorPalette } from "@/lib/colors"
|
||||
import { trackEvent } from "@/lib/events"
|
||||
import { showMcpDocs } from "@/lib/flags"
|
||||
import { source } from "@/lib/source"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { useConfig } from "@/hooks/use-config"
|
||||
import { useIsMac } from "@/hooks/use-is-mac"
|
||||
import { useMutationObserver } from "@/hooks/use-mutation-observer"
|
||||
import { copyToClipboardWithMeta } from "@/components/copy-button"
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
@@ -31,7 +32,9 @@ import {
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/registry/new-york-v4/ui/dialog"
|
||||
import { Kbd, KbdGroup } from "@/registry/new-york-v4/ui/kbd"
|
||||
import { Separator } from "@/registry/new-york-v4/ui/separator"
|
||||
import { Spinner } from "@/registry/new-york-v4/ui/spinner"
|
||||
|
||||
export function CommandMenu({
|
||||
tree,
|
||||
@@ -46,15 +49,63 @@ export function CommandMenu({
|
||||
navItems?: { href: string; label: string }[]
|
||||
}) {
|
||||
const router = useRouter()
|
||||
const isMac = useIsMac()
|
||||
const [config] = useConfig()
|
||||
const [open, setOpen] = React.useState(false)
|
||||
const [selectedType, setSelectedType] = React.useState<
|
||||
"color" | "page" | "component" | "block" | null
|
||||
>(null)
|
||||
const [copyPayload, setCopyPayload] = React.useState("")
|
||||
|
||||
const { search, setSearch, query } = useDocsSearch({
|
||||
type: "fetch",
|
||||
})
|
||||
const packageManager = config.packageManager || "pnpm"
|
||||
|
||||
// Track search queries with debouncing to avoid excessive tracking.
|
||||
const searchTimeoutRef = React.useRef<NodeJS.Timeout | undefined>(undefined)
|
||||
const lastTrackedQueryRef = React.useRef<string>("")
|
||||
|
||||
const trackSearchQuery = React.useCallback((query: string) => {
|
||||
const trimmedQuery = query.trim()
|
||||
|
||||
// Only track if the query is different from the last tracked query and has content.
|
||||
if (trimmedQuery && trimmedQuery !== lastTrackedQueryRef.current) {
|
||||
lastTrackedQueryRef.current = trimmedQuery
|
||||
trackEvent({
|
||||
name: "search_query",
|
||||
properties: {
|
||||
query: trimmedQuery,
|
||||
query_length: trimmedQuery.length,
|
||||
},
|
||||
})
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleSearchChange = React.useCallback(
|
||||
(value: string) => {
|
||||
// Clear existing timeout.
|
||||
if (searchTimeoutRef.current) {
|
||||
clearTimeout(searchTimeoutRef.current)
|
||||
}
|
||||
|
||||
// Set new timeout to debounce both search and tracking.
|
||||
searchTimeoutRef.current = setTimeout(() => {
|
||||
setSearch(value)
|
||||
trackSearchQuery(value)
|
||||
}, 500)
|
||||
},
|
||||
[setSearch, trackSearchQuery]
|
||||
)
|
||||
|
||||
// Cleanup timeout on unmount.
|
||||
React.useEffect(() => {
|
||||
return () => {
|
||||
if (searchTimeoutRef.current) {
|
||||
clearTimeout(searchTimeoutRef.current)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handlePageHighlight = React.useCallback(
|
||||
(isComponent: boolean, item: { url: string; name?: React.ReactNode }) => {
|
||||
if (isComponent) {
|
||||
@@ -144,7 +195,7 @@ export function CommandMenu({
|
||||
<Button
|
||||
variant="secondary"
|
||||
className={cn(
|
||||
"bg-surface text-surface-foreground/60 dark:bg-card relative h-8 w-full justify-start pl-2.5 font-normal shadow-none sm:pr-12 md:w-40 lg:w-56 xl:w-64"
|
||||
"bg-surface text-foreground dark:bg-card relative h-8 w-full justify-start pl-3 font-medium shadow-none sm:pr-12 md:w-48 lg:w-56 xl:w-64"
|
||||
)}
|
||||
onClick={() => setOpen(true)}
|
||||
{...props}
|
||||
@@ -152,8 +203,10 @@ export function CommandMenu({
|
||||
<span className="hidden lg:inline-flex">Search documentation...</span>
|
||||
<span className="inline-flex lg:hidden">Search...</span>
|
||||
<div className="absolute top-1.5 right-1.5 hidden gap-1 sm:flex">
|
||||
<CommandMenuKbd>{isMac ? "⌘" : "Ctrl"}</CommandMenuKbd>
|
||||
<CommandMenuKbd className="aspect-square">K</CommandMenuKbd>
|
||||
<KbdGroup>
|
||||
<Kbd className="border">⌘</Kbd>
|
||||
<Kbd className="border">K</Kbd>
|
||||
</KbdGroup>
|
||||
</div>
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
@@ -168,6 +221,7 @@ export function CommandMenu({
|
||||
<Command
|
||||
className="**:data-[slot=command-input-wrapper]:bg-input/50 **:data-[slot=command-input-wrapper]:border-input rounded-none bg-transparent **:data-[slot=command-input]:!h-9 **:data-[slot=command-input]:py-0 **:data-[slot=command-input-wrapper]:mb-0 **:data-[slot=command-input-wrapper]:!h-9 **:data-[slot=command-input-wrapper]:rounded-md **:data-[slot=command-input-wrapper]:border"
|
||||
filter={(value, search, keywords) => {
|
||||
handleSearchChange(search)
|
||||
const extendValue = value + " " + (keywords?.join(" ") || "")
|
||||
if (extendValue.toLowerCase().includes(search.toLowerCase())) {
|
||||
return 1
|
||||
@@ -175,10 +229,17 @@ export function CommandMenu({
|
||||
return 0
|
||||
}}
|
||||
>
|
||||
<CommandInput placeholder="Search documentation..." />
|
||||
<div className="relative">
|
||||
<CommandInput placeholder="Search documentation..." />
|
||||
{query.isLoading && (
|
||||
<div className="pointer-events-none absolute top-1/2 right-3 z-10 flex -translate-y-1/2 items-center justify-center">
|
||||
<Spinner className="text-muted-foreground size-4" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<CommandList className="no-scrollbar min-h-80 scroll-pt-2 scroll-pb-1.5">
|
||||
<CommandEmpty className="text-muted-foreground py-12 text-center text-sm">
|
||||
No results found.
|
||||
{query.isLoading ? "Searching..." : "No results found."}
|
||||
</CommandEmpty>
|
||||
{navItems && navItems.length > 0 && (
|
||||
<CommandGroup
|
||||
@@ -319,6 +380,12 @@ export function CommandMenu({
|
||||
))}
|
||||
</CommandGroup>
|
||||
) : null}
|
||||
<SearchResults
|
||||
open={open}
|
||||
setOpen={setOpen}
|
||||
query={query}
|
||||
search={search}
|
||||
/>
|
||||
</CommandList>
|
||||
</Command>
|
||||
<div className="text-muted-foreground absolute inset-x-0 bottom-0 z-20 flex h-10 items-center gap-2 rounded-b-xl border-t border-t-neutral-100 bg-neutral-50 px-4 text-xs font-medium dark:border-t-neutral-700 dark:bg-neutral-800">
|
||||
@@ -335,7 +402,7 @@ export function CommandMenu({
|
||||
<>
|
||||
<Separator orientation="vertical" className="!h-4" />
|
||||
<div className="flex items-center gap-1">
|
||||
<CommandMenuKbd>{isMac ? "⌘" : "Ctrl"}</CommandMenuKbd>
|
||||
<CommandMenuKbd>⌘</CommandMenuKbd>
|
||||
<CommandMenuKbd>C</CommandMenuKbd>
|
||||
{copyPayload}
|
||||
</div>
|
||||
@@ -396,3 +463,66 @@ function CommandMenuKbd({ className, ...props }: React.ComponentProps<"kbd">) {
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
type Query = Awaited<ReturnType<typeof useDocsSearch>>["query"]
|
||||
|
||||
function SearchResults({
|
||||
setOpen,
|
||||
query,
|
||||
search,
|
||||
}: {
|
||||
open: boolean
|
||||
setOpen: (open: boolean) => void
|
||||
query: Query
|
||||
search: string
|
||||
}) {
|
||||
const router = useRouter()
|
||||
|
||||
const uniqueResults =
|
||||
query.data && Array.isArray(query.data)
|
||||
? query.data.filter(
|
||||
(item, index, self) =>
|
||||
!(
|
||||
item.type === "text" &&
|
||||
item.content.trim().split(/\s+/).length <= 1
|
||||
) && index === self.findIndex((t) => t.content === item.content)
|
||||
)
|
||||
: []
|
||||
|
||||
if (!search.trim()) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (!query.data || query.data === "empty") {
|
||||
return null
|
||||
}
|
||||
|
||||
if (query.data && uniqueResults.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<CommandGroup
|
||||
className="!px-0 [&_[cmdk-group-heading]]:scroll-mt-16 [&_[cmdk-group-heading]]:!p-3 [&_[cmdk-group-heading]]:!pb-1"
|
||||
heading="Search Results"
|
||||
>
|
||||
{uniqueResults.map((item) => {
|
||||
return (
|
||||
<CommandItem
|
||||
key={item.id}
|
||||
data-type={item.type}
|
||||
onSelect={() => {
|
||||
router.push(item.url)
|
||||
setOpen(false)
|
||||
}}
|
||||
className="data-[selected=true]:border-input data-[selected=true]:bg-input/50 h-9 rounded-md border border-transparent !px-3 font-normal"
|
||||
keywords={[item.content]}
|
||||
value={`${item.content} ${item.type}`}
|
||||
>
|
||||
<div className="line-clamp-1 text-sm">{item.content}</div>
|
||||
</CommandItem>
|
||||
)
|
||||
})}
|
||||
</CommandGroup>
|
||||
)
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user