mirror of
https://github.com/shadcn-ui/ui.git
synced 2026-06-22 04:05:48 +00:00
Compare commits
91 Commits
shadcn/tes
...
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 |
@@ -7,5 +7,5 @@
|
||||
"access": "public",
|
||||
"baseBranch": "main",
|
||||
"updateInternalDependencies": "patch",
|
||||
"ignore": ["www", "v4", "tests"]
|
||||
"ignore": ["v4", "tests"]
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"shadcn": patch
|
||||
---
|
||||
|
||||
Fix support for universal registry items that only have dependencies without files
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"shadcn": patch
|
||||
---
|
||||
|
||||
add support for color as var
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"shadcn": patch
|
||||
---
|
||||
|
||||
fix adding registry item with CSS at-property
|
||||
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
|
||||
9
.github/workflows/deprecated.yml
vendored
9
.github/workflows/deprecated.yml
vendored
@@ -1,13 +1,14 @@
|
||||
name: Deprecated
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
pull_request_target:
|
||||
types: [opened, synchronize]
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
deprecated:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -15,11 +16,12 @@ jobs:
|
||||
- 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@v44
|
||||
uses: tj-actions/changed-files@v46
|
||||
with:
|
||||
files: |
|
||||
apps/www/**
|
||||
@@ -37,7 +39,8 @@ jobs:
|
||||
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.startsWith('apps/www/public/r/') &&
|
||||
file !== 'apps/www/package.json'
|
||||
);
|
||||
|
||||
if (wwwFiles.length > 0) {
|
||||
|
||||
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
|
||||
|
||||
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@@ -10,6 +10,6 @@
|
||||
"**/fixtures/**"
|
||||
],
|
||||
"files.exclude": {
|
||||
"apps/www": true
|
||||
"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.
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { IconMinus, IconPlus } from "@tabler/icons-react"
|
||||
import { CheckIcon } from "lucide-react"
|
||||
|
||||
import { useThemeConfig } from "@/components/active-theme"
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import { ButtonGroup } from "@/registry/new-york-v4/ui/button-group"
|
||||
import {
|
||||
@@ -18,34 +17,31 @@ import {
|
||||
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,
|
||||
} from "@/registry/new-york-v4/ui/radio-group"
|
||||
import { Switch } from "@/registry/new-york-v4/ui/switch"
|
||||
|
||||
const accents = [
|
||||
{
|
||||
name: "Blue",
|
||||
value: "blue",
|
||||
},
|
||||
{
|
||||
name: "Amber",
|
||||
value: "amber",
|
||||
},
|
||||
{
|
||||
name: "Green",
|
||||
value: "green",
|
||||
},
|
||||
{
|
||||
name: "Rose",
|
||||
value: "rose",
|
||||
},
|
||||
]
|
||||
|
||||
export function AppearanceSettings() {
|
||||
const { activeTheme, setActiveTheme } = useThemeConfig()
|
||||
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>
|
||||
@@ -90,37 +86,6 @@ export function AppearanceSettings() {
|
||||
</RadioGroup>
|
||||
</FieldSet>
|
||||
<FieldSeparator />
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldTitle>Accent</FieldTitle>
|
||||
<FieldDescription>Select the accent color.</FieldDescription>
|
||||
</FieldContent>
|
||||
<FieldSet aria-label="Accent">
|
||||
<RadioGroup
|
||||
className="flex flex-wrap gap-2"
|
||||
value={activeTheme}
|
||||
onValueChange={setActiveTheme}
|
||||
>
|
||||
{accents.map((accent) => (
|
||||
<Label
|
||||
htmlFor={accent.value}
|
||||
key={accent.value}
|
||||
data-theme={accent.value}
|
||||
className="flex size-6 items-center justify-center rounded-full data-[theme=amber]:bg-amber-600 data-[theme=blue]:bg-blue-700 data-[theme=green]:bg-green-600 data-[theme=rose]:bg-rose-600"
|
||||
>
|
||||
<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="horizontal">
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor="number-of-gpus-f6l">Number of GPUs</FieldLabel>
|
||||
@@ -129,7 +94,8 @@ export function AppearanceSettings() {
|
||||
<ButtonGroup>
|
||||
<Input
|
||||
id="number-of-gpus-f6l"
|
||||
placeholder="8"
|
||||
value={gpuCount}
|
||||
onChange={handleGpuInputChange}
|
||||
size={3}
|
||||
className="h-8 !w-14 font-mono"
|
||||
maxLength={3}
|
||||
@@ -139,6 +105,8 @@ export function AppearanceSettings() {
|
||||
size="icon-sm"
|
||||
type="button"
|
||||
aria-label="Decrement"
|
||||
onClick={() => handleGpuAdjustment(-1)}
|
||||
disabled={gpuCount <= 1}
|
||||
>
|
||||
<IconMinus />
|
||||
</Button>
|
||||
@@ -147,6 +115,8 @@ export function AppearanceSettings() {
|
||||
size="icon-sm"
|
||||
type="button"
|
||||
aria-label="Increment"
|
||||
onClick={() => handleGpuAdjustment(1)}
|
||||
disabled={gpuCount >= 99}
|
||||
>
|
||||
<IconPlus />
|
||||
</Button>
|
||||
|
||||
@@ -33,7 +33,7 @@ export function RootComponents() {
|
||||
<div className="flex flex-col gap-6 *:[div]:w-full *:[div]:max-w-full">
|
||||
<InputGroupButtonExample />
|
||||
<ItemDemo />
|
||||
<FieldSeparator>Appearance Settings</FieldSeparator>
|
||||
<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">
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Spinner } from "@/registry/new-york-v4/ui/spinner"
|
||||
|
||||
export function SpinnerBadge() {
|
||||
return (
|
||||
<div className="flex items-center gap-2 [--radius:1.2rem]">
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge>
|
||||
<Spinner />
|
||||
Syncing
|
||||
|
||||
@@ -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"
|
||||
@@ -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>
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -3,22 +3,26 @@ 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.
|
||||
const processedContent = processMdxForLLMs(page.data.content)
|
||||
const processedContent = processMdxForLLMs(
|
||||
await page.data.getText("raw"),
|
||||
activeStyle.name
|
||||
)
|
||||
|
||||
return new NextResponse(processedContent, {
|
||||
headers: {
|
||||
|
||||
@@ -36,6 +36,7 @@ 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"
|
||||
@@ -279,6 +280,13 @@ export const componentRegistry: Record<string, ComponentConfig> = {
|
||||
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,
|
||||
|
||||
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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
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,7 +84,7 @@ export default function RootLayout({
|
||||
</head>
|
||||
<body
|
||||
className={cn(
|
||||
"text-foreground group/body theme-blue 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
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -5,7 +5,7 @@ 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">
|
||||
<span className="flex size-2 rounded-full bg-blue-500" title="New" />
|
||||
New Components: Field, Input Group, Item and more <ArrowRightIcon />
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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"
|
||||
@@ -33,6 +34,7 @@ import {
|
||||
} 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,
|
||||
@@ -47,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) {
|
||||
@@ -154,7 +204,7 @@ export function CommandMenu({
|
||||
<span className="inline-flex lg:hidden">Search...</span>
|
||||
<div className="absolute top-1.5 right-1.5 hidden gap-1 sm:flex">
|
||||
<KbdGroup>
|
||||
<Kbd className="border">{isMac ? "⌘" : "Ctrl"}</Kbd>
|
||||
<Kbd className="border">⌘</Kbd>
|
||||
<Kbd className="border">K</Kbd>
|
||||
</KbdGroup>
|
||||
</div>
|
||||
@@ -171,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
|
||||
@@ -178,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
|
||||
@@ -322,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">
|
||||
@@ -338,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>
|
||||
@@ -399,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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Tabs, TabsList, TabsTrigger } from "@/registry/new-york-v4/ui/tabs"
|
||||
|
||||
export function ComponentPreviewTabs({
|
||||
className,
|
||||
@@ -20,64 +19,32 @@ export function ComponentPreviewTabs({
|
||||
component: React.ReactNode
|
||||
source: React.ReactNode
|
||||
}) {
|
||||
const [tab, setTab] = React.useState("preview")
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn("group relative mt-4 mb-12 flex flex-col gap-2", className)}
|
||||
className={cn(
|
||||
"group relative mt-4 mb-12 flex flex-col gap-2 rounded-lg border",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<Tabs
|
||||
className="relative mr-auto w-full"
|
||||
value={tab}
|
||||
onValueChange={setTab}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
{!hideCode && (
|
||||
<TabsList className="justify-start gap-4 rounded-none bg-transparent px-2 md:px-0">
|
||||
<TabsTrigger
|
||||
value="preview"
|
||||
className="text-muted-foreground data-[state=active]:text-foreground px-0 text-base data-[state=active]:shadow-none dark:data-[state=active]:border-transparent dark:data-[state=active]:bg-transparent"
|
||||
>
|
||||
Preview
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="code"
|
||||
className="text-muted-foreground data-[state=active]:text-foreground px-0 text-base data-[state=active]:shadow-none dark:data-[state=active]:border-transparent dark:data-[state=active]:bg-transparent"
|
||||
>
|
||||
Code
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<div data-slot="preview">
|
||||
<div
|
||||
data-align={align}
|
||||
className={cn(
|
||||
"preview flex w-full justify-center data-[align=center]:items-center data-[align=end]:items-end data-[align=start]:items-start",
|
||||
chromeLessOnMobile ? "sm:p-10" : "h-[450px] p-10"
|
||||
)}
|
||||
</div>
|
||||
</Tabs>
|
||||
<div
|
||||
data-tab={tab}
|
||||
data-chrome-less-on-mobile={chromeLessOnMobile}
|
||||
className="data-[tab=code]:border-code relative rounded-lg border data-[chrome-less-on-mobile=true]:border-0 sm:data-[chrome-less-on-mobile=true]:border md:-mx-1"
|
||||
>
|
||||
<div
|
||||
data-slot="preview"
|
||||
data-active={tab === "preview"}
|
||||
className="invisible data-[active=true]:visible"
|
||||
>
|
||||
{component}
|
||||
</div>
|
||||
{!hideCode && (
|
||||
<div
|
||||
data-align={align}
|
||||
className={cn(
|
||||
"preview flex w-full justify-center data-[align=center]:items-center data-[align=end]:items-end data-[align=start]:items-start",
|
||||
chromeLessOnMobile ? "sm:p-10" : "h-[450px] p-10"
|
||||
)}
|
||||
data-slot="code"
|
||||
className="overflow-hidden [&_[data-rehype-pretty-code-figure]]:!m-0 [&_[data-rehype-pretty-code-figure]]:rounded-t-none [&_[data-rehype-pretty-code-figure]]:border-t [&_pre]:max-h-[400px]"
|
||||
>
|
||||
{component}
|
||||
{source}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
data-slot="code"
|
||||
data-active={tab === "code"}
|
||||
className="absolute inset-0 hidden overflow-hidden data-[active=true]:block **:[figure]:!m-0 **:[pre]:h-[450px]"
|
||||
>
|
||||
{source}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -3,9 +3,11 @@ import Image from "next/image"
|
||||
import { ComponentPreviewTabs } from "@/components/component-preview-tabs"
|
||||
import { ComponentSource } from "@/components/component-source"
|
||||
import { Index } from "@/registry/__index__"
|
||||
import { type Style } from "@/registry/styles"
|
||||
|
||||
export function ComponentPreview({
|
||||
name,
|
||||
styleName = "new-york-v4",
|
||||
type,
|
||||
className,
|
||||
align = "center",
|
||||
@@ -14,13 +16,14 @@ export function ComponentPreview({
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & {
|
||||
name: string
|
||||
styleName?: Style["name"]
|
||||
align?: "center" | "start" | "end"
|
||||
description?: string
|
||||
hideCode?: boolean
|
||||
type?: "block" | "component" | "example"
|
||||
chromeLessOnMobile?: boolean
|
||||
}) {
|
||||
const Component = Index[name]?.component
|
||||
const Component = Index[styleName]?.[name]?.component
|
||||
|
||||
if (!Component) {
|
||||
return (
|
||||
@@ -52,7 +55,7 @@ export function ComponentPreview({
|
||||
className="bg-background absolute top-0 left-0 z-20 hidden w-[970px] max-w-none sm:w-[1280px] md:hidden dark:block md:dark:hidden"
|
||||
/>
|
||||
<div className="bg-background absolute inset-0 hidden w-[1600px] md:block">
|
||||
<iframe src={`/view/${name}`} className="size-full" />
|
||||
<iframe src={`/view/${styleName}/${name}`} className="size-full" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -64,7 +67,13 @@ export function ComponentPreview({
|
||||
align={align}
|
||||
hideCode={hideCode}
|
||||
component={<Component />}
|
||||
source={<ComponentSource name={name} collapsible={false} />}
|
||||
source={
|
||||
<ComponentSource
|
||||
name={name}
|
||||
collapsible={false}
|
||||
styleName={styleName}
|
||||
/>
|
||||
}
|
||||
chromeLessOnMobile={chromeLessOnMobile}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
@@ -8,6 +8,7 @@ import { cn } from "@/lib/utils"
|
||||
import { CodeCollapsibleWrapper } from "@/components/code-collapsible-wrapper"
|
||||
import { CopyButton } from "@/components/copy-button"
|
||||
import { getIconForLanguageExtension } from "@/components/icons"
|
||||
import { type Style } from "@/registry/styles"
|
||||
|
||||
export async function ComponentSource({
|
||||
name,
|
||||
@@ -16,12 +17,14 @@ export async function ComponentSource({
|
||||
language,
|
||||
collapsible = true,
|
||||
className,
|
||||
styleName = "new-york-v4",
|
||||
}: React.ComponentProps<"div"> & {
|
||||
name?: string
|
||||
src?: string
|
||||
title?: string
|
||||
language?: string
|
||||
collapsible?: boolean
|
||||
styleName?: Style["name"]
|
||||
}) {
|
||||
if (!name && !src) {
|
||||
return null
|
||||
@@ -30,7 +33,7 @@ export async function ComponentSource({
|
||||
let code: string | undefined
|
||||
|
||||
if (name) {
|
||||
const item = await getRegistryItem(name)
|
||||
const item = await getRegistryItem(name, styleName)
|
||||
code = item?.files?.[0]?.content
|
||||
}
|
||||
|
||||
@@ -44,8 +47,8 @@ export async function ComponentSource({
|
||||
}
|
||||
|
||||
// Fix imports.
|
||||
// Replace @/registry/new-york-v4/ with @/components/.
|
||||
code = code.replaceAll("@/registry/new-york-v4/", "@/components/")
|
||||
// Replace @/registry/${style}/ with @/components/.
|
||||
code = code.replaceAll(`@/registry/${styleName}/`, "@/components/")
|
||||
|
||||
// Replace export default with export.
|
||||
code = code.replaceAll("export default", "export")
|
||||
|
||||
@@ -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"
|
||||
@@ -24,11 +24,13 @@ export function CopyButton({
|
||||
className,
|
||||
variant = "ghost",
|
||||
event,
|
||||
tooltip = "Copy to Clipboard",
|
||||
...props
|
||||
}: React.ComponentProps<typeof Button> & {
|
||||
value: string
|
||||
src?: string
|
||||
event?: Event["name"]
|
||||
tooltip?: string
|
||||
}) {
|
||||
const [hasCopied, setHasCopied] = React.useState(false)
|
||||
|
||||
@@ -43,6 +45,7 @@ export function CopyButton({
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
data-slot="copy-button"
|
||||
data-copied={hasCopied}
|
||||
size="icon"
|
||||
variant={variant}
|
||||
className={cn(
|
||||
@@ -66,12 +69,10 @@ export function CopyButton({
|
||||
{...props}
|
||||
>
|
||||
<span className="sr-only">Copy</span>
|
||||
{hasCopied ? <CheckIcon /> : <ClipboardIcon />}
|
||||
{hasCopied ? <IconCheck /> : <IconCopy />}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{hasCopied ? "Copied" : "Copy to Clipboard"}
|
||||
</TooltipContent>
|
||||
<TooltipContent>{hasCopied ? "Copied" : tooltip}</TooltipContent>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
157
apps/v4/components/directory-add-button.tsx
Normal file
157
apps/v4/components/directory-add-button.tsx
Normal file
@@ -0,0 +1,157 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import Link from "next/link"
|
||||
import { IconCheck } from "@tabler/icons-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { useCopyToClipboard } from "@/hooks/use-copy-to-clipboard"
|
||||
import { useIsMobile } from "@/hooks/use-mobile"
|
||||
import { CopyButton } from "@/components/copy-button"
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/registry/new-york-v4/ui/dialog"
|
||||
import {
|
||||
Drawer,
|
||||
DrawerClose,
|
||||
DrawerContent,
|
||||
DrawerDescription,
|
||||
DrawerFooter,
|
||||
DrawerHeader,
|
||||
DrawerTitle,
|
||||
DrawerTrigger,
|
||||
} from "@/registry/new-york-v4/ui/drawer"
|
||||
|
||||
export function DirectoryAddButton({
|
||||
registry,
|
||||
}: {
|
||||
registry: {
|
||||
name: string
|
||||
url: string
|
||||
}
|
||||
}) {
|
||||
const { copyToClipboard, isCopied } = useCopyToClipboard()
|
||||
const isMobile = useIsMobile()
|
||||
const [open, setOpen] = React.useState(false)
|
||||
|
||||
const jsonValue = `{
|
||||
"registries": {
|
||||
"${registry.name}": "${registry.url}"
|
||||
}
|
||||
}`
|
||||
|
||||
const Trigger = (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="relative z-10"
|
||||
onClick={() => setOpen(true)}
|
||||
>
|
||||
{isCopied ? (
|
||||
<IconCheck />
|
||||
) : (
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>Model Context Protocol</title>
|
||||
<path
|
||||
d="M13.85 0a4.16 4.16 0 0 0-2.95 1.217L1.456 10.66a.835.835 0 0 0 0 1.18.835.835 0 0 0 1.18 0l9.442-9.442a2.49 2.49 0 0 1 3.541 0 2.49 2.49 0 0 1 0 3.541L8.59 12.97l-.1.1a.835.835 0 0 0 0 1.18.835.835 0 0 0 1.18 0l.1-.098 7.03-7.034a2.49 2.49 0 0 1 3.542 0l.049.05a2.49 2.49 0 0 1 0 3.54l-8.54 8.54a1.96 1.96 0 0 0 0 2.755l1.753 1.753a.835.835 0 0 0 1.18 0 .835.835 0 0 0 0-1.18l-1.753-1.753a.266.266 0 0 1 0-.394l8.54-8.54a4.185 4.185 0 0 0 0-5.9l-.05-.05a4.16 4.16 0 0 0-2.95-1.218c-.2 0-.401.02-.6.048a4.17 4.17 0 0 0-1.17-3.552A4.16 4.16 0 0 0 13.85 0m0 3.333a.84.84 0 0 0-.59.245L6.275 10.56a4.186 4.186 0 0 0 0 5.902 4.186 4.186 0 0 0 5.902 0L19.16 9.48a.835.835 0 0 0 0-1.18.835.835 0 0 0-1.18 0l-6.985 6.984a2.49 2.49 0 0 1-3.54 0 2.49 2.49 0 0 1 0-3.54l6.983-6.985a.835.835 0 0 0 0-1.18.84.84 0 0 0-.59-.245"
|
||||
className="fill-foreground"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
MCP
|
||||
</Button>
|
||||
)
|
||||
|
||||
const Content = (
|
||||
<>
|
||||
<figure
|
||||
data-rehype-pretty-code-figure
|
||||
className={cn(
|
||||
"group relative mt-0",
|
||||
!isMobile &&
|
||||
"dark:bg-background dark:[&_[data-line]:not([data-highlighted-line]):before]:bg-background!"
|
||||
)}
|
||||
>
|
||||
<CopyButton
|
||||
value={jsonValue}
|
||||
className="top-3 right-2"
|
||||
tooltip="Copy Code"
|
||||
/>
|
||||
<div data-rehype-pretty-code-title>components.json</div>
|
||||
<pre className="no-scrollbar min-w-0 overflow-x-auto px-4 py-3.5 outline-none has-[[data-highlighted-line]]:px-0 has-[[data-line-numbers]]:px-0 has-[[data-slot=tabs]]:p-0">
|
||||
<code data-line-numbers data-language="json">
|
||||
<span data-line>{"{"}</span>
|
||||
<span data-line>{' "registries": {'}</span>
|
||||
<span
|
||||
data-line
|
||||
data-highlighted-line
|
||||
>{` "${registry.name}": "${registry.url}"`}</span>
|
||||
<span data-line>{" }"}</span>
|
||||
<span data-line>{"}"}</span>
|
||||
</code>
|
||||
</pre>
|
||||
</figure>
|
||||
</>
|
||||
)
|
||||
|
||||
if (isMobile) {
|
||||
return (
|
||||
<Drawer open={open} onOpenChange={setOpen}>
|
||||
<DrawerTrigger asChild>{Trigger}</DrawerTrigger>
|
||||
<DrawerContent>
|
||||
<DrawerHeader>
|
||||
<DrawerTitle>Configure MCP</DrawerTitle>
|
||||
<DrawerDescription>
|
||||
Copy and paste the following code into your project's
|
||||
components.json.
|
||||
</DrawerDescription>
|
||||
</DrawerHeader>
|
||||
<div className="px-6">{Content}</div>
|
||||
<DrawerFooter>
|
||||
<DrawerClose asChild>
|
||||
<Button size="sm">Close</Button>
|
||||
</DrawerClose>
|
||||
<Button size="sm" asChild variant="outline">
|
||||
<Link href="/docs/mcp">Read the docs</Link>
|
||||
</Button>
|
||||
</DrawerFooter>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>{Trigger}</DialogTrigger>
|
||||
<DialogContent
|
||||
className="rounded-xl border-none bg-clip-padding shadow-2xl ring-4 ring-neutral-200/80 sm:max-w-[600px] dark:bg-neutral-900 dark:ring-neutral-800"
|
||||
onOpenAutoFocus={(e) => e.preventDefault()}
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Configure MCP</DialogTitle>
|
||||
<DialogDescription>
|
||||
Copy and paste the following code into your project's
|
||||
components.json.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{Content}
|
||||
<DialogFooter className="justify-between!">
|
||||
<Button size="sm" asChild variant="ghost">
|
||||
<Link href="/docs/mcp">Read the docs</Link>
|
||||
</Button>
|
||||
<DialogClose asChild>
|
||||
<Button size="sm">Done</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
70
apps/v4/components/directory-list.tsx
Normal file
70
apps/v4/components/directory-list.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import * as React from "react"
|
||||
import { IconArrowUpRight } from "@tabler/icons-react"
|
||||
|
||||
import { DirectoryAddButton } from "@/components/directory-add-button"
|
||||
import registries from "@/registry/directory.json"
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import {
|
||||
Item,
|
||||
ItemActions,
|
||||
ItemContent,
|
||||
ItemDescription,
|
||||
ItemFooter,
|
||||
ItemGroup,
|
||||
ItemMedia,
|
||||
ItemSeparator,
|
||||
ItemTitle,
|
||||
} from "@/registry/new-york-v4/ui/item"
|
||||
|
||||
export function DirectoryList() {
|
||||
return (
|
||||
<ItemGroup className="my-8">
|
||||
{registries.map((registry, index) => (
|
||||
<React.Fragment key={index}>
|
||||
<Item className="group/item relative gap-6 px-0 sm:px-4">
|
||||
<ItemMedia
|
||||
variant="image"
|
||||
dangerouslySetInnerHTML={{ __html: registry.logo }}
|
||||
className="*:[svg]:fill-foreground grayscale *:[svg]:size-8"
|
||||
/>
|
||||
<ItemContent>
|
||||
<ItemTitle>
|
||||
<a
|
||||
href={registry.homepage}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{registry.name}
|
||||
</a>
|
||||
</ItemTitle>
|
||||
{registry.description && (
|
||||
<ItemDescription className="text-pretty">
|
||||
{registry.description}
|
||||
</ItemDescription>
|
||||
)}
|
||||
</ItemContent>
|
||||
<ItemActions className="relative z-10 hidden self-start sm:flex">
|
||||
<Button size="sm" variant="outline" asChild>
|
||||
<a
|
||||
href={registry.homepage}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
View <IconArrowUpRight />
|
||||
</a>
|
||||
</Button>
|
||||
<DirectoryAddButton registry={registry} />
|
||||
</ItemActions>
|
||||
<ItemFooter className="justify-start pl-16 sm:hidden">
|
||||
<Button size="sm" variant="outline">
|
||||
View <IconArrowUpRight />
|
||||
</Button>
|
||||
<DirectoryAddButton registry={registry} />
|
||||
</ItemFooter>
|
||||
</Item>
|
||||
{index < registries.length - 1 && <ItemSeparator className="my-1" />}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</ItemGroup>
|
||||
)
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import { Fragment } from "react"
|
||||
import Link from "next/link"
|
||||
import { usePathname } from "next/navigation"
|
||||
import { useBreadcrumb } from "fumadocs-core/breadcrumb"
|
||||
import type { PageTree } from "fumadocs-core/server"
|
||||
import type { Root } from "fumadocs-core/page-tree"
|
||||
|
||||
import {
|
||||
Breadcrumb,
|
||||
@@ -19,7 +19,7 @@ export function DocsBreadcrumb({
|
||||
tree,
|
||||
className,
|
||||
}: {
|
||||
tree: PageTree.Root
|
||||
tree: Root
|
||||
className?: string
|
||||
}) {
|
||||
const pathname = usePathname()
|
||||
|
||||
@@ -24,8 +24,8 @@ const TOP_LEVEL_SECTIONS = [
|
||||
href: "/docs/components",
|
||||
},
|
||||
{
|
||||
name: "Registry",
|
||||
href: "/docs/registry",
|
||||
name: "Directory",
|
||||
href: "/docs/directory",
|
||||
},
|
||||
{
|
||||
name: "MCP Server",
|
||||
@@ -51,12 +51,12 @@ export function DocsSidebar({
|
||||
|
||||
return (
|
||||
<Sidebar
|
||||
className="sticky top-[calc(var(--header-height)+1px)] z-30 hidden h-[calc(100svh-var(--footer-height)+2rem)] bg-transparent lg:flex"
|
||||
className="sticky top-[calc(var(--header-height)+1px)] z-30 hidden h-[calc(100svh-var(--footer-height)-4rem)] overscroll-none bg-transparent lg:flex"
|
||||
collapsible="none"
|
||||
{...props}
|
||||
>
|
||||
<SidebarContent className="no-scrollbar overflow-x-hidden px-2 pb-12">
|
||||
<div className="h-(--top-spacing) shrink-0" />
|
||||
<SidebarContent className="no-scrollbar overflow-x-hidden px-2">
|
||||
<div className="from-background via-background/80 to-background/50 sticky -top-1 z-10 h-8 shrink-0 bg-gradient-to-b blur-xs" />
|
||||
<SidebarGroup>
|
||||
<SidebarGroupLabel className="text-muted-foreground font-medium">
|
||||
Sections
|
||||
@@ -141,6 +141,7 @@ export function DocsSidebar({
|
||||
</SidebarGroup>
|
||||
)
|
||||
})}
|
||||
<div className="from-background via-background/80 to-background/50 sticky -bottom-1 z-10 h-16 shrink-0 bg-gradient-to-t blur-xs" />
|
||||
</SidebarContent>
|
||||
</Sidebar>
|
||||
)
|
||||
|
||||
@@ -16,7 +16,7 @@ export function MainNav({
|
||||
const pathname = usePathname()
|
||||
|
||||
return (
|
||||
<nav className={cn("items-center gap-0.5", className)} {...props}>
|
||||
<nav className={cn("items-center", className)} {...props}>
|
||||
{items.map((item) => (
|
||||
<Button key={item.href} variant="ghost" asChild size="sm">
|
||||
<Link
|
||||
|
||||
@@ -22,8 +22,8 @@ const TOP_LEVEL_SECTIONS = [
|
||||
href: "/docs/components",
|
||||
},
|
||||
{
|
||||
name: "Registry",
|
||||
href: "/docs/registry",
|
||||
name: "Directory",
|
||||
href: "/docs/directory",
|
||||
},
|
||||
{
|
||||
name: "MCP Server",
|
||||
|
||||
@@ -21,7 +21,7 @@ export function SiteHeader() {
|
||||
return (
|
||||
<header className="bg-background sticky top-0 z-50 w-full">
|
||||
<div className="container-wrapper 3xl:fixed:px-0 px-6">
|
||||
<div className="3xl:fixed:container flex h-(--header-height) items-center gap-2 **:data-[slot=separator]:!h-4">
|
||||
<div className="3xl:fixed:container flex h-(--header-height) items-center **:data-[slot=separator]:!h-4">
|
||||
<MobileNav
|
||||
tree={pageTree}
|
||||
items={siteConfig.navItems}
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { IconCheck, IconCopy } from "@tabler/icons-react"
|
||||
import template from "lodash/template"
|
||||
import { CheckIcon, ClipboardIcon } from "lucide-react"
|
||||
|
||||
import { THEMES } from "@/lib/themes"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { useThemeConfig } from "@/components/active-theme"
|
||||
import { copyToClipboardWithMeta } from "@/components/copy-button"
|
||||
import { Icons } from "@/components/icons"
|
||||
import { BaseColor, baseColors, baseColorsOKLCH } from "@/registry/base-colors"
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import {
|
||||
Dialog,
|
||||
@@ -41,21 +43,12 @@ import {
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from "@/registry/new-york-v4/ui/tabs"
|
||||
import {
|
||||
BaseColor,
|
||||
baseColors,
|
||||
baseColorsOKLCH,
|
||||
} from "@/registry/registry-base-colors"
|
||||
|
||||
interface BaseColorOKLCH {
|
||||
light: Record<string, string>
|
||||
dark: Record<string, string>
|
||||
}
|
||||
|
||||
const THEMES = baseColors.filter(
|
||||
(theme) => !["slate", "stone", "gray", "zinc"].includes(theme.name)
|
||||
)
|
||||
|
||||
export function ThemeCustomizer({ className }: React.ComponentProps<"div">) {
|
||||
const { activeTheme = "neutral", setActiveTheme } = useThemeConfig()
|
||||
|
||||
@@ -143,11 +136,18 @@ export function CopyCodeButton({
|
||||
</Drawer>
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button className={cn("hidden sm:flex", className)} {...props}>
|
||||
Copy Code
|
||||
<Button
|
||||
data-size={props.size}
|
||||
className={cn("group/button hidden sm:flex", className)}
|
||||
{...props}
|
||||
>
|
||||
<IconCopy />
|
||||
<span className="group-data-[size=icon-sm]/button:sr-only">
|
||||
Copy Code
|
||||
</span>
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="outline-none md:max-w-3xl">
|
||||
<DialogContent className="rounded-xl border-none bg-clip-padding shadow-2xl ring-4 ring-neutral-200/80 outline-none md:max-w-2xl dark:bg-neutral-800 dark:ring-neutral-900">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="capitalize">
|
||||
{activeThemeName === "neutral" ? "Default" : activeThemeName}
|
||||
@@ -231,7 +231,7 @@ function CustomizerCode({ themeName }: { themeName: string }) {
|
||||
}}
|
||||
>
|
||||
<span className="sr-only">Copy</span>
|
||||
{hasCopied ? <CheckIcon /> : <ClipboardIcon />}
|
||||
{hasCopied ? <IconCheck /> : <IconCopy />}
|
||||
</Button>
|
||||
<code data-line-numbers data-language="css">
|
||||
<span data-line className="line text-code-foreground">
|
||||
@@ -311,7 +311,7 @@ function CustomizerCode({ themeName }: { themeName: string }) {
|
||||
}}
|
||||
>
|
||||
<span className="sr-only">Copy</span>
|
||||
{hasCopied ? <CheckIcon /> : <ClipboardIcon />}
|
||||
{hasCopied ? <IconCheck /> : <IconCopy />}
|
||||
</Button>
|
||||
<code data-line-numbers data-language="css">
|
||||
<span data-line className="line">
|
||||
|
||||
@@ -1,64 +1,18 @@
|
||||
"use client"
|
||||
|
||||
import { THEMES } from "@/lib/themes"
|
||||
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 DEFAULT_THEMES = [
|
||||
{
|
||||
name: "Default",
|
||||
value: "default",
|
||||
},
|
||||
{
|
||||
name: "Scaled",
|
||||
value: "scaled",
|
||||
},
|
||||
{
|
||||
name: "Mono",
|
||||
value: "mono",
|
||||
},
|
||||
]
|
||||
|
||||
const COLOR_THEMES = [
|
||||
{
|
||||
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",
|
||||
},
|
||||
]
|
||||
import { CopyCodeButton } from "./theme-customizer"
|
||||
|
||||
export function ThemeSelector({ className }: React.ComponentProps<"div">) {
|
||||
const { activeTheme, setActiveTheme } = useThemeConfig()
|
||||
@@ -78,32 +32,18 @@ export function ThemeSelector({ className }: React.ComponentProps<"div">) {
|
||||
<SelectValue placeholder="Select a theme" />
|
||||
</SelectTrigger>
|
||||
<SelectContent align="end">
|
||||
<SelectGroup>
|
||||
{DEFAULT_THEMES.map((theme) => (
|
||||
<SelectItem
|
||||
key={theme.name}
|
||||
value={theme.value}
|
||||
className="data-[state=checked]:opacity-50"
|
||||
>
|
||||
{theme.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
<SelectSeparator />
|
||||
<SelectGroup>
|
||||
<SelectLabel>Colors</SelectLabel>
|
||||
{COLOR_THEMES.map((theme) => (
|
||||
<SelectItem
|
||||
key={theme.name}
|
||||
value={theme.value}
|
||||
className="data-[state=checked]:opacity-50"
|
||||
>
|
||||
{theme.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
{THEMES.map((theme) => (
|
||||
<SelectItem
|
||||
key={theme.name}
|
||||
value={theme.name}
|
||||
className="data-[state=checked]:opacity-50"
|
||||
>
|
||||
{theme.label === "Neutral" ? "Default" : theme.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<CopyCodeButton variant="secondary" size="icon-sm" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
69
apps/v4/content/docs/(root)/directory.mdx
Normal file
69
apps/v4/content/docs/(root)/directory.mdx
Normal file
@@ -0,0 +1,69 @@
|
||||
---
|
||||
title: Registry Directory
|
||||
description: Discover community registries for shadcn/ui components and blocks.
|
||||
---
|
||||
|
||||
These registries are built into the CLI with no additional configuration required. To add a component, run: `npx shadcn add @<registry>/<component>`.
|
||||
|
||||
<DirectoryList />
|
||||
|
||||
Don't see a registry? Learn how to [add it here](/docs/registry/registry-index).
|
||||
|
||||
## Documentation
|
||||
|
||||
You can use the `shadcn` CLI to run your own code registry. Running your own registry allows you to distribute your custom components, hooks, pages, config, rules and other files to any project.
|
||||
|
||||
<div className="mt-6 grid gap-4 sm:grid-cols-2">
|
||||
<LinkedCard href="/docs/registry/getting-started" className="items-start text-sm md:p-6">
|
||||
<div className="font-medium">Getting Started</div>
|
||||
<div className="text-muted-foreground">
|
||||
Set up and build your own registry
|
||||
</div>
|
||||
</LinkedCard>
|
||||
|
||||
<LinkedCard
|
||||
href="/docs/registry/authentication"
|
||||
className="items-start text-sm md:p-6"
|
||||
>
|
||||
<div className="font-medium">Authentication</div>
|
||||
<div className="text-muted-foreground">
|
||||
Secure your registry with authentication
|
||||
</div>
|
||||
</LinkedCard>
|
||||
<LinkedCard
|
||||
href="/docs/registry/namespace"
|
||||
className="items-start text-sm md:p-6"
|
||||
>
|
||||
<div className="font-medium">Namespaces</div>
|
||||
<div className="text-muted-foreground">
|
||||
Configure registries with namespaces
|
||||
</div>
|
||||
</LinkedCard>
|
||||
<LinkedCard
|
||||
href="/docs/registry/registry-index"
|
||||
className="items-start text-sm md:p-6"
|
||||
>
|
||||
<div className="font-medium">Add a Registry</div>
|
||||
<div className="text-muted-foreground">
|
||||
Learn how to add a registry to the directory
|
||||
</div>
|
||||
</LinkedCard>
|
||||
<LinkedCard
|
||||
href="/docs/registry/examples"
|
||||
className="items-start text-sm md:p-6"
|
||||
>
|
||||
<div className="font-medium">Examples</div>
|
||||
<div className="text-muted-foreground">
|
||||
Registry item examples and configurations
|
||||
</div>
|
||||
</LinkedCard>
|
||||
<LinkedCard
|
||||
href="/docs/registry/registry-json"
|
||||
className="items-start text-sm md:p-6"
|
||||
>
|
||||
<div className="font-medium">Schema</div>
|
||||
<div className="text-muted-foreground">
|
||||
Schema specification for registry.json
|
||||
</div>
|
||||
</LinkedCard>
|
||||
</div>
|
||||
@@ -9,20 +9,24 @@ However we provide a JavaScript version of the components as well. The JavaScrip
|
||||
|
||||
To opt-out of TypeScript, you can use the `tsx` flag in your `components.json` file.
|
||||
|
||||
```json {10} title="components.json" showLineNumbers
|
||||
```json {4} title="components.json" showLineNumbers
|
||||
{
|
||||
"style": "default",
|
||||
"style": "new-york",
|
||||
"rsc": false,
|
||||
"tsx": false,
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.js",
|
||||
"config": "",
|
||||
"css": "src/app/globals.css",
|
||||
"baseColor": "zinc",
|
||||
"cssVariables": true
|
||||
},
|
||||
"rsc": false,
|
||||
"tsx": false,
|
||||
"iconLibrary": "lucide",
|
||||
"aliases": {
|
||||
"utils": "~/lib/utils",
|
||||
"components": "~/components"
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -3,17 +3,9 @@ title: Next.js 15 + React 19
|
||||
description: Using shadcn/ui with Next.js 15 and React 19.
|
||||
---
|
||||
|
||||
<Callout className="mb-6 border-blue-600 bg-blue-50 dark:border-blue-900 dark:bg-blue-950 [&_code]:bg-blue-100 dark:[&_code]:bg-blue-900">
|
||||
<Callout className="">
|
||||
**Update:** We have added full support for React 19 and Tailwind v4 in the
|
||||
`canary` release. See the docs for [Tailwind v4](/docs/tailwind-v4) for more
|
||||
information.
|
||||
</Callout>
|
||||
|
||||
<Callout>
|
||||
**The following guide applies to any framework that supports React 19**. I
|
||||
titled this page "Next.js 15 + React 19" to help people upgrading to Next.js
|
||||
15 find it. We are working with package maintainers to help upgrade to React
|
||||
19.
|
||||
`latest` release. **This guide might be outdated. Proceed with caution.**
|
||||
</Callout>
|
||||
|
||||
## TL;DR
|
||||
@@ -148,7 +140,7 @@ To make it easy for you track the progress of the upgrade, I've created a table
|
||||
| [react-day-picker](https://www.npmjs.com/package/react-day-picker) | ✅ | Works with flag for npm. Work to upgrade to v9 in progress. |
|
||||
| [input-otp](https://www.npmjs.com/package/input-otp) | ✅ | |
|
||||
| [vaul](https://www.npmjs.com/package/vaul) | ✅ | |
|
||||
| [@radix-ui/react-icons](https://www.npmjs.com/package/@radix-ui/react-icons) | 🚧 | See [PR #194](https://github.com/radix-ui/icons/pull/194) |
|
||||
| [@radix-ui/react-icons](https://www.npmjs.com/package/@radix-ui/react-icons) | ✅ | See [PR #194](https://github.com/radix-ui/icons/pull/194) |
|
||||
| [cmdk](https://www.npmjs.com/package/cmdk) | ✅ | |
|
||||
|
||||
If you have any questions, please [open an issue](https://github.com/shadcn/ui/issues) on GitHub.
|
||||
|
||||
@@ -1,208 +0,0 @@
|
||||
---
|
||||
title: Styleguide
|
||||
description: A styleguide for writing documentation in mdx.
|
||||
---
|
||||
|
||||
The OpenAI API provides a simple interface to state-of-the-art AI models for text generation, natural language processing, computer vision, and more. This example generates text output from a prompt, as you might using ChatGPT.
|
||||
|
||||
## Analyze image inputs
|
||||
|
||||
You can provide image inputs to the model as well. Scan receipts, analyze screenshots, or find objects in the real world with [computer vision](/docs/installation/computer-vision). This is code in a `pre` tag and `npx` command in a `code` tag.
|
||||
|
||||
```bash
|
||||
npm install foo
|
||||
```
|
||||
|
||||
```bash
|
||||
npx shadcn@latest init
|
||||
```
|
||||
|
||||
```bash
|
||||
npx shadcn@latest add button
|
||||
```
|
||||
|
||||
```tsx
|
||||
<Button>Click me</Button>
|
||||
```
|
||||
|
||||
```tsx showLineNumbers
|
||||
// With line numbers
|
||||
export default function Home() {
|
||||
return <div>Hello</div>
|
||||
}
|
||||
```
|
||||
|
||||
```tsx title="Button.tsx"
|
||||
export default function Button({ children }: { children: React.ReactNode }) {
|
||||
return <button>{children}</button>
|
||||
}
|
||||
```
|
||||
|
||||
This is a code block with a title.
|
||||
|
||||
## Line Numbers and Line Highlighting
|
||||
|
||||
Draw attention to a particular line of code.
|
||||
|
||||
```tsx {4} showLineNumbers
|
||||
import { useFloating } from "@floating-ui/react"
|
||||
|
||||
function MyComponent() {
|
||||
const { refs, floatingStyles } = useFloating()
|
||||
|
||||
return (
|
||||
<>
|
||||
<div ref={refs.setReference} />
|
||||
<div ref={refs.setFloating} style={floatingStyles} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Word Highlighting
|
||||
|
||||
Draw attention to a particular word or series of characters.
|
||||
|
||||
```tsx /floatingStyles/
|
||||
import { useFloating } from "@floating-ui/react"
|
||||
|
||||
function MyComponent() {
|
||||
const { refs, floatingStyles } = useFloating()
|
||||
|
||||
return (
|
||||
<>
|
||||
<div ref={refs.setReference} />
|
||||
<div ref={refs.setFloating} style={floatingStyles} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
How
|
||||
|
||||
```tsx title="apps/www/registry/registry-blocks.tsx"
|
||||
export const blocks = [
|
||||
// ...
|
||||
{
|
||||
name: "dashboard-01",
|
||||
author: "shadcn (https://ui.shadcn.com)",
|
||||
title: "Dashboard",
|
||||
description: "A simple dashboard with a hello world component.",
|
||||
type: "registry:block",
|
||||
registryDependencies: ["input", "button", "card"],
|
||||
dependencies: ["zod"],
|
||||
files: [
|
||||
{
|
||||
path: "blocks/dashboard-01/page.tsx",
|
||||
type: "registry:page",
|
||||
target: "app/dashboard/page.tsx",
|
||||
},
|
||||
{
|
||||
path: "blocks/dashboard-01/components/hello-world.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/dashboard-01/components/example-card.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/dashboard-01/hooks/use-hello-world.ts",
|
||||
type: "registry:hook",
|
||||
},
|
||||
{
|
||||
path: "blocks/dashboard-01/lib/format-date.ts",
|
||||
type: "registry:lib",
|
||||
},
|
||||
],
|
||||
categories: ["dashboard"],
|
||||
},
|
||||
]
|
||||
```
|
||||
|
||||
```txt
|
||||
apps
|
||||
└── web # Your app goes here.
|
||||
├── app
|
||||
│ └── page.tsx
|
||||
├── components
|
||||
│ └── login-form.tsx
|
||||
├── components.json
|
||||
└── package.json
|
||||
packages
|
||||
└── ui # Your components and dependencies are installed here.
|
||||
├── src
|
||||
│ ├── components
|
||||
│ │ └── button.tsx
|
||||
│ ├── hooks
|
||||
│ ├── lib
|
||||
│ │ └── utils.ts
|
||||
│ └── styles
|
||||
│ └── globals.css
|
||||
├── components.json
|
||||
└── package.json
|
||||
package.json
|
||||
turbo.json
|
||||
```
|
||||
|
||||
```diff showLineNumbers
|
||||
- @plugin 'tailwindcss-animate';
|
||||
+ @import "tw-animate-css";
|
||||
```
|
||||
|
||||
## CSS Variables
|
||||
|
||||
```tsx /bg-background/ /text-foreground/
|
||||
<div className="bg-background text-foreground" />
|
||||
```
|
||||
|
||||
To use CSS variables for theming set `tailwind.cssVariables` to `true` in your `components.json` file.
|
||||
|
||||
```json {8} title="components.json"
|
||||
{
|
||||
"style": "default",
|
||||
"rsc": true,
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"css": "app/globals.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
},
|
||||
"iconLibrary": "lucide"
|
||||
}
|
||||
```
|
||||
|
||||
## Utility classes
|
||||
|
||||
```tsx /bg-zinc-950/ /text-zinc-50/ /dark:bg-white/ /dark:text-zinc-950/
|
||||
<div className="bg-zinc-950 dark:bg-white" />
|
||||
```
|
||||
|
||||
To use utility classes for theming set `tailwind.cssVariables` to `false` in your `components.json` file.
|
||||
|
||||
```json {8} title="components.json"
|
||||
{
|
||||
"style": "default",
|
||||
"rsc": true,
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"css": "app/globals.css",
|
||||
"baseColor": "slate",
|
||||
"cssVariables": false
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
},
|
||||
"iconLibrary": "lucide"
|
||||
}
|
||||
```
|
||||
@@ -15,7 +15,7 @@ To use CSS variables for theming set `tailwind.cssVariables` to `true` in your `
|
||||
|
||||
```json {8} title="components.json" showLineNumbers
|
||||
{
|
||||
"style": "default",
|
||||
"style": "new-york",
|
||||
"rsc": true,
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
@@ -44,7 +44,7 @@ To use utility classes for theming set `tailwind.cssVariables` to `false` in you
|
||||
|
||||
```json {8} title="components.json" showLineNumbers
|
||||
{
|
||||
"style": "default",
|
||||
"style": "new-york",
|
||||
"rsc": true,
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
@@ -52,14 +52,14 @@ To use utility classes for theming set `tailwind.cssVariables` to `false` in you
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": false
|
||||
},
|
||||
"iconLibrary": "lucide",
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
},
|
||||
"iconLibrary": "lucide"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -163,7 +163,7 @@ Here's the list of variables available for customization:
|
||||
|
||||
## Adding new colors
|
||||
|
||||
To add new colors, you need to add them to your CSS file and to your `tailwind.config.js` file.
|
||||
To add new colors, you need to add them to your CSS file under the `:root` and `dark` pseudo-classes. Then, use the `@theme inline` directive to make the colors available as CSS variables.
|
||||
|
||||
```css title="app/globals.css" showLineNumbers
|
||||
:root {
|
||||
|
||||
@@ -49,7 +49,7 @@ import { Badge } from "@/components/ui/badge"
|
||||
```
|
||||
|
||||
```tsx
|
||||
<Badge variant="default |outline | secondary | destructive">Badge</Badge>
|
||||
<Badge variant="default | outline | secondary | destructive">Badge</Badge>
|
||||
```
|
||||
|
||||
### Link
|
||||
|
||||
@@ -108,12 +108,40 @@ To use the Persian calendar, edit `components/ui/calendar.tsx` and replace `reac
|
||||
description="A Persian calendar."
|
||||
/>
|
||||
|
||||
## Selected Date (With TimeZone)
|
||||
|
||||
The Calendar component accepts a `timeZone` prop to ensure dates are displayed and selected in the user's local timezone.
|
||||
|
||||
```tsx showLineNumbers
|
||||
export function CalendarWithTimezone() {
|
||||
const [date, setDate] = React.useState<Date | undefined>(undefined)
|
||||
const [timeZone, setTimeZone] = React.useState<string | undefined>(undefined)
|
||||
|
||||
React.useEffect(() => {
|
||||
setTimeZone(Intl.DateTimeFormat().resolvedOptions().timeZone)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={date}
|
||||
onSelect={setDate}
|
||||
timeZone={timeZone}
|
||||
/>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
**Note:** If you notice a selected date offset (for example, selecting the 20th highlights the 19th), make sure the `timeZone` prop is set to the user's local timezone.
|
||||
|
||||
**Why client-side?** The timezone is detected using `Intl.DateTimeFormat().resolvedOptions().timeZone` inside a `useEffect` to ensure compatibility with server-side rendering. Detecting the timezone during render would cause hydration mismatches, as the server and client may be in different timezones.
|
||||
|
||||
## Examples
|
||||
|
||||
### Range Calendar
|
||||
|
||||
<ComponentPreview
|
||||
name="calendar-02"
|
||||
name="calendar-05"
|
||||
title="Range Calendar"
|
||||
description="A calendar showing the current date and range selection."
|
||||
className="**:[.preview]:h-auto lg:**:[.preview]:h-[450px]"
|
||||
@@ -153,9 +181,36 @@ This component uses the `chrono-node` library to parse natural language dates.
|
||||
description="A calendar with natural language picker."
|
||||
/>
|
||||
|
||||
### Form
|
||||
### Custom Cell Size
|
||||
|
||||
<ComponentPreview name="calendar-form" />
|
||||
<ComponentPreview
|
||||
name="calendar-18"
|
||||
title="Custom Cell Size"
|
||||
description="A calendar with custom cell size that's responsive."
|
||||
className="**:[.preview]:h-[560px]"
|
||||
/>
|
||||
|
||||
You can customize the size of calendar cells using the `--cell-size` CSS variable. You can also make it responsive by using breakpoint-specific values:
|
||||
|
||||
```tsx showLineNumbers
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={date}
|
||||
onSelect={setDate}
|
||||
className="rounded-lg border [--cell-size:--spacing(11)] md:[--cell-size:--spacing(12)]"
|
||||
/>
|
||||
```
|
||||
|
||||
Or use fixed values:
|
||||
|
||||
```tsx showLineNumbers
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={date}
|
||||
onSelect={setDate}
|
||||
className="rounded-lg border [--cell-size:2.75rem] md:[--cell-size:3rem]"
|
||||
/>
|
||||
```
|
||||
|
||||
## Upgrade Guide
|
||||
|
||||
@@ -289,7 +344,10 @@ function Calendar({
|
||||
defaultClassNames.week_number
|
||||
),
|
||||
day: cn(
|
||||
"group/day relative aspect-square h-full w-full select-none p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md",
|
||||
"relative w-full h-full p-0 text-center [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none",
|
||||
props.showWeekNumber
|
||||
? "[&:nth-child(2)[data-selected=true]_button]:rounded-l-md"
|
||||
: "[&:first-child[data-selected=true]_button]:rounded-l-md",
|
||||
defaultClassNames.day
|
||||
),
|
||||
range_start: cn(
|
||||
@@ -417,3 +475,25 @@ npx shadcn@latest add calendar-02
|
||||
```
|
||||
|
||||
This will install the latest version of the calendar blocks.
|
||||
|
||||
## Changelog
|
||||
|
||||
### 2025-10-26 Fixed day radius with week numbers
|
||||
|
||||
We have fixed an issue where the selected day's left border radius was not applied correctly when week numbers were displayed. The fix ensures that when `showWeekNumber` is enabled, the first day (which is the second child due to the week number column) correctly receives the rounded left border.
|
||||
|
||||
To apply this fix, edit `components/ui/calendar.tsx` and update the `day` class in `classNames`:
|
||||
|
||||
```tsx showLineNumbers title="components/ui/calendar.tsx" {5-7}
|
||||
classNames={{
|
||||
// ... other classNames
|
||||
day: cn(
|
||||
"relative w-full h-full p-0 text-center [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none",
|
||||
props.showWeekNumber
|
||||
? "[&:nth-child(2)[data-selected=true]_button]:rounded-l-md"
|
||||
: "[&:first-child[data-selected=true]_button]:rounded-l-md",
|
||||
defaultClassNames.day
|
||||
),
|
||||
// ... other classNames
|
||||
}}
|
||||
```
|
||||
|
||||
@@ -4,3 +4,7 @@ description: Here you can find all the components available in the library. We a
|
||||
---
|
||||
|
||||
<ComponentsList />
|
||||
|
||||
---
|
||||
|
||||
Can't find what you need? Try the [registry directory](/docs/directory) for community-maintained components.
|
||||
|
||||
205
apps/v4/content/docs/components/native-select.mdx
Normal file
205
apps/v4/content/docs/components/native-select.mdx
Normal file
@@ -0,0 +1,205 @@
|
||||
---
|
||||
title: Native Select
|
||||
description: A styled native HTML select element with consistent design system integration.
|
||||
component: true
|
||||
---
|
||||
|
||||
import { InfoIcon } from "lucide-react"
|
||||
|
||||
<Callout variant="info" icon={<InfoIcon className="!translate-y-[3px]" />}>
|
||||
For a styled select component, see the [Select](/docs/components/select)
|
||||
component.
|
||||
</Callout>
|
||||
|
||||
<ComponentPreview name="native-select-demo" />
|
||||
|
||||
## Installation
|
||||
|
||||
<CodeTabs>
|
||||
|
||||
<TabsList>
|
||||
<TabsTrigger value="cli">CLI</TabsTrigger>
|
||||
<TabsTrigger value="manual">Manual</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="cli">
|
||||
|
||||
```bash
|
||||
npx shadcn@latest add native-select
|
||||
```
|
||||
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="manual">
|
||||
|
||||
<Steps>
|
||||
|
||||
<Step>Copy and paste the following code into your project.</Step>
|
||||
|
||||
<ComponentSource name="native-select" title="components/ui/native-select.tsx" />
|
||||
|
||||
<Step>Update the import paths to match your project setup.</Step>
|
||||
|
||||
</Steps>
|
||||
|
||||
</TabsContent>
|
||||
|
||||
</CodeTabs>
|
||||
|
||||
## Usage
|
||||
|
||||
```tsx showLineNumbers
|
||||
import {
|
||||
NativeSelect,
|
||||
NativeSelectOptGroup,
|
||||
NativeSelectOption,
|
||||
} from "@/components/ui/native-select"
|
||||
```
|
||||
|
||||
```tsx showLineNumbers
|
||||
<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>
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### With Groups
|
||||
|
||||
Organize options using `NativeSelectOptGroup` for better categorization.
|
||||
|
||||
<ComponentPreview name="native-select-groups" />
|
||||
|
||||
```tsx showLineNumbers
|
||||
<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>
|
||||
```
|
||||
|
||||
### Disabled State
|
||||
|
||||
Disable individual options or the entire select component.
|
||||
|
||||
<ComponentPreview name="native-select-disabled" />
|
||||
|
||||
### Invalid State
|
||||
|
||||
Show validation errors with the `aria-invalid` attribute and error styling.
|
||||
|
||||
<ComponentPreview name="native-select-invalid" />
|
||||
|
||||
```tsx showLineNumbers
|
||||
<NativeSelect aria-invalid="true">
|
||||
<NativeSelectOption value="">Select a country</NativeSelectOption>
|
||||
<NativeSelectOption value="us">United States</NativeSelectOption>
|
||||
<NativeSelectOption value="uk">United Kingdom</NativeSelectOption>
|
||||
<NativeSelectOption value="ca">Canada</NativeSelectOption>
|
||||
</NativeSelect>
|
||||
```
|
||||
|
||||
### Form Integration
|
||||
|
||||
Use with form libraries like React Hook Form for controlled components.
|
||||
|
||||
<ComponentPreview name="native-select-form" />
|
||||
|
||||
### Input Group Integration
|
||||
|
||||
Combine with `InputGroup` for complex input layouts.
|
||||
|
||||
<ComponentPreview name="native-select-input-group" />
|
||||
|
||||
## Native Select vs Select
|
||||
|
||||
- Use `NativeSelect` when you need native browser behavior, better performance, or mobile-optimized dropdowns.
|
||||
- Use `Select` when you need custom styling, animations, or complex interactions.
|
||||
|
||||
The `NativeSelect` component provides native HTML select functionality with consistent styling that matches your design system.
|
||||
|
||||
## Accessibility
|
||||
|
||||
- The component maintains all native HTML select accessibility features.
|
||||
- Screen readers can navigate through options using arrow keys.
|
||||
- The chevron icon is marked as `aria-hidden="true"` to avoid duplication.
|
||||
- Use `aria-label` or `aria-labelledby` for additional context when needed.
|
||||
|
||||
```tsx showLineNumbers
|
||||
<NativeSelect aria-label="Choose your preferred language">
|
||||
<NativeSelectOption value="en">English</NativeSelectOption>
|
||||
<NativeSelectOption value="es">Spanish</NativeSelectOption>
|
||||
<NativeSelectOption value="fr">French</NativeSelectOption>
|
||||
</NativeSelect>
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### NativeSelect
|
||||
|
||||
The main select component that wraps the native HTML select element.
|
||||
|
||||
| Prop | Type | Default |
|
||||
| ----------- | -------- | ------- |
|
||||
| `className` | `string` | |
|
||||
|
||||
All other props are passed through to the underlying `<select>` element.
|
||||
|
||||
```tsx
|
||||
<NativeSelect>
|
||||
<NativeSelectOption value="option1">Option 1</NativeSelectOption>
|
||||
<NativeSelectOption value="option2">Option 2</NativeSelectOption>
|
||||
</NativeSelect>
|
||||
```
|
||||
|
||||
### NativeSelectOption
|
||||
|
||||
Represents an individual option within the select.
|
||||
|
||||
| Prop | Type | Default |
|
||||
| ----------- | --------- | ------- |
|
||||
| `value` | `string` | |
|
||||
| `disabled` | `boolean` | `false` |
|
||||
| `className` | `string` | |
|
||||
|
||||
All other props are passed through to the underlying `<option>` element.
|
||||
|
||||
```tsx
|
||||
<NativeSelectOption value="apple">Apple</NativeSelectOption>
|
||||
<NativeSelectOption value="banana" disabled>
|
||||
Banana
|
||||
</NativeSelectOption>
|
||||
```
|
||||
|
||||
### NativeSelectOptGroup
|
||||
|
||||
Groups related options together for better organization.
|
||||
|
||||
| Prop | Type | Default |
|
||||
| ----------- | --------- | ------- |
|
||||
| `label` | `string` | |
|
||||
| `disabled` | `boolean` | `false` |
|
||||
| `className` | `string` | |
|
||||
|
||||
All other props are passed through to the underlying `<optgroup>` element.
|
||||
|
||||
```tsx
|
||||
<NativeSelectOptGroup label="Fruits">
|
||||
<NativeSelectOption value="apple">Apple</NativeSelectOption>
|
||||
<NativeSelectOption value="banana">Banana</NativeSelectOption>
|
||||
</NativeSelectOptGroup>
|
||||
```
|
||||
@@ -7,7 +7,10 @@ links:
|
||||
api: https://www.radix-ui.com/docs/primitives/components/navigation-menu#api-reference
|
||||
---
|
||||
|
||||
<ComponentPreview name="navigation-menu-demo" />
|
||||
<ComponentPreview
|
||||
name="navigation-menu-demo"
|
||||
className="[&_.preview]:!items-start [&_.preview]:p-4 [&_.preview]:pt-8 md:[&_.preview]:pt-16"
|
||||
/>
|
||||
|
||||
## Installation
|
||||
|
||||
|
||||
@@ -7,10 +7,7 @@ links:
|
||||
api: https://www.radix-ui.com/docs/primitives/components/toggle-group#api-reference
|
||||
---
|
||||
|
||||
<ComponentPreview
|
||||
name="toggle-group-demo"
|
||||
description="A toggle group with three items."
|
||||
/>
|
||||
<ComponentPreview name="toggle-group-spacing" />
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -66,13 +63,6 @@ import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"
|
||||
|
||||
## Examples
|
||||
|
||||
### Default
|
||||
|
||||
<ComponentPreview
|
||||
name="toggle-group-demo"
|
||||
description="A toggle group with three items."
|
||||
/>
|
||||
|
||||
### Outline
|
||||
|
||||
<ComponentPreview
|
||||
@@ -107,3 +97,42 @@ import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"
|
||||
name="toggle-group-disabled"
|
||||
description="A disabled toggle group."
|
||||
/>
|
||||
|
||||
### Spacing
|
||||
|
||||
Use `spacing={2}` to add spacing between toggle group items.
|
||||
|
||||
<ComponentPreview
|
||||
name="toggle-group-spacing"
|
||||
description="A toggle group with spacing."
|
||||
/>
|
||||
|
||||
## API Reference
|
||||
|
||||
### ToggleGroup
|
||||
|
||||
The main component that wraps toggle group items.
|
||||
|
||||
| Prop | Type | Default |
|
||||
| ----------- | --------------------------- | ----------- |
|
||||
| `type` | `"single" \| "multiple"` | `"single"` |
|
||||
| `variant` | `"default" \| "outline"` | `"default"` |
|
||||
| `size` | `"default" \| "sm" \| "lg"` | `"default"` |
|
||||
| `spacing` | `number` | `0` |
|
||||
| `className` | `string` | |
|
||||
|
||||
```tsx
|
||||
<ToggleGroup type="single" variant="outline" size="sm">
|
||||
<ToggleGroupItem value="a">A</ToggleGroupItem>
|
||||
<ToggleGroupItem value="b">B</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
```
|
||||
|
||||
### ToggleGroupItem
|
||||
|
||||
Individual toggle items within a toggle group. Remember to add an `aria-label` to each item for accessibility.
|
||||
|
||||
| Prop | Type | Default |
|
||||
| ----------- | -------- | -------- |
|
||||
| `value` | `string` | Required |
|
||||
| `className` | `string` | |
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
title: Index
|
||||
title: Add a Registry
|
||||
description: Open Source Registry Index
|
||||
---
|
||||
|
||||
@@ -11,16 +11,9 @@ You can see the full list at [https://ui.shadcn.com/r/registries.json](https://u
|
||||
|
||||
## Adding a Registry
|
||||
|
||||
You can submit a PR to add a registry to the index by adding it to the [registries.json](https://github.com/shadcn-ui/ui/blob/main/apps/v4/public/r/registries.json) file.
|
||||
You can open an issue to add a registry to the index by filling out the [registry directory issue template](https://github.com/shadcn-ui/ui/issues/new?template=registry_directory.yml).
|
||||
|
||||
Here's an example of how to add a registry to the index:
|
||||
|
||||
```json title="registries.json" showLineNumbers
|
||||
{
|
||||
"@acme": "https://registry.acme.com/r/{name}.json",
|
||||
"@example": "https://example.com/r/{name}"
|
||||
}
|
||||
```
|
||||
Once you have submitted your issue, it will be validated and reviewed by the team.
|
||||
|
||||
### Requirements
|
||||
|
||||
@@ -65,15 +58,3 @@ Here's an example of a valid registry:
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Validation
|
||||
|
||||
At the root of the `shadcn/ui` project, you can run the following command to validate the `registries.json` file.
|
||||
|
||||
```bash
|
||||
pnpm validate:registries
|
||||
```
|
||||
|
||||
This will validate the registries.json file and output any errors.
|
||||
|
||||
Once you have submitted your PR, it will be validated and reviewed by the team.
|
||||
|
||||
@@ -1,21 +1,25 @@
|
||||
import { dirname } from "path"
|
||||
import { fileURLToPath } from "url"
|
||||
import { FlatCompat } from "@eslint/eslintrc"
|
||||
import { defineConfig, globalIgnores } from "eslint/config"
|
||||
import nextVitals from "eslint-config-next/core-web-vitals"
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = dirname(__filename)
|
||||
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
})
|
||||
|
||||
const eslintConfig = [
|
||||
...compat.config({
|
||||
extends: ["next/core-web-vitals", "next/typescript"],
|
||||
const eslintConfig = defineConfig([
|
||||
...nextVitals,
|
||||
globalIgnores([
|
||||
"node_modules/**",
|
||||
".next/**",
|
||||
"out/**",
|
||||
"build/**",
|
||||
"next-env.d.ts",
|
||||
".source/**",
|
||||
]),
|
||||
{
|
||||
rules: {
|
||||
"@next/next/no-duplicate-head": "off",
|
||||
"react-hooks/incompatible-library": "off",
|
||||
"react-hooks/purity": "off",
|
||||
"@next/next/no-html-link-for-pages": "off",
|
||||
"@next/next/no-img-element": "off",
|
||||
"@typescript-eslint/no-unused-vars": "off",
|
||||
},
|
||||
}),
|
||||
]
|
||||
},
|
||||
])
|
||||
|
||||
export default eslintConfig
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import { useEffect, useState } from "react"
|
||||
|
||||
export function useIsMac() {
|
||||
const [isMac, setIsMac] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
setIsMac(navigator.platform.toUpperCase().includes("MAC"))
|
||||
}, [])
|
||||
|
||||
return isMac
|
||||
}
|
||||
@@ -61,7 +61,10 @@ const Layout = ({
|
||||
}
|
||||
})
|
||||
|
||||
const attrs = !value ? ["layout-fixed", "layout-full"] : Object.values(value)
|
||||
const attrs = React.useMemo(
|
||||
() => (!value ? ["layout-fixed", "layout-full"] : Object.values(value)),
|
||||
[value]
|
||||
)
|
||||
|
||||
const applyLayout = React.useCallback(
|
||||
(layout: Layout) => {
|
||||
|
||||
@@ -11,7 +11,7 @@ export function useIsMobile(mobileBreakpoint = 768) {
|
||||
mql.addEventListener("change", onChange)
|
||||
setIsMobile(window.innerWidth < mobileBreakpoint)
|
||||
return () => mql.removeEventListener("change", onChange)
|
||||
}, [])
|
||||
}, [mobileBreakpoint])
|
||||
|
||||
return !!isMobile
|
||||
}
|
||||
|
||||
@@ -23,9 +23,31 @@ export async function getAllBlocks(
|
||||
categories: string[] = []
|
||||
) {
|
||||
const { Index } = await import("@/registry/__index__")
|
||||
const index = z.record(registryItemSchema).parse(Index)
|
||||
|
||||
return Object.values(index).filter(
|
||||
// Collect all blocks from all styles.
|
||||
const allBlocks: z.infer<typeof registryItemSchema>[] = []
|
||||
|
||||
for (const style in Index) {
|
||||
const styleIndex = Index[style]
|
||||
if (typeof styleIndex === "object" && styleIndex !== null) {
|
||||
for (const itemName in styleIndex) {
|
||||
const item = styleIndex[itemName]
|
||||
allBlocks.push(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate each block.
|
||||
const validatedBlocks = allBlocks
|
||||
.map((block) => {
|
||||
const result = registryItemSchema.safeParse(block)
|
||||
return result.success ? result.data : null
|
||||
})
|
||||
.filter(
|
||||
(block): block is z.infer<typeof registryItemSchema> => block !== null
|
||||
)
|
||||
|
||||
return validatedBlocks.filter(
|
||||
(block) =>
|
||||
types.includes(block.type) &&
|
||||
(categories.length === 0 ||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { z } from "zod"
|
||||
|
||||
import { colors } from "@/registry/registry-colors"
|
||||
import { colors } from "@/registry/colors"
|
||||
|
||||
const colorSchema = z.object({
|
||||
name: z.string(),
|
||||
|
||||
@@ -25,6 +25,10 @@ export const siteConfig = {
|
||||
href: "/charts/area",
|
||||
label: "Charts",
|
||||
},
|
||||
{
|
||||
href: "/docs/directory",
|
||||
label: "Directory",
|
||||
},
|
||||
{
|
||||
href: "/themes",
|
||||
label: "Themes",
|
||||
|
||||
@@ -6,6 +6,7 @@ export const PAGES_NEW = [
|
||||
"/docs/components/item",
|
||||
"/docs/components/kbd",
|
||||
"/docs/components/spinner",
|
||||
"/docs/components/native-select",
|
||||
]
|
||||
|
||||
export const PAGES_UPDATED = ["/docs/components/button"]
|
||||
|
||||
@@ -16,6 +16,7 @@ const eventSchema = z.object({
|
||||
"copy_chart_data",
|
||||
"copy_color",
|
||||
"set_layout",
|
||||
"search_query",
|
||||
]),
|
||||
// declare type AllowedPropertyValues = string | number | boolean | null
|
||||
properties: z
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import fs from "fs"
|
||||
|
||||
import { Index } from "@/registry/__index__"
|
||||
import { type Style } from "@/registry/styles"
|
||||
|
||||
export function processMdxForLLMs(content: string) {
|
||||
export function processMdxForLLMs(content: string, style: Style["name"]) {
|
||||
const componentPreviewRegex =
|
||||
/<ComponentPreview\s+[^>]*name="([^"]+)"[^>]*\/>/g
|
||||
/<ComponentPreview[\s\S]*?name="([^"]+)"[\s\S]*?\/>/g
|
||||
|
||||
return content.replace(componentPreviewRegex, (match, name) => {
|
||||
try {
|
||||
const component = Index[name]
|
||||
const component = Index[style]?.[name]
|
||||
if (!component?.files) {
|
||||
return match
|
||||
}
|
||||
|
||||
@@ -6,13 +6,36 @@ import { Project, ScriptKind } from "ts-morph"
|
||||
import { z } from "zod"
|
||||
|
||||
import { Index } from "@/registry/__index__"
|
||||
import { type Style } from "@/registry/styles"
|
||||
|
||||
export function getRegistryComponent(name: string) {
|
||||
return Index[name]?.component
|
||||
export function getRegistryComponent(name: string, styleName: Style["name"]) {
|
||||
return Index[styleName]?.[name]?.component
|
||||
}
|
||||
|
||||
export async function getRegistryItem(name: string) {
|
||||
const item = Index[name]
|
||||
export async function getRegistryItems(
|
||||
styleName: Style["name"],
|
||||
filter?: (item: z.infer<typeof registryItemSchema>) => boolean
|
||||
) {
|
||||
const styleIndex = Index[styleName]
|
||||
|
||||
if (!styleIndex) {
|
||||
return []
|
||||
}
|
||||
|
||||
const entries = Object.values(styleIndex)
|
||||
|
||||
const filteredEntries = filter ? entries.filter(filter) : entries
|
||||
|
||||
return await Promise.all(
|
||||
filteredEntries.map(async (entry) => {
|
||||
const item = await getRegistryItem(entry.name, styleName)
|
||||
return item
|
||||
})
|
||||
).then((results) => results.filter(Boolean))
|
||||
}
|
||||
|
||||
export async function getRegistryItem(name: string, styleName: Style["name"]) {
|
||||
const item = Index[styleName]?.[name]
|
||||
|
||||
if (!item) {
|
||||
return null
|
||||
|
||||
@@ -4,6 +4,7 @@ import { u } from "unist-builder"
|
||||
import { visit } from "unist-util-visit"
|
||||
|
||||
import { Index } from "@/registry/__index__"
|
||||
import { getActiveStyle } from "@/registry/styles"
|
||||
|
||||
interface UnistNode {
|
||||
type: string
|
||||
@@ -26,6 +27,8 @@ export interface UnistTree {
|
||||
|
||||
export function rehypeComponent() {
|
||||
return async (tree: UnistTree) => {
|
||||
const activeStyle = await getActiveStyle()
|
||||
|
||||
visit(tree, (node: UnistNode) => {
|
||||
// src prop overrides both name and fileName.
|
||||
const { value: srcPath } =
|
||||
@@ -111,7 +114,7 @@ export function rehypeComponent() {
|
||||
}
|
||||
|
||||
try {
|
||||
const component = Index[name]
|
||||
const component = Index[activeStyle.name]?.[name]
|
||||
const src = component.files[0]?.path
|
||||
|
||||
// Read the source file.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { docs } from "@/.source"
|
||||
import { loader } from "fumadocs-core/source"
|
||||
|
||||
export const source: ReturnType<typeof loader> = loader({
|
||||
export const source = loader({
|
||||
baseUrl: "/docs",
|
||||
source: docs.toFumadocsSource(),
|
||||
})
|
||||
|
||||
@@ -1,31 +1,5 @@
|
||||
export const THEMES = [
|
||||
{
|
||||
name: "Default",
|
||||
value: "default",
|
||||
},
|
||||
{
|
||||
name: "Neutral",
|
||||
value: "neutral",
|
||||
},
|
||||
{
|
||||
name: "Stone",
|
||||
value: "stone",
|
||||
},
|
||||
{
|
||||
name: "Zinc",
|
||||
value: "zinc",
|
||||
},
|
||||
{
|
||||
name: "Gray",
|
||||
value: "gray",
|
||||
},
|
||||
{
|
||||
name: "Slate",
|
||||
value: "slate",
|
||||
},
|
||||
{
|
||||
name: "Scaled",
|
||||
value: "scaled",
|
||||
},
|
||||
]
|
||||
export type Theme = (typeof THEMES)[number]
|
||||
import { baseColors } from "@/registry/base-colors"
|
||||
|
||||
export const THEMES = baseColors.filter(
|
||||
(theme) => !["slate", "stone", "gray", "zinc"].includes(theme.name)
|
||||
)
|
||||
|
||||
@@ -11,6 +11,7 @@ import { ComponentPreview } from "@/components/component-preview"
|
||||
import { ComponentSource } from "@/components/component-source"
|
||||
import { ComponentsList } from "@/components/components-list"
|
||||
import { CopyButton } from "@/components/copy-button"
|
||||
import { DirectoryList } from "@/components/directory-list"
|
||||
import { getIconForLanguageExtension } from "@/components/icons"
|
||||
import {
|
||||
Accordion,
|
||||
@@ -127,7 +128,6 @@ export const mdxComponents = {
|
||||
/>
|
||||
),
|
||||
img: ({ className, alt, ...props }: React.ComponentProps<"img">) => (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img className={cn("rounded-md", className)} alt={alt} {...props} />
|
||||
),
|
||||
hr: ({ ...props }: React.ComponentProps<"hr">) => (
|
||||
@@ -280,7 +280,7 @@ export const mdxComponents = {
|
||||
}: React.ComponentProps<"img">) => (
|
||||
<Image
|
||||
className={cn("mt-6 rounded-md border", className)}
|
||||
src={src || ""}
|
||||
src={(src as string) || ""}
|
||||
width={Number(width)}
|
||||
height={Number(height)}
|
||||
alt={alt || ""}
|
||||
@@ -344,6 +344,7 @@ export const mdxComponents = {
|
||||
ComponentSource,
|
||||
CodeCollapsibleWrapper,
|
||||
ComponentsList,
|
||||
DirectoryList,
|
||||
Link: ({ className, ...props }: React.ComponentProps<typeof Link>) => (
|
||||
<Link
|
||||
className={cn("font-medium underline underline-offset-4", className)}
|
||||
|
||||
@@ -25,6 +25,9 @@ const nextConfig = {
|
||||
},
|
||||
],
|
||||
},
|
||||
experimental: {
|
||||
turbopackFileSystemCacheForDev: true,
|
||||
},
|
||||
redirects() {
|
||||
return [
|
||||
{
|
||||
@@ -72,6 +75,11 @@ const nextConfig = {
|
||||
destination: "/docs/mcp",
|
||||
permanent: false,
|
||||
},
|
||||
{
|
||||
source: "/directory",
|
||||
destination: "/docs/directory",
|
||||
permanent: false,
|
||||
},
|
||||
]
|
||||
},
|
||||
rewrites() {
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
"dev": "next dev --turbopack --port 4000",
|
||||
"build": "pnpm --filter=shadcn build && next build",
|
||||
"start": "next start --port 4000",
|
||||
"lint": "next lint",
|
||||
"lint:fix": "next lint --fix",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint --fix .",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"format:write": "prettier --write \"**/*.{ts,tsx,mdx}\" --cache",
|
||||
"format:check": "prettier --check \"**/*.{ts,tsx,mdx}\" --cache",
|
||||
@@ -67,48 +67,48 @@
|
||||
"date-fns": "^4.1.0",
|
||||
"embla-carousel-autoplay": "8.5.2",
|
||||
"embla-carousel-react": "8.5.2",
|
||||
"fumadocs-core": "15.3.1",
|
||||
"front-matter": "^4.0.2",
|
||||
"fumadocs-core": "16.0.5",
|
||||
"fumadocs-docgen": "2.0.0",
|
||||
"fumadocs-mdx": "11.6.3",
|
||||
"fumadocs-ui": "15.3.1",
|
||||
"fumadocs-mdx": "13.0.2",
|
||||
"fumadocs-ui": "16.0.5",
|
||||
"input-otp": "^1.4.2",
|
||||
"jotai": "^2.1.0",
|
||||
"little-date": "^1.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "0.474.0",
|
||||
"motion": "^12.12.1",
|
||||
"next": "15.3.1",
|
||||
"next": "16.0.0",
|
||||
"next-themes": "0.4.6",
|
||||
"postcss": "^8.5.1",
|
||||
"react": "19.1.0",
|
||||
"react": "19.2.0",
|
||||
"react-day-picker": "^9.7.0",
|
||||
"react-dom": "19.1.0",
|
||||
"react-dom": "19.2.0",
|
||||
"react-hook-form": "^7.62.0",
|
||||
"react-resizable-panels": "^2.1.7",
|
||||
"react-textarea-autosize": "^8.5.9",
|
||||
"recharts": "2.15.1",
|
||||
"rehype-pretty-code": "^0.14.1",
|
||||
"rimraf": "^6.0.1",
|
||||
"shadcn": "3.4.0",
|
||||
"shadcn": "3.5.0",
|
||||
"shiki": "^1.10.1",
|
||||
"sonner": "^2.0.0",
|
||||
"tailwind-merge": "^3.0.1",
|
||||
"ts-morph": "18.0.0",
|
||||
"vaul": "1.1.2",
|
||||
"zod": "^3.24.1"
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3",
|
||||
"@ianvs/prettier-plugin-sort-imports": "^4.4.1",
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/lodash": "^4.17.7",
|
||||
"@types/mdx": "^2.0.13",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "19.1.2",
|
||||
"@types/react-dom": "19.1.2",
|
||||
"@types/react": "19.2.2",
|
||||
"@types/react-dom": "19.2.2",
|
||||
"@typescript-eslint/parser": "^8.31.0",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "15.3.1",
|
||||
"eslint-config-next": "16.0.0",
|
||||
"prettier": "^3.4.2",
|
||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||
"tailwindcss": "^4.1.11",
|
||||
|
||||
@@ -1,51 +1,75 @@
|
||||
{
|
||||
"@8bitcn": "https://8bitcn.com/r/{name}.json",
|
||||
"@97cn": "https://97cn.itzik.co/r/{name}.json",
|
||||
"@aceternity": "https://ui.aceternity.com/registry/{name}.json",
|
||||
"@ai-elements": "https://registry.ai-sdk.dev/{name}.json",
|
||||
"@alexcarpenter": "https://ui.alexcarpenter.me/r/{name}.json",
|
||||
"@algolia": "https://sitesearch.algolia.com/r/{name}.json",
|
||||
"@alpine": "https://alpine-registry.vercel.app/r/{name}.json",
|
||||
"@animate-ui": "https://animate-ui.com/r/{name}.json",
|
||||
"@assistant-ui": "https://r.assistant-ui.com/{name}.json",
|
||||
"@austin-ui": "https://austin-ui.netlify.app/r/{name}.json",
|
||||
"@basecn": "https://basecn.dev/r/{name}.json",
|
||||
"@better-upload": "https://better-upload.com/r/{name}.json",
|
||||
"@billingsdk": "https://billingsdk.com/r/{name}.json",
|
||||
"@blocks": "https://blocks.so/r/{name}.json",
|
||||
"@bucharitesh": "https://bucharitesh.in/r/{name}.json",
|
||||
"@clerk": "https://clerk.com/r/{name}.json",
|
||||
"@coss": "https://coss.com/ui/r/{name}.json",
|
||||
"@chisom-ui": "https://chisom-ui.netlify.app/r/{name}.json",
|
||||
"@cult-ui": "https://cult-ui.com/r/{name}.json",
|
||||
"@diceui": "https://diceui.com/r/{name}.json",
|
||||
"@efferd": "https://efferd.com/r/{name}.json",
|
||||
"@eldoraui": "https://eldoraui.site/r/{name}.json",
|
||||
"@elements": "https://tryelements.dev/r/{name}.json",
|
||||
"@elevenlabs-ui": "https://ui.elevenlabs.io/r/{name}.json",
|
||||
"@fancy": "https://fancycomponents.dev/r/{name}.json",
|
||||
"@formcn": "https://formcn.dev/r/{name}.json",
|
||||
"@heseui": "https://www.heseui.com/r/{name}.json",
|
||||
"@hooks": "https://shadcn-hooks.vercel.app/r/{name}.json",
|
||||
"@intentui": "https://intentui.com/r/{name}",
|
||||
"@kibo-ui": "https://www.kibo-ui.com/r/{name}.json",
|
||||
"@kanpeki": "https://kanpeki.vercel.app/r/{name}.json",
|
||||
"@kokonutui": "https://kokonutui.com/r/{name}.json",
|
||||
"@limeplay": "https://limeplay.winoffrg.dev/r/{name}.json",
|
||||
"@lytenyte": "https://www.1771technologies.com/r/{name}.json",
|
||||
"@magicui": "https://magicui.design/r/{name}.json",
|
||||
"@magicui-pro": "https://pro.magicui.design/registry/{name}",
|
||||
"@motion-primitives": "https://motion-primitives.com/c/{name}.json",
|
||||
"@originui": "https://originui.com/r/{name}.json",
|
||||
"@nativeui": "https://nativeui.io/registry/{name}.json",
|
||||
"@ncdai": "https://chanhdai.com/r/{name}.json",
|
||||
"@nuqs": "https://nuqs.dev/r/{name}.json",
|
||||
"@oui": "https://oui.mw10013.workers.dev/r/{name}.json",
|
||||
"@paceui": "https://ui.paceui.com/r/{name}.json",
|
||||
"@prompt-kit": "https://prompt-kit.com/c/{name}.json",
|
||||
"@react-bits": "https://reactbits.dev/r/{name}.json",
|
||||
"@react-market": "https://www.react-market.com/get/{name}.json",
|
||||
"@retroui": "https://retroui.dev/r/{name}.json",
|
||||
"@reui": "https://reui.io/r/{name}.json",
|
||||
"@rigidui": "https://rigidui.com/r/{name}.json",
|
||||
"@roiui": "https://roiui.com/r/{name}.json",
|
||||
"@solaceui": "https://www.solaceui.com/r/{name}.json",
|
||||
"@scrollxui": "https://www.scrollxui.dev/registry/{name}.json",
|
||||
"@shadcn-editor": "https://shadcn-editor.vercel.app/r/{name}.json",
|
||||
"@shadcn-map": "http://shadcn-map.vercel.app/r/{name}.json",
|
||||
"@shadcn-studio": "https://shadcnstudio.com/r/{name}.json",
|
||||
"@shadcnblocks": "https://shadcnblocks.com/r/{name}.json",
|
||||
"@simple-ai": "https://simple-ai.dev/r/{name}.json",
|
||||
"@skiper-ui": "https://skiper-ui.com/registry/{name}.json",
|
||||
"@skyr": "https://ui-play.skyroc.me/r/{name}.json",
|
||||
"@smoothui": "https://smoothui.dev/r/{name}.json",
|
||||
"@spectrumui": "https://ui.spectrumhq.in/r/{name}.json",
|
||||
"@supabase": "https://supabase.com/ui/r/{name}.json",
|
||||
"@svgl": "https://svgl.app/r/{name}.json",
|
||||
"@tailark": "https://tailark.com/r/{name}.json",
|
||||
"@tweakcn": "https://tweakcn.com/r/themes/{name}.json",
|
||||
"@react-bits": "https://reactbits.dev/r/{name}.json",
|
||||
"@reui": "https://reui.io/r/{name}.json",
|
||||
"@heseui": "https://www.heseui.com/r/{name}.json",
|
||||
"@paceui-ui": "https://ui.paceui.com/r/{name}.json",
|
||||
"@basecn": "https://basecn.dev/r/{name}.json",
|
||||
"@ncdai": "https://chanhdai.com/r/{name}.json",
|
||||
"@8bitcn": "https://8bitcn.com/r/{name}.json",
|
||||
"@billingsdk": "https://billingsdk.com/r/{name}.json",
|
||||
"@elements": "https://tryelements.dev/r/{name}.json",
|
||||
"@nativeui": "https://nativeui.io/registry/{name}.json",
|
||||
"@scrollxui": "https://www.scrollxui.dev/registry/{name}.json",
|
||||
"@smoothui": "https://smoothui.dev/r/{name}.json",
|
||||
"@svgl": "https://svgl.app/r/{name}.json",
|
||||
"@formcn": "https://formcn.dev/r/{name}.json",
|
||||
"@limeplay": "https://limeplay.winoffrg.dev/r/{name}.json",
|
||||
"@skiper-ui": "https://skiper-ui.com/registry/{name}.json",
|
||||
"@shadcnblocks": "https://shadcnblocks.com/r/{name}.json",
|
||||
"@shadcn-editor": "https://shadcn-editor.vercel.app/r/{name}.json",
|
||||
"@shadcn-studio": "https://shadcnstudio.com/r/{name}.json",
|
||||
"@rigidui": "https://rigidui.com/r/{name}.json",
|
||||
"@skyr": "https://ui-play.skyroc.me/r/{name}.json",
|
||||
"@retroui": "https://retroui.dev/r/{name}.json",
|
||||
"@wds": "https://wds-shadcn-registry.netlify.app/r/{name}.json",
|
||||
"@97cn": "https://97cn.itzik.co/r/{name}.json",
|
||||
"@better-upload": "https://better-upload.com/r/{name}.json",
|
||||
"@wandry-ui": "https://ui.wandry.com.ua/r/{name}.json",
|
||||
"@wigggle-ui": "https://wigggle-ui.vercel.app/r/{name}.json",
|
||||
"@pixelact-ui": "https://www.pixelactui.com/r/{name}.json",
|
||||
"@zippystarter": "https://zippystarter.com/r/{name}.json",
|
||||
"@elevenlabs-ui": "https://ui.elevenlabs.io/r/{name}.json",
|
||||
"@eldoraui": "https://eldoraui.site/r/{name}.json",
|
||||
"@intentui": "https://intentui.com/r/{name}",
|
||||
"@shadcn-map": "http://shadcn-map.vercel.app/r/{name}.json",
|
||||
"@fancy": "https://fancycomponents.dev/r/{name}.json"
|
||||
"@shadcndesign": "https://shadcndesign-free.vercel.app/r/{name}.json",
|
||||
"@ha-components": "https://hacomponents.keshuac.com/r/{name}.json",
|
||||
"@shadix-ui": "https://shadix-ui.vercel.app/r/{name}.json",
|
||||
"@utilcn": "https://utilcn.dev/r/{name}.json"
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"files": [
|
||||
{
|
||||
"path": "ui/form.tsx",
|
||||
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as LabelPrimitive from \"@radix-ui/react-label\"\nimport { Slot } from \"@radix-ui/react-slot\"\nimport {\n Controller,\n FormProvider,\n useFormContext,\n type ControllerProps,\n type FieldPath,\n type FieldValues,\n} from \"react-hook-form\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Label } from \"@/registry/default/ui/label\"\n\nconst Form = FormProvider\n\ntype FormFieldContextValue<\n TFieldValues extends FieldValues = FieldValues,\n TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>\n> = {\n name: TName\n}\n\nconst FormFieldContext = React.createContext<FormFieldContextValue>(\n {} as FormFieldContextValue\n)\n\nconst FormField = <\n TFieldValues extends FieldValues = FieldValues,\n TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>\n>({\n ...props\n}: ControllerProps<TFieldValues, TName>) => {\n return (\n <FormFieldContext.Provider value={{ name: props.name }}>\n <Controller {...props} />\n </FormFieldContext.Provider>\n )\n}\n\nconst useFormField = () => {\n const fieldContext = React.useContext(FormFieldContext)\n const itemContext = React.useContext(FormItemContext)\n const { getFieldState, formState } = useFormContext()\n\n const fieldState = getFieldState(fieldContext.name, formState)\n\n if (!fieldContext) {\n throw new Error(\"useFormField should be used within <FormField>\")\n }\n\n const { id } = itemContext\n\n return {\n id,\n name: fieldContext.name,\n formItemId: `${id}-form-item`,\n formDescriptionId: `${id}-form-item-description`,\n formMessageId: `${id}-form-item-message`,\n ...fieldState,\n }\n}\n\ntype FormItemContextValue = {\n id: string\n}\n\nconst FormItemContext = React.createContext<FormItemContextValue>(\n {} as FormItemContextValue\n)\n\nconst FormItem = React.forwardRef<\n HTMLDivElement,\n React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => {\n const id = React.useId()\n\n return (\n <FormItemContext.Provider value={{ id }}>\n <div ref={ref} className={cn(\"space-y-2\", className)} {...props} />\n </FormItemContext.Provider>\n )\n})\nFormItem.displayName = \"FormItem\"\n\nconst FormLabel = React.forwardRef<\n React.ElementRef<typeof LabelPrimitive.Root>,\n React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>\n>(({ className, ...props }, ref) => {\n const { error, formItemId } = useFormField()\n\n return (\n <Label\n ref={ref}\n className={cn(error && \"text-destructive\", className)}\n htmlFor={formItemId}\n {...props}\n />\n )\n})\nFormLabel.displayName = \"FormLabel\"\n\nconst FormControl = React.forwardRef<\n React.ElementRef<typeof Slot>,\n React.ComponentPropsWithoutRef<typeof Slot>\n>(({ ...props }, ref) => {\n const { error, formItemId, formDescriptionId, formMessageId } = useFormField()\n\n return (\n <Slot\n ref={ref}\n id={formItemId}\n aria-describedby={\n !error\n ? `${formDescriptionId}`\n : `${formDescriptionId} ${formMessageId}`\n }\n aria-invalid={!!error}\n {...props}\n />\n )\n})\nFormControl.displayName = \"FormControl\"\n\nconst FormDescription = React.forwardRef<\n HTMLParagraphElement,\n React.HTMLAttributes<HTMLParagraphElement>\n>(({ className, ...props }, ref) => {\n const { formDescriptionId } = useFormField()\n\n return (\n <p\n ref={ref}\n id={formDescriptionId}\n className={cn(\"text-sm text-muted-foreground\", className)}\n {...props}\n />\n )\n})\nFormDescription.displayName = \"FormDescription\"\n\nconst FormMessage = React.forwardRef<\n HTMLParagraphElement,\n React.HTMLAttributes<HTMLParagraphElement>\n>(({ className, children, ...props }, ref) => {\n const { error, formMessageId } = useFormField()\n const body = error ? String(error?.message ?? \"\") : children\n\n if (!body) {\n return null\n }\n\n return (\n <p\n ref={ref}\n id={formMessageId}\n className={cn(\"text-sm font-medium text-destructive\", className)}\n {...props}\n >\n {body}\n </p>\n )\n})\nFormMessage.displayName = \"FormMessage\"\n\nexport {\n useFormField,\n Form,\n FormItem,\n FormLabel,\n FormControl,\n FormDescription,\n FormMessage,\n FormField,\n}\n",
|
||||
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as LabelPrimitive from \"@radix-ui/react-label\"\nimport { Slot } from \"@radix-ui/react-slot\"\nimport {\n Controller,\n FormProvider,\n useFormContext,\n type ControllerProps,\n type FieldPath,\n type FieldValues,\n} from \"react-hook-form\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Label } from \"@/registry/default/ui/label\"\n\nconst Form = FormProvider\n\ntype FormFieldContextValue<\n TFieldValues extends FieldValues = FieldValues,\n TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>\n> = {\n name: TName\n}\n\nconst FormFieldContext = React.createContext<FormFieldContextValue | null>(null)\n\nconst FormField = <\n TFieldValues extends FieldValues = FieldValues,\n TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>\n>({\n ...props\n}: ControllerProps<TFieldValues, TName>) => {\n return (\n <FormFieldContext.Provider value={{ name: props.name }}>\n <Controller {...props} />\n </FormFieldContext.Provider>\n )\n}\n\nconst useFormField = () => {\n const fieldContext = React.useContext(FormFieldContext)\n const itemContext = React.useContext(FormItemContext)\n const { getFieldState, formState } = useFormContext()\n\n if (!fieldContext) {\n throw new Error(\"useFormField should be used within <FormField>\")\n }\n\n if (!itemContext) {\n throw new Error(\"useFormField should be used within <FormItem>\")\n }\n\n const fieldState = getFieldState(fieldContext.name, formState)\n\n const { id } = itemContext\n\n return {\n id,\n name: fieldContext.name,\n formItemId: `${id}-form-item`,\n formDescriptionId: `${id}-form-item-description`,\n formMessageId: `${id}-form-item-message`,\n ...fieldState,\n }\n}\n\ntype FormItemContextValue = {\n id: string\n}\n\nconst FormItemContext = React.createContext<FormItemContextValue | null>(null)\n\nconst FormItem = React.forwardRef<\n HTMLDivElement,\n React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => {\n const id = React.useId()\n\n return (\n <FormItemContext.Provider value={{ id }}>\n <div ref={ref} className={cn(\"space-y-2\", className)} {...props} />\n </FormItemContext.Provider>\n )\n})\nFormItem.displayName = \"FormItem\"\n\nconst FormLabel = React.forwardRef<\n React.ElementRef<typeof LabelPrimitive.Root>,\n React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>\n>(({ className, ...props }, ref) => {\n const { error, formItemId } = useFormField()\n\n return (\n <Label\n ref={ref}\n className={cn(error && \"text-destructive\", className)}\n htmlFor={formItemId}\n {...props}\n />\n )\n})\nFormLabel.displayName = \"FormLabel\"\n\nconst FormControl = React.forwardRef<\n React.ElementRef<typeof Slot>,\n React.ComponentPropsWithoutRef<typeof Slot>\n>(({ ...props }, ref) => {\n const { error, formItemId, formDescriptionId, formMessageId } = useFormField()\n\n return (\n <Slot\n ref={ref}\n id={formItemId}\n aria-describedby={\n !error\n ? `${formDescriptionId}`\n : `${formDescriptionId} ${formMessageId}`\n }\n aria-invalid={!!error}\n {...props}\n />\n )\n})\nFormControl.displayName = \"FormControl\"\n\nconst FormDescription = React.forwardRef<\n HTMLParagraphElement,\n React.HTMLAttributes<HTMLParagraphElement>\n>(({ className, ...props }, ref) => {\n const { formDescriptionId } = useFormField()\n\n return (\n <p\n ref={ref}\n id={formDescriptionId}\n className={cn(\"text-sm text-muted-foreground\", className)}\n {...props}\n />\n )\n})\nFormDescription.displayName = \"FormDescription\"\n\nconst FormMessage = React.forwardRef<\n HTMLParagraphElement,\n React.HTMLAttributes<HTMLParagraphElement>\n>(({ className, children, ...props }, ref) => {\n const { error, formMessageId } = useFormField()\n const body = error ? String(error?.message ?? \"\") : children\n\n if (!body) {\n return null\n }\n\n return (\n <p\n ref={ref}\n id={formMessageId}\n className={cn(\"text-sm font-medium text-destructive\", className)}\n {...props}\n >\n {body}\n </p>\n )\n})\nFormMessage.displayName = \"FormMessage\"\n\nexport {\n useFormField,\n Form,\n FormItem,\n FormLabel,\n FormControl,\n FormDescription,\n FormMessage,\n FormField,\n}\n",
|
||||
"type": "registry:ui",
|
||||
"target": ""
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -8,7 +8,7 @@
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/new-york-v4/ui/badge.tsx",
|
||||
"content": "import * as React from \"react\"\nimport { Slot } from \"@radix-ui/react-slot\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst badgeVariants = cva(\n \"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden\",\n {\n variants: {\n variant: {\n default:\n \"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90\",\n secondary:\n \"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90\",\n destructive:\n \"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60\",\n outline:\n \"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n },\n }\n)\n\nfunction Badge({\n className,\n variant,\n asChild = false,\n ...props\n}: React.ComponentProps<\"span\"> &\n VariantProps<typeof badgeVariants> & { asChild?: boolean }) {\n const Comp = asChild ? Slot : \"span\"\n\n return (\n <Comp\n data-slot=\"badge\"\n className={cn(badgeVariants({ variant }), className)}\n {...props}\n />\n )\n}\n\nexport { Badge, badgeVariants }\n",
|
||||
"content": "import * as React from \"react\"\nimport { Slot } from \"@radix-ui/react-slot\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst badgeVariants = cva(\n \"inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden\",\n {\n variants: {\n variant: {\n default:\n \"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90\",\n secondary:\n \"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90\",\n destructive:\n \"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60\",\n outline:\n \"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n },\n }\n)\n\nfunction Badge({\n className,\n variant,\n asChild = false,\n ...props\n}: React.ComponentProps<\"span\"> &\n VariantProps<typeof badgeVariants> & { asChild?: boolean }) {\n const Comp = asChild ? Slot : \"span\"\n\n return (\n <Comp\n data-slot=\"badge\"\n className={cn(badgeVariants({ variant }), className)}\n {...props}\n />\n )\n}\n\nexport { Badge, badgeVariants }\n",
|
||||
"type": "registry:ui"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/new-york-v4/blocks/calendar-15.tsx",
|
||||
"content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport { Calendar } from \"@/registry/new-york-v4/ui/calendar\"\n\nexport default function Calendar15() {\n const [date, setDate] = React.useState<Date | undefined>(\n new Date(2025, 5, 12)\n )\n\n return (\n <Calendar\n mode=\"single\"\n defaultMonth={date}\n selected={date}\n onSelect={setDate}\n className=\"rounded-lg border shadow-sm\"\n showWeekNumber\n />\n )\n}\n",
|
||||
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { DateRange } from \"react-day-picker\"\n\nimport { Calendar } from \"@/registry/new-york-v4/ui/calendar\"\n\nexport default function Calendar15() {\n const [dateRange, setDateRange] = React.useState<DateRange | undefined>({\n from: new Date(2025, 5, 12),\n to: new Date(2025, 5, 23),\n })\n\n return (\n <Calendar\n mode=\"range\"\n defaultMonth={dateRange?.from}\n selected={dateRange}\n onSelect={setDateRange}\n className=\"rounded-lg border shadow-sm\"\n showWeekNumber\n />\n )\n}\n",
|
||||
"type": "registry:component"
|
||||
}
|
||||
],
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -8,7 +8,7 @@
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/new-york-v4/examples/field-input.tsx",
|
||||
"content": "import {\n Field,\n FieldDescription,\n FieldGroup,\n FieldLabel,\n FieldSet,\n} from \"@/registry/new-york-v4/ui/field\"\nimport { Input } from \"@/registry/new-york-v4/ui/input\"\n\nexport default function FieldInput() {\n return (\n <div className=\"w-full max-w-md\">\n <FieldSet>\n <FieldGroup>\n <Field>\n <FieldLabel htmlFor=\"username\">Username</FieldLabel>\n <Input id=\"username\" type=\"text\" placeholder=\"Max Leiter\" />\n <FieldDescription>\n Choose a unique username for your account.\n </FieldDescription>\n </Field>\n <Field>\n <FieldLabel htmlFor=\"password\">Password</FieldLabel>\n <FieldDescription>\n Must be at least 8 characters long.\n </FieldDescription>\n <Input id=\"password\" type=\"password\" placeholder=\"********\" />\n </Field>\n </FieldGroup>\n </FieldSet>\n </div>\n )\n}\n",
|
||||
"content": "import {\n Field,\n FieldDescription,\n FieldGroup,\n FieldLabel,\n FieldSet,\n} from \"@/registry/new-york-v4/ui/field\"\nimport { Input } from \"@/registry/new-york-v4/ui/input\"\n\nexport default function FieldInput() {\n return (\n <div className=\"w-full max-w-md\">\n <FieldSet>\n <FieldGroup>\n <Field>\n <FieldLabel htmlFor=\"username\">Username</FieldLabel>\n <Input id=\"username\" type=\"text\" placeholder=\"Max Leiter\" />\n <FieldDescription>\n Choose a unique username for your account.\n </FieldDescription>\n </Field>\n <Field>\n <FieldLabel htmlFor=\"password\">Password</FieldLabel>\n <FieldDescription>\n Must be at least 8 characters long.\n </FieldDescription>\n <Input id=\"password\" type=\"password\" placeholder=\"••••••••\" />\n </Field>\n </FieldGroup>\n </FieldSet>\n </div>\n )\n}\n",
|
||||
"type": "registry:example"
|
||||
}
|
||||
]
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
15
apps/v4/public/r/styles/new-york-v4/native-select-demo.json
Normal file
15
apps/v4/public/r/styles/new-york-v4/native-select-demo.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
||||
"name": "native-select-demo",
|
||||
"type": "registry:example",
|
||||
"registryDependencies": [
|
||||
"native-select"
|
||||
],
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/new-york-v4/examples/native-select-demo.tsx",
|
||||
"content": "import {\n NativeSelect,\n NativeSelectOption,\n} from \"@/registry/new-york-v4/ui/native-select\"\n\nexport default function NativeSelectDemo() {\n return (\n <NativeSelect>\n <NativeSelectOption value=\"\">Select status</NativeSelectOption>\n <NativeSelectOption value=\"todo\">Todo</NativeSelectOption>\n <NativeSelectOption value=\"in-progress\">In Progress</NativeSelectOption>\n <NativeSelectOption value=\"done\">Done</NativeSelectOption>\n <NativeSelectOption value=\"cancelled\">Cancelled</NativeSelectOption>\n </NativeSelect>\n )\n}\n",
|
||||
"type": "registry:example"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
||||
"name": "native-select-disabled",
|
||||
"type": "registry:example",
|
||||
"registryDependencies": [
|
||||
"native-select"
|
||||
],
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/new-york-v4/examples/native-select-disabled.tsx",
|
||||
"content": "import {\n NativeSelect,\n NativeSelectOption,\n} from \"@/registry/new-york-v4/ui/native-select\"\n\nexport default function NativeSelectDisabled() {\n return (\n <NativeSelect disabled>\n <NativeSelectOption value=\"\">Select priority</NativeSelectOption>\n <NativeSelectOption value=\"low\">Low</NativeSelectOption>\n <NativeSelectOption value=\"medium\">Medium</NativeSelectOption>\n <NativeSelectOption value=\"high\">High</NativeSelectOption>\n <NativeSelectOption value=\"critical\">Critical</NativeSelectOption>\n </NativeSelect>\n )\n}\n",
|
||||
"type": "registry:example"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
||||
"name": "native-select-groups",
|
||||
"type": "registry:example",
|
||||
"registryDependencies": [
|
||||
"native-select"
|
||||
],
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/new-york-v4/examples/native-select-groups.tsx",
|
||||
"content": "import {\n NativeSelect,\n NativeSelectOptGroup,\n NativeSelectOption,\n} from \"@/registry/new-york-v4/ui/native-select\"\n\nexport default function NativeSelectGroups() {\n return (\n <NativeSelect>\n <NativeSelectOption value=\"\">Select department</NativeSelectOption>\n <NativeSelectOptGroup label=\"Engineering\">\n <NativeSelectOption value=\"frontend\">Frontend</NativeSelectOption>\n <NativeSelectOption value=\"backend\">Backend</NativeSelectOption>\n <NativeSelectOption value=\"devops\">DevOps</NativeSelectOption>\n </NativeSelectOptGroup>\n <NativeSelectOptGroup label=\"Sales\">\n <NativeSelectOption value=\"sales-rep\">Sales Rep</NativeSelectOption>\n <NativeSelectOption value=\"account-manager\">\n Account Manager\n </NativeSelectOption>\n <NativeSelectOption value=\"sales-director\">\n Sales Director\n </NativeSelectOption>\n </NativeSelectOptGroup>\n <NativeSelectOptGroup label=\"Operations\">\n <NativeSelectOption value=\"support\">\n Customer Support\n </NativeSelectOption>\n <NativeSelectOption value=\"product-manager\">\n Product Manager\n </NativeSelectOption>\n <NativeSelectOption value=\"ops-manager\">\n Operations Manager\n </NativeSelectOption>\n </NativeSelectOptGroup>\n </NativeSelect>\n )\n}\n",
|
||||
"type": "registry:example"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
||||
"name": "native-select-invalid",
|
||||
"type": "registry:example",
|
||||
"registryDependencies": [
|
||||
"native-select"
|
||||
],
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/new-york-v4/examples/native-select-invalid.tsx",
|
||||
"content": "import {\n NativeSelect,\n NativeSelectOption,\n} from \"@/registry/new-york-v4/ui/native-select\"\n\nexport default function NativeSelectInvalid() {\n return (\n <NativeSelect aria-invalid=\"true\">\n <NativeSelectOption value=\"\">Select role</NativeSelectOption>\n <NativeSelectOption value=\"admin\">Admin</NativeSelectOption>\n <NativeSelectOption value=\"editor\">Editor</NativeSelectOption>\n <NativeSelectOption value=\"viewer\">Viewer</NativeSelectOption>\n <NativeSelectOption value=\"guest\">Guest</NativeSelectOption>\n </NativeSelect>\n )\n}\n",
|
||||
"type": "registry:example"
|
||||
}
|
||||
]
|
||||
}
|
||||
12
apps/v4/public/r/styles/new-york-v4/native-select.json
Normal file
12
apps/v4/public/r/styles/new-york-v4/native-select.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
||||
"name": "native-select",
|
||||
"type": "registry:ui",
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/new-york-v4/ui/native-select.tsx",
|
||||
"content": "import * as React from \"react\"\nimport { ChevronDownIcon } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\nfunction NativeSelect({ className, ...props }: React.ComponentProps<\"select\">) {\n return (\n <div\n className=\"group/native-select relative w-fit has-[select:disabled]:opacity-50\"\n data-slot=\"native-select-wrapper\"\n >\n <select\n data-slot=\"native-select\"\n className={cn(\n \"border-input placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 dark:hover:bg-input/50 h-9 w-full min-w-0 appearance-none rounded-md border bg-transparent px-3 py-2 pr-9 text-sm shadow-xs transition-[color,box-shadow] outline-none disabled:pointer-events-none disabled:cursor-not-allowed\",\n \"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]\",\n \"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive\",\n className\n )}\n {...props}\n />\n <ChevronDownIcon\n className=\"text-muted-foreground pointer-events-none absolute top-1/2 right-3.5 size-4 -translate-y-1/2 opacity-50 select-none\"\n aria-hidden=\"true\"\n data-slot=\"native-select-icon\"\n />\n </div>\n )\n}\n\nfunction NativeSelectOption({ ...props }: React.ComponentProps<\"option\">) {\n return <option data-slot=\"native-select-option\" {...props} />\n}\n\nfunction NativeSelectOptGroup({\n className,\n ...props\n}: React.ComponentProps<\"optgroup\">) {\n return (\n <optgroup\n data-slot=\"native-select-optgroup\"\n className={cn(className)}\n {...props}\n />\n )\n}\n\nexport { NativeSelect, NativeSelectOptGroup, NativeSelectOption }\n",
|
||||
"type": "registry:ui"
|
||||
}
|
||||
]
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -786,6 +786,16 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "native-select",
|
||||
"type": "registry:ui",
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/new-york-v4/ui/native-select.tsx",
|
||||
"type": "registry:ui"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "dashboard-01",
|
||||
"type": "registry:block",
|
||||
@@ -6589,6 +6599,58 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "native-select-demo",
|
||||
"type": "registry:example",
|
||||
"registryDependencies": [
|
||||
"native-select"
|
||||
],
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/new-york-v4/examples/native-select-demo.tsx",
|
||||
"type": "registry:example"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "native-select-groups",
|
||||
"type": "registry:example",
|
||||
"registryDependencies": [
|
||||
"native-select"
|
||||
],
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/new-york-v4/examples/native-select-groups.tsx",
|
||||
"type": "registry:example"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "native-select-disabled",
|
||||
"type": "registry:example",
|
||||
"registryDependencies": [
|
||||
"native-select"
|
||||
],
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/new-york-v4/examples/native-select-disabled.tsx",
|
||||
"type": "registry:example"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "native-select-invalid",
|
||||
"type": "registry:example",
|
||||
"registryDependencies": [
|
||||
"native-select"
|
||||
],
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/new-york-v4/examples/native-select-invalid.tsx",
|
||||
"type": "registry:example"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "pagination-demo",
|
||||
"type": "registry:example",
|
||||
@@ -7224,6 +7286,19 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "toggle-group-spacing",
|
||||
"type": "registry:example",
|
||||
"registryDependencies": [
|
||||
"toggle-group"
|
||||
],
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/new-york-v4/examples/toggle-group-spacing.tsx",
|
||||
"type": "registry:example"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "toggle-demo",
|
||||
"type": "registry:example",
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/new-york-v4/examples/sheet-side.tsx",
|
||||
"content": "\"use client\"\n\nimport { Button } from \"@/registry/new-york-v4/ui/button\"\nimport { Input } from \"@/registry/new-york-v4/ui/input\"\nimport { Label } from \"@/registry/new-york-v4/ui/label\"\nimport {\n Sheet,\n SheetClose,\n SheetContent,\n SheetDescription,\n SheetFooter,\n SheetHeader,\n SheetTitle,\n SheetTrigger,\n} from \"@/registry/new-york-v4/ui/sheet\"\n\nconst SHEET_SIDES = [\"top\", \"right\", \"bottom\", \"left\"] as const\n\ntype SheetSide = (typeof SHEET_SIDES)[number]\n\nexport default function SheetSide() {\n return (\n <div className=\"grid grid-cols-2 gap-2\">\n {SHEET_SIDES.map((side) => (\n <Sheet key={side}>\n <SheetTrigger asChild>\n <Button variant=\"outline\">{side}</Button>\n </SheetTrigger>\n <SheetContent side={side}>\n <SheetHeader>\n <SheetTitle>Edit profile</SheetTitle>\n <SheetDescription>\n Make changes to your profile here. Click save when you're done.\n </SheetDescription>\n </SheetHeader>\n <div className=\"grid gap-4 py-4\">\n <div className=\"grid grid-cols-4 items-center gap-4\">\n <Label htmlFor=\"name\" className=\"text-right\">\n Name\n </Label>\n <Input id=\"name\" value=\"Pedro Duarte\" className=\"col-span-3\" />\n </div>\n <div className=\"grid grid-cols-4 items-center gap-4\">\n <Label htmlFor=\"username\" className=\"text-right\">\n Username\n </Label>\n <Input id=\"username\" value=\"@peduarte\" className=\"col-span-3\" />\n </div>\n </div>\n <SheetFooter>\n <SheetClose asChild>\n <Button type=\"submit\">Save changes</Button>\n </SheetClose>\n </SheetFooter>\n </SheetContent>\n </Sheet>\n ))}\n </div>\n )\n}\n",
|
||||
"content": "\"use client\"\n\nimport { Button } from \"@/registry/new-york-v4/ui/button\"\nimport { Input } from \"@/registry/new-york-v4/ui/input\"\nimport { Label } from \"@/registry/new-york-v4/ui/label\"\nimport {\n Sheet,\n SheetClose,\n SheetContent,\n SheetDescription,\n SheetFooter,\n SheetHeader,\n SheetTitle,\n SheetTrigger,\n} from \"@/registry/new-york-v4/ui/sheet\"\n\nconst SHEET_SIDES = [\"top\", \"right\", \"bottom\", \"left\"] as const\n\ntype SheetSide = (typeof SHEET_SIDES)[number]\n\nexport default function SheetSide() {\n return (\n <div className=\"grid grid-cols-2 gap-2\">\n {SHEET_SIDES.map((side) => (\n <Sheet key={side}>\n <SheetTrigger asChild>\n <Button variant=\"outline\">{side}</Button>\n </SheetTrigger>\n <SheetContent side={side}>\n <SheetHeader>\n <SheetTitle>Edit profile</SheetTitle>\n <SheetDescription>\n Make changes to your profile here. Click save when you're\n done.\n </SheetDescription>\n </SheetHeader>\n <div className=\"grid gap-4 py-4\">\n <div className=\"grid grid-cols-4 items-center gap-4\">\n <Label htmlFor=\"name\" className=\"text-right\">\n Name\n </Label>\n <Input id=\"name\" value=\"Pedro Duarte\" className=\"col-span-3\" />\n </div>\n <div className=\"grid grid-cols-4 items-center gap-4\">\n <Label htmlFor=\"username\" className=\"text-right\">\n Username\n </Label>\n <Input id=\"username\" value=\"@peduarte\" className=\"col-span-3\" />\n </div>\n </div>\n <SheetFooter>\n <SheetClose asChild>\n <Button type=\"submit\">Save changes</Button>\n </SheetClose>\n </SheetFooter>\n </SheetContent>\n </Sheet>\n ))}\n </div>\n )\n}\n",
|
||||
"type": "registry:example"
|
||||
}
|
||||
]
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user