mirror of
https://github.com/shadcn-ui/ui.git
synced 2026-06-18 13:21:35 +00:00
Compare commits
49 Commits
shadcn@3.4
...
shadcn@3.4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a4764ed91 | ||
|
|
e934d4645b | ||
|
|
08b8e499d8 | ||
|
|
69402b3579 | ||
|
|
679c852254 | ||
|
|
d478412e44 | ||
|
|
d5c8a25150 | ||
|
|
26433a651c | ||
|
|
c3da716e94 | ||
|
|
b2572d0287 | ||
|
|
b83f042416 | ||
|
|
6567897393 | ||
|
|
2675fa3941 | ||
|
|
fbda67c88c | ||
|
|
e8674ee848 | ||
|
|
adb66f4d43 | ||
|
|
3afb46eaf6 | ||
|
|
7cd019ad36 | ||
|
|
bea7d30536 | ||
|
|
40c3ff513a | ||
|
|
89ebfdce47 | ||
|
|
b83023034a | ||
|
|
6a534d7954 | ||
|
|
ef1987ded9 | ||
|
|
77bf7d28b4 | ||
|
|
41f4f7357d | ||
|
|
bc99818e04 | ||
|
|
162ba7b13c | ||
|
|
f12db1e3a2 | ||
|
|
ce3e2b1df8 | ||
|
|
dcfe911b33 | ||
|
|
7210a4919a | ||
|
|
d198908510 | ||
|
|
b0b1cd1f0d | ||
|
|
f3d70724b6 | ||
|
|
407e9c6802 | ||
|
|
c67e630521 | ||
|
|
f494411953 | ||
|
|
a43c1d1342 | ||
|
|
607a6fd127 | ||
|
|
fbcc665b49 | ||
|
|
7ddcf31e43 | ||
|
|
3e39163b08 | ||
|
|
e311fdae04 | ||
|
|
26640d9d88 | ||
|
|
3e20c228da | ||
|
|
0810c0e1a2 | ||
|
|
1205ea5445 | ||
|
|
4430ab8bab |
78
.github/workflows/deprecated.yml
vendored
Normal file
78
.github/workflows/deprecated.yml
vendored
Normal file
@@ -0,0 +1,78 @@
|
||||
name: Deprecated
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, synchronize]
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
deprecated:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout PR
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Get changed files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@v46
|
||||
with:
|
||||
files: |
|
||||
apps/www/**
|
||||
files_ignore: |
|
||||
apps/www/public/r/**
|
||||
base_sha: ${{ github.event.pull_request.base.sha }}
|
||||
sha: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Comment on PR if www files changed
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const changedFiles = `${{ steps.changed-files.outputs.all_changed_files }}`.split(' ');
|
||||
const wwwFiles = changedFiles.filter(file =>
|
||||
file.startsWith('apps/www/') &&
|
||||
!file.startsWith('apps/www/public/r/') &&
|
||||
file !== 'apps/www/package.json'
|
||||
);
|
||||
|
||||
if (wwwFiles.length > 0) {
|
||||
const comment = `Looks like this PR modifies files in \`apps/www\`, which is deprecated.
|
||||
|
||||
Consider applying the change to \`apps/v4\` if relevant.`;
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
body: comment
|
||||
});
|
||||
|
||||
// Add deprecated label
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
labels: ['deprecated']
|
||||
});
|
||||
} else {
|
||||
// Remove deprecated label if no www files are changed
|
||||
try {
|
||||
await github.rest.issues.removeLabel({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
name: 'deprecated'
|
||||
});
|
||||
} catch (error) {
|
||||
// Label doesn't exist, which is fine
|
||||
console.log('Deprecated label not found, skipping removal');
|
||||
}
|
||||
}
|
||||
@@ -142,13 +142,7 @@ const chartConfig = {
|
||||
|
||||
export function ChartAreaInteractive() {
|
||||
const isMobile = useIsMobile()
|
||||
const [timeRange, setTimeRange] = React.useState("90d")
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isMobile) {
|
||||
setTimeRange("7d")
|
||||
}
|
||||
}, [isMobile])
|
||||
const [timeRange, setTimeRange] = React.useState("7d")
|
||||
|
||||
const filteredData = chartData.filter((item) => {
|
||||
const date = new Date(item.date)
|
||||
|
||||
@@ -9,12 +9,14 @@ export function ComponentPreviewTabs({
|
||||
className,
|
||||
align = "center",
|
||||
hideCode = false,
|
||||
chromeLessOnMobile = false,
|
||||
component,
|
||||
source,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & {
|
||||
align?: "center" | "start" | "end"
|
||||
hideCode?: boolean
|
||||
chromeLessOnMobile?: boolean
|
||||
component: React.ReactNode
|
||||
source: React.ReactNode
|
||||
}) {
|
||||
@@ -51,7 +53,8 @@ export function ComponentPreviewTabs({
|
||||
</Tabs>
|
||||
<div
|
||||
data-tab={tab}
|
||||
className="data-[tab=code]:border-code relative rounded-lg border md:-mx-1"
|
||||
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"
|
||||
@@ -61,7 +64,8 @@ export function ComponentPreviewTabs({
|
||||
<div
|
||||
data-align={align}
|
||||
className={cn(
|
||||
"preview flex h-[450px] w-full justify-center p-10 data-[align=center]:items-center data-[align=end]:items-end data-[align=start]:items-start"
|
||||
"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"
|
||||
)}
|
||||
>
|
||||
{component}
|
||||
|
||||
@@ -10,6 +10,7 @@ export function ComponentPreview({
|
||||
className,
|
||||
align = "center",
|
||||
hideCode = false,
|
||||
chromeLessOnMobile = false,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & {
|
||||
name: string
|
||||
@@ -17,12 +18,13 @@ export function ComponentPreview({
|
||||
description?: string
|
||||
hideCode?: boolean
|
||||
type?: "block" | "component" | "example"
|
||||
chromeLessOnMobile?: boolean
|
||||
}) {
|
||||
const Component = Index[name]?.component
|
||||
|
||||
if (!Component) {
|
||||
return (
|
||||
<p className="text-muted-foreground text-sm">
|
||||
<p className="text-muted-foreground mt-6 text-sm">
|
||||
Component{" "}
|
||||
<code className="bg-muted relative rounded px-[0.3rem] py-[0.2rem] font-mono text-sm">
|
||||
{name}
|
||||
@@ -63,6 +65,7 @@ export function ComponentPreview({
|
||||
hideCode={hideCode}
|
||||
component={<Component />}
|
||||
source={<ComponentSource name={name} collapsible={false} />}
|
||||
chromeLessOnMobile={chromeLessOnMobile}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -43,6 +43,14 @@ export async function ComponentSource({
|
||||
return null
|
||||
}
|
||||
|
||||
// Fix imports.
|
||||
// Replace @/registry/new-york-v4/ with @/components/.
|
||||
code = code.replaceAll("@/registry/new-york-v4/", "@/components/")
|
||||
|
||||
// Replace export default with export.
|
||||
code = code.replaceAll("export default", "export")
|
||||
code = code.replaceAll("/* eslint-disable react/no-children-prop */\n", "")
|
||||
|
||||
const lang = language ?? title?.split(".").pop() ?? "tsx"
|
||||
const highlightedCode = await highlightCode(code, lang)
|
||||
|
||||
|
||||
@@ -31,6 +31,10 @@ const TOP_LEVEL_SECTIONS = [
|
||||
name: "MCP Server",
|
||||
href: "/docs/mcp",
|
||||
},
|
||||
{
|
||||
name: "Forms",
|
||||
href: "/docs/forms",
|
||||
},
|
||||
{
|
||||
name: "Changelog",
|
||||
href: "/docs/changelog",
|
||||
|
||||
@@ -29,6 +29,10 @@ const TOP_LEVEL_SECTIONS = [
|
||||
name: "MCP Server",
|
||||
href: "/docs/mcp",
|
||||
},
|
||||
{
|
||||
name: "Forms",
|
||||
href: "/docs/forms",
|
||||
},
|
||||
{
|
||||
name: "Changelog",
|
||||
href: "/docs/changelog",
|
||||
|
||||
@@ -34,15 +34,24 @@ import { Spinner } from "@/components/ui/spinner"
|
||||
|
||||
Here's what it looks like:
|
||||
|
||||
<ComponentPreview name="spinner-basic" className="[&_.preview]:h-[250px]" />
|
||||
<ComponentPreview
|
||||
name="spinner-basic"
|
||||
className="[&_.preview]:h-[250px] [&_pre]:!h-[250px]"
|
||||
/>
|
||||
|
||||
Here's what it looks like in a button:
|
||||
|
||||
<ComponentPreview name="spinner-button" className="[&_.preview]:h-[250px]" />
|
||||
<ComponentPreview
|
||||
name="spinner-button"
|
||||
className="[&_.preview]:h-[250px] [&_pre]:!h-[250px]"
|
||||
/>
|
||||
|
||||
You can edit the code and replace it with your own spinner.
|
||||
|
||||
<ComponentPreview name="spinner-custom" className="[&_.preview]:h-[250px]" />
|
||||
<ComponentPreview
|
||||
name="spinner-custom"
|
||||
className="[&_.preview]:h-[250px] [&_pre]:!h-[250px]"
|
||||
/>
|
||||
|
||||
### Kbd
|
||||
|
||||
@@ -65,7 +74,10 @@ Use `KbdGroup` to group keyboard keys together.
|
||||
</KbdGroup>
|
||||
```
|
||||
|
||||
<ComponentPreview name="kbd-demo" className="[&_.preview]:h-[250px]" />
|
||||
<ComponentPreview
|
||||
name="kbd-demo"
|
||||
className="[&_.preview]:h-[250px] [&_pre]:!h-[250px]"
|
||||
/>
|
||||
|
||||
You can add it to buttons, tooltips, input groups, and more.
|
||||
|
||||
@@ -73,7 +85,10 @@ You can add it to buttons, tooltips, input groups, and more.
|
||||
|
||||
I got a lot of requests for this one: Button Group. It's a container that groups related buttons together with consistent styling. Great for action groups, split buttons, and more.
|
||||
|
||||
<ComponentPreview name="button-group-demo" className="[&_.preview]:h-[250px]" />
|
||||
<ComponentPreview
|
||||
name="button-group-demo"
|
||||
className="[&_.preview]:h-[250px] [&_pre]:!h-[250px]"
|
||||
/>
|
||||
|
||||
Here's the code:
|
||||
|
||||
@@ -107,14 +122,14 @@ Use `ButtonGroupSeparator` to create split buttons. Classic dropdown pattern.
|
||||
|
||||
<ComponentPreview
|
||||
name="button-group-dropdown"
|
||||
className="[&_.preview]:h-[250px]"
|
||||
className="[&_.preview]:h-[250px] [&_pre]:!h-[250px]"
|
||||
/>
|
||||
|
||||
You can also use it to add prefix or suffix buttons and text to inputs.
|
||||
|
||||
<ComponentPreview
|
||||
name="button-group-select"
|
||||
className="[&_.preview]:h-[250px]"
|
||||
className="[&_.preview]:h-[250px] [&_pre]:!h-[250px]"
|
||||
/>
|
||||
|
||||
```tsx showLineNumbers
|
||||
@@ -148,31 +163,37 @@ import {
|
||||
|
||||
Here's a preview with icons:
|
||||
|
||||
<ComponentPreview name="input-group-icon" className="[&_.preview]:h-[300px]" />
|
||||
<ComponentPreview
|
||||
name="input-group-icon"
|
||||
className="[&_.preview]:h-[300px] [&_pre]:!h-[300px]"
|
||||
/>
|
||||
|
||||
You can also add buttons to the input group.
|
||||
|
||||
<ComponentPreview
|
||||
name="input-group-button"
|
||||
className="[&_.preview]:h-[300px]"
|
||||
className="[&_.preview]:h-[300px] [&_pre]:!h-[300px]"
|
||||
/>
|
||||
|
||||
Or text, labels, tooltips,...
|
||||
|
||||
<ComponentPreview name="input-group-text" className="[&_.preview]:h-[350px]" />
|
||||
<ComponentPreview
|
||||
name="input-group-text"
|
||||
className="[&_.preview]:h-[350px] [&_pre]:!h-[350px]"
|
||||
/>
|
||||
|
||||
It also works with textareas so you can build really complex components with lots of knobs and dials or yet another prompt form.
|
||||
|
||||
<ComponentPreview
|
||||
name="input-group-textarea"
|
||||
className="[&_.preview]:h-[450px]"
|
||||
className="[&_.preview]:h-[450px] [&_pre]:!h-[450px]"
|
||||
/>
|
||||
|
||||
Oh here are some cool ones with spinners:
|
||||
|
||||
<ComponentPreview
|
||||
name="input-group-spinner"
|
||||
className="[&_.preview]:h-[350px]"
|
||||
className="[&_.preview]:h-[350px] [&_pre]:!h-[350px]"
|
||||
/>
|
||||
|
||||
### Field
|
||||
@@ -202,15 +223,24 @@ Here's a basic field with an input:
|
||||
</Field>
|
||||
```
|
||||
|
||||
<ComponentPreview name="field-input" className="[&_.preview]:h-[350px]" />
|
||||
<ComponentPreview
|
||||
name="field-input"
|
||||
className="[&_.preview]:h-[350px] [&_pre]:!h-[350px]"
|
||||
/>
|
||||
|
||||
It works with all form controls. Inputs, textareas, selects, checkboxes, radios, switches, sliders, you name it. Here's a full example:
|
||||
|
||||
<ComponentPreview name="field-demo" className="[&_.preview]:h-[850px]" />
|
||||
<ComponentPreview
|
||||
name="field-demo"
|
||||
className="[&_.preview]:h-[850px] [&_pre]:!h-[850px]"
|
||||
/>
|
||||
|
||||
Here are some checkbox fields:
|
||||
|
||||
<ComponentPreview name="field-checkbox" className="[&_.preview]:h-[500px]" />
|
||||
<ComponentPreview
|
||||
name="field-checkbox"
|
||||
className="[&_.preview]:h-[500px] [&_pre]:!h-[500px]"
|
||||
/>
|
||||
|
||||
You can group fields together using `FieldGroup` and `FieldSet`. Perfect for
|
||||
multi-section forms.
|
||||
@@ -225,16 +255,25 @@ multi-section forms.
|
||||
</FieldSet>
|
||||
```
|
||||
|
||||
<ComponentPreview name="field-fieldset" className="[&_.preview]:h-[500px]" />
|
||||
<ComponentPreview
|
||||
name="field-fieldset"
|
||||
className="[&_.preview]:h-[500px] [&_pre]:!h-[500px]"
|
||||
/>
|
||||
|
||||
Making it responsive is easy. Use `orientation="responsive"` and it switches
|
||||
between vertical and horizontal layouts based on container width. Done.
|
||||
|
||||
<ComponentPreview name="field-responsive" className="[&_.preview]:h-[600px]" />
|
||||
<ComponentPreview
|
||||
name="field-responsive"
|
||||
className="[&_.preview]:h-[600px] [&_pre]:!h-[600px]"
|
||||
/>
|
||||
|
||||
Wait here's more. Wrap your fields in `FieldLabel` to create a selectable field group. Really easy. And it looks great.
|
||||
|
||||
<ComponentPreview name="field-choice-card" className="[&_.preview]:h-[600px]" />
|
||||
<ComponentPreview
|
||||
name="field-choice-card"
|
||||
className="[&_.preview]:h-[600px] [&_pre]:!h-[600px]"
|
||||
/>
|
||||
|
||||
### Item
|
||||
|
||||
@@ -268,26 +307,26 @@ Here's a basic item:
|
||||
|
||||
<ComponentPreview
|
||||
name="item-demo"
|
||||
className="[&_.preview]:h-[300px] [&_.preview]:p-4"
|
||||
className="[&_.preview]:h-[300px] [&_.preview]:p-4 [&_pre]:!h-[300px]"
|
||||
/>
|
||||
|
||||
You can add icons, avatars, or images to the item.
|
||||
|
||||
<ComponentPreview
|
||||
name="item-icon"
|
||||
className="[&_.preview]:h-[300px] [&_.preview]:p-4"
|
||||
className="[&_.preview]:h-[300px] [&_.preview]:p-4 [&_pre]:!h-[300px]"
|
||||
/>
|
||||
|
||||
<ComponentPreview
|
||||
name="item-avatar"
|
||||
className="[&_.preview]:h-[300px] [&_.preview]:p-4"
|
||||
className="[&_.preview]:h-[300px] [&_.preview]:p-4 [&_pre]:!h-[300px]"
|
||||
/>
|
||||
|
||||
And here's what a list of items looks like with `ItemGroup`:
|
||||
|
||||
<ComponentPreview
|
||||
name="item-group"
|
||||
className="[&_.preview]:h-[500px] [&_.preview]:p-4"
|
||||
className="[&_.preview]:h-[500px] [&_.preview]:p-4 [&_pre]:!h-[500px]"
|
||||
/>
|
||||
|
||||
Need it as a link? Use the `asChild` prop:
|
||||
@@ -308,7 +347,7 @@ Need it as a link? Use the `asChild` prop:
|
||||
|
||||
<ComponentPreview
|
||||
name="item-link"
|
||||
className="[&_.preview]:h-[400px] [&_.preview]:p-4"
|
||||
className="[&_.preview]:h-[400px] [&_.preview]:p-4 [&_pre]:!h-[400px]"
|
||||
/>
|
||||
|
||||
### Empty
|
||||
@@ -342,16 +381,22 @@ Here's how you use it:
|
||||
|
||||
<ComponentPreview
|
||||
name="empty-demo"
|
||||
className="[&_.preview]:h-[400px] [&_.preview]:p-4"
|
||||
className="[&_.preview]:h-[400px] [&_.preview]:p-4 [&_pre]:!h-[400px]"
|
||||
/>
|
||||
|
||||
You can use it with avatars:
|
||||
|
||||
<ComponentPreview name="empty-avatar" className="[&_.preview]:h-[400px]" />
|
||||
<ComponentPreview
|
||||
name="empty-avatar"
|
||||
className="[&_.preview]:h-[400px] [&_pre]:!h-[400px]"
|
||||
/>
|
||||
|
||||
Or with input groups for things like search results or email subscriptions:
|
||||
|
||||
<ComponentPreview name="empty-input-group" className="[&_.preview]:h-[450px]" />
|
||||
<ComponentPreview
|
||||
name="empty-input-group"
|
||||
className="[&_.preview]:h-[450px] [&_pre]:!h-[450px]"
|
||||
/>
|
||||
|
||||
That's it. Seven new components. Works with all your libraries. Ready for your projects.
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"blocks",
|
||||
"figma",
|
||||
"changelog",
|
||||
"[llms.txt](/llms.txt)",
|
||||
"legacy"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ import { Badge } from "@/components/ui/badge"
|
||||
You can use the `asChild` prop to make another component look like a badge. Here's an example of a link that looks like a badge.
|
||||
|
||||
```tsx showLineNumbers
|
||||
import { Link } from "next/link"
|
||||
import Link from "next/link"
|
||||
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
|
||||
|
||||
@@ -180,7 +180,7 @@ To use a custom link component from your routing library, you can use the `asChi
|
||||
/>
|
||||
|
||||
```tsx showLineNumbers {1,8-10}
|
||||
import { Link } from "next/link"
|
||||
import Link from "next/link"
|
||||
|
||||
...
|
||||
|
||||
|
||||
@@ -71,7 +71,20 @@ import { Button } from "@/components/ui/button"
|
||||
<Button variant="outline">Button</Button>
|
||||
```
|
||||
|
||||
---
|
||||
## Cursor
|
||||
|
||||
Tailwind v4 [switched](https://tailwindcss.com/docs/upgrade-guide#buttons-use-the-default-cursor) from `cursor: pointer` to `cursor: default` for the button component.
|
||||
|
||||
If you want to keep the `cursor: pointer` behavior, add the following code to your CSS file:
|
||||
|
||||
```css showLineNumbers title="globals.css"
|
||||
@layer base {
|
||||
button:not(:disabled),
|
||||
[role="button"]:not(:disabled) {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -265,7 +278,7 @@ To create a button group, use the `ButtonGroup` component. See the [Button Group
|
||||
You can use the `asChild` prop to make another component look like a button. Here's an example of a link that looks like a button.
|
||||
|
||||
```tsx showLineNumbers
|
||||
import { Link } from "next/link"
|
||||
import Link from "next/link"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
|
||||
|
||||
@@ -56,9 +56,3 @@ import { Checkbox } from "@/components/ui/checkbox"
|
||||
```tsx
|
||||
<Checkbox />
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Form
|
||||
|
||||
<ComponentPreview name="checkbox-form-multiple" />
|
||||
|
||||
@@ -143,7 +143,3 @@ export function ExampleCombobox() {
|
||||
You can create a responsive combobox by using the `<Popover />` on desktop and the `<Drawer />` components on mobile.
|
||||
|
||||
<ComponentPreview name="combobox-responsive" />
|
||||
|
||||
### Form
|
||||
|
||||
<ComponentPreview name="combobox-form" />
|
||||
|
||||
@@ -94,7 +94,3 @@ This component uses the `chrono-node` library to parse natural language dates.
|
||||
title="Natural Language Picker"
|
||||
description="A calendar with natural language picker."
|
||||
/>
|
||||
|
||||
### Form
|
||||
|
||||
<ComponentPreview name="date-picker-form" />
|
||||
|
||||
@@ -93,3 +93,19 @@ import {
|
||||
name="dropdown-menu-radio-group"
|
||||
description="A dropdown menu with radio items."
|
||||
/>
|
||||
|
||||
### Dialog
|
||||
|
||||
This example shows how to open a dialog from a dropdown menu.
|
||||
|
||||
Use `modal={false}` on the `DropdownMenu` component.
|
||||
|
||||
```tsx showLineNumbers
|
||||
<DropdownMenu modal={false}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline">Actions</Button>
|
||||
</DropdownMenuTrigger>
|
||||
</DropdownMenu>
|
||||
```
|
||||
|
||||
<ComponentPreview name="dropdown-menu-dialog" />
|
||||
|
||||
@@ -57,9 +57,9 @@ import {
|
||||
<EmptyMedia variant="icon">
|
||||
<Icon />
|
||||
</EmptyMedia>
|
||||
<EmptyTitle>No data</EmptyTitle>
|
||||
<EmptyDescription>No data found</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
<EmptyTitle>No data</EmptyTitle>
|
||||
<EmptyDescription>No data found</EmptyDescription>
|
||||
<EmptyContent>
|
||||
<Button>Add data</Button>
|
||||
</EmptyContent>
|
||||
|
||||
@@ -98,6 +98,10 @@ The `Field` family is designed for composing accessible forms. A typical field i
|
||||
- `FieldContent` is a flex column that groups label and description. Not required if you have no description.
|
||||
- Wrap related fields with `FieldGroup`, and use `FieldSet` with `FieldLegend` for semantic grouping.
|
||||
|
||||
## Form
|
||||
|
||||
See the [Form](/docs/forms) documentation for building forms with the `Field` component and [React Hook Form](/docs/forms/react-hook-form) or [Tanstack Form](/docs/forms/tanstack-form).
|
||||
|
||||
## Examples
|
||||
|
||||
### Input
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
---
|
||||
title: React Hook Form
|
||||
title: Form
|
||||
description: Building forms with React Hook Form and Zod.
|
||||
links:
|
||||
doc: https://react-hook-form.com
|
||||
---
|
||||
|
||||
import { InfoIcon } from "lucide-react"
|
||||
|
||||
<Callout icon={<InfoIcon />} title="We are not actively developing this component anymore.">
|
||||
|
||||
The Form component is an abstraction over the `react-hook-form` library. Going forward, we recommend using the [`<Field />`](/docs/components/field) component to build forms. See the [Form](/docs/forms) documentation for more information.
|
||||
|
||||
</Callout>
|
||||
|
||||
Forms are tricky. They are one of the most common things you'll build in a web application, but also one of the most complex.
|
||||
|
||||
Well-designed HTML forms are:
|
||||
@@ -119,8 +127,6 @@ npm install @radix-ui/react-label @radix-ui/react-slot react-hook-form @hookform
|
||||
|
||||
## Usage
|
||||
|
||||
<Steps>
|
||||
|
||||
### Create a form schema
|
||||
|
||||
Define the shape of your form using a Zod schema. You can read more about using Zod in the [Zod documentation](https://zod.dev).
|
||||
@@ -233,23 +239,3 @@ export function ProfileForm() {
|
||||
### Done
|
||||
|
||||
That's it. You now have a fully accessible form that is type-safe with client-side validation.
|
||||
|
||||
<ComponentPreview
|
||||
name="input-form"
|
||||
className="[&_[role=tablist]]:hidden [&>div>div:first-child]:hidden"
|
||||
/>
|
||||
|
||||
</Steps>
|
||||
|
||||
## Examples
|
||||
|
||||
See the following links for more examples on how to use the `<Form />` component with other components:
|
||||
|
||||
- [Checkbox](/docs/components/checkbox#form)
|
||||
- [Date Picker](/docs/components/date-picker#form)
|
||||
- [Input](/docs/components/input#form)
|
||||
- [Radio Group](/docs/components/radio-group#form)
|
||||
- [Select](/docs/components/select#form)
|
||||
- [Switch](/docs/components/switch#form)
|
||||
- [Textarea](/docs/components/textarea#form)
|
||||
- [Combobox](/docs/components/combobox#form)
|
||||
|
||||
@@ -253,3 +253,9 @@ All other props are passed through to the underlying `<Textarea />` component.
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
```
|
||||
|
||||
## Changelog
|
||||
|
||||
### 2025-10-06 `InputGroup`
|
||||
|
||||
Add the `min-w-0` class to the `InputGroup` component. See [diff](https://github.com/shadcn-ui/ui/pull/8341/files#diff-0e2ee95d0050ca4c5d82339df86c54e14a6739dc4638fdda0eec8f73aebc2da9).
|
||||
|
||||
@@ -94,10 +94,6 @@ import { Input } from "@/components/ui/input"
|
||||
description="An input component with a button."
|
||||
/>
|
||||
|
||||
### Form
|
||||
|
||||
<ComponentPreview name="input-form" />
|
||||
|
||||
## Changelog
|
||||
|
||||
### 2025-09-18 Remove `flex` class
|
||||
|
||||
@@ -83,7 +83,7 @@ import {
|
||||
You can use the `asChild` prop to make another component look like a navigation menu trigger. Here's an example of a link that looks like a navigation menu trigger.
|
||||
|
||||
```tsx showLineNumbers title="components/example-navigation-menu.tsx"
|
||||
import { Link } from "next/link"
|
||||
import Link from "next/link"
|
||||
|
||||
export function NavigationMenuDemo() {
|
||||
return (
|
||||
|
||||
@@ -69,9 +69,3 @@ import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
|
||||
</div>
|
||||
</RadioGroup>
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Form
|
||||
|
||||
<ComponentPreview name="radio-group-form" />
|
||||
|
||||
@@ -84,7 +84,3 @@ import {
|
||||
name="select-scrollable"
|
||||
description="A select component with a scrollable list of options."
|
||||
/>
|
||||
|
||||
### Form
|
||||
|
||||
<ComponentPreview name="select-form" />
|
||||
|
||||
@@ -68,7 +68,7 @@ npm install sonner next-themes
|
||||
|
||||
<Step>Add the Toaster component</Step>
|
||||
|
||||
```tsx title="app/layout.tsx" {1,9}
|
||||
```tsx showLineNumbers title="app/layout.tsx" {1,8}
|
||||
import { Toaster } from "@/components/ui/sonner"
|
||||
|
||||
export default function RootLayout({ children }) {
|
||||
@@ -76,8 +76,8 @@ export default function RootLayout({ children }) {
|
||||
<html lang="en">
|
||||
<head />
|
||||
<body>
|
||||
<main>{children}</main>
|
||||
<Toaster />
|
||||
<main>{children}</main>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
@@ -99,3 +99,56 @@ import { toast } from "sonner"
|
||||
```tsx
|
||||
toast("Event has been created.")
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
<ComponentPreview name="sonner-types" />
|
||||
|
||||
## Changelog
|
||||
|
||||
### 2025-10-13 Icons
|
||||
|
||||
We've updated the Sonner component to use icons from `lucide`. Update your `sonner.tsx` file to use the new icons.
|
||||
|
||||
```tsx showLineNumbers title="components/ui/sonner.tsx" {3-9,20-26}
|
||||
"use client"
|
||||
|
||||
import {
|
||||
CircleCheckIcon,
|
||||
InfoIcon,
|
||||
Loader2Icon,
|
||||
OctagonXIcon,
|
||||
TriangleAlertIcon,
|
||||
} from "lucide-react"
|
||||
import { useTheme } from "next-themes"
|
||||
import { Toaster as Sonner, ToasterProps } from "sonner"
|
||||
|
||||
const Toaster = ({ ...props }: ToasterProps) => {
|
||||
const { theme = "system" } = useTheme()
|
||||
|
||||
return (
|
||||
<Sonner
|
||||
theme={theme as ToasterProps["theme"]}
|
||||
className="toaster group"
|
||||
icons={{
|
||||
success: <CircleCheckIcon className="size-4" />,
|
||||
info: <InfoIcon className="size-4" />,
|
||||
warning: <TriangleAlertIcon className="size-4" />,
|
||||
error: <OctagonXIcon className="size-4" />,
|
||||
loading: <Loader2Icon className="size-4 animate-spin" />,
|
||||
}}
|
||||
style={
|
||||
{
|
||||
"--normal-bg": "var(--popover)",
|
||||
"--normal-text": "var(--popover-foreground)",
|
||||
"--normal-border": "var(--border)",
|
||||
"--border-radius": "var(--radius)",
|
||||
} as React.CSSProperties
|
||||
}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Toaster }
|
||||
```
|
||||
|
||||
@@ -56,9 +56,3 @@ import { Switch } from "@/components/ui/switch"
|
||||
```tsx
|
||||
<Switch />
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Form
|
||||
|
||||
<ComponentPreview name="switch-form" />
|
||||
|
||||
@@ -79,7 +79,3 @@ import { Textarea } from "@/components/ui/textarea"
|
||||
name="textarea-with-button"
|
||||
description="A textarea with a button"
|
||||
/>
|
||||
|
||||
### Form
|
||||
|
||||
<ComponentPreview name="textarea-form" />
|
||||
|
||||
45
apps/v4/content/docs/forms/index.mdx
Normal file
45
apps/v4/content/docs/forms/index.mdx
Normal file
@@ -0,0 +1,45 @@
|
||||
---
|
||||
title: Forms
|
||||
description: Build forms with React and shadcn/ui.
|
||||
---
|
||||
|
||||
import { ClipboardListIcon, InfoIcon } from "lucide-react"
|
||||
|
||||
## Pick Your Framework
|
||||
|
||||
Start by selecting your framework. Then follow the instructions to learn how to build forms with shadcn/ui and the form library of your choice.
|
||||
|
||||
<div className="mt-8 grid gap-4 sm:grid-cols-2 sm:gap-6">
|
||||
<LinkedCard href="/docs/forms/react-hook-form">
|
||||
<ClipboardListIcon className="size-10" />
|
||||
<p className="mt-2 font-medium">React Hook Form</p>
|
||||
</LinkedCard>
|
||||
<LinkedCard href="/docs/forms/tanstack-form">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
className="size-10"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M6.93 13.688a.343.343 0 0 1 .468.132l.063.106c.48.851.98 1.66 1.5 2.426a35.65 35.65 0 0 0 2.074 2.742.345.345 0 0 1-.039.484l-.074.066c-2.543 2.223-4.191 2.665-4.953 1.333-.746-1.305-.477-3.672.808-7.11a.344.344 0 0 1 .153-.18ZM17.75 16.3a.34.34 0 0 1 .395.27l.02.1c.628 3.286.187 4.93-1.325 4.93-1.48 0-3.36-1.402-5.649-4.203a.327.327 0 0 1-.074-.222c0-.188.156-.34.344-.34h.121a32.984 32.984 0 0 0 2.809-.098c1.07-.086 2.191-.23 3.359-.437zm.871-6.977a.353.353 0 0 1 .445-.21l.102.034c3.262 1.11 4.504 2.332 3.719 3.664-.766 1.305-2.993 2.254-6.684 2.848a.362.362 0 0 1-.238-.047.343.343 0 0 1-.125-.476l.062-.106a34.07 34.07 0 0 0 1.367-2.523c.477-.989.93-2.051 1.352-3.184zM7.797 8.34a.362.362 0 0 1 .238.047.343.343 0 0 1 .125.476l-.062.106a34.088 34.088 0 0 0-1.367 2.523c-.477.988-.93 2.051-1.352 3.184a.353.353 0 0 1-.445.21l-.102-.034C1.57 13.742.328 12.52 1.113 11.188 1.88 9.883 4.106 8.934 7.797 8.34Zm5.281-3.984c2.543-2.223 4.192-2.664 4.953-1.332.746 1.304.477 3.671-.808 7.109a.344.344 0 0 1-.153.18.343.343 0 0 1-.468-.133l-.063-.106a34.64 34.64 0 0 0-1.5-2.426 35.65 35.65 0 0 0-2.074-2.742.345.345 0 0 1 .039-.484ZM7.285 2.274c1.48 0 3.364 1.402 5.649 4.203a.349.349 0 0 1 .078.218.348.348 0 0 1-.348.344l-.117-.004a34.584 34.584 0 0 0-2.809.102 35.54 35.54 0 0 0-3.363.437.343.343 0 0 1-.394-.273l-.02-.098c-.629-3.285-.188-4.93 1.324-4.93Zm2.871 5.812h3.688a.638.638 0 0 1 .55.316l1.848 3.22a.644.644 0 0 1 0 .628l-1.847 3.223a.638.638 0 0 1-.551.316h-3.688a.627.627 0 0 1-.547-.316L7.758 12.25a.644.644 0 0 1 0-.629L9.61 8.402a.627.627 0 0 1 .546-.316Zm3.23.793a.638.638 0 0 1 .552.316l1.39 2.426a.644.644 0 0 1 0 .629l-1.39 2.43a.638.638 0 0 1-.551.316h-2.774a.627.627 0 0 1-.546-.316l-1.395-2.43a.644.644 0 0 1 0-.629l1.395-2.426a.627.627 0 0 1 .546-.316Zm-.491.867h-1.79a.624.624 0 0 0-.546.316l-.899 1.56a.644.644 0 0 0 0 .628l.899 1.563a.632.632 0 0 0 .547.316h1.789a.632.632 0 0 0 .547-.316l.898-1.563a.644.644 0 0 0 0-.629l-.898-1.558a.624.624 0 0 0-.547-.317Zm-.477.828c.227 0 .438.121.547.317l.422.73a.625.625 0 0 1 0 .629l-.422.734a.627.627 0 0 1-.547.317h-.836a.632.632 0 0 1-.547-.317l-.422-.734a.625.625 0 0 1 0-.629l.422-.73a.632.632 0 0 1 .547-.317zm-.418.817a.548.548 0 0 0-.473.273.547.547 0 0 0 0 .547.544.544 0 0 0 .473.27.544.544 0 0 0 .473-.27.547.547 0 0 0 0-.547.548.548 0 0 0-.473-.273Zm-4.422.546h.98M18.98 7.75c.391-1.895.477-3.344.223-4.398-.148-.63-.422-1.137-.84-1.508-.441-.39-1-.582-1.625-.582-1.035 0-2.12.472-3.281 1.367a14.9 14.9 0 0 0-1.473 1.316 1.206 1.206 0 0 0-.136-.144c-1.446-1.285-2.66-2.082-3.7-2.39-.617-.184-1.195-.2-1.722-.024-.559.187-1.004.574-1.317 1.117-.515.894-.652 2.074-.46 3.527.078.59.214 1.235.402 1.934a1.119 1.119 0 0 0-.215.047C3.008 8.62 1.71 9.269.926 10.015c-.465.442-.77.938-.883 1.481-.113.578 0 1.156.312 1.7.516.894 1.465 1.597 2.817 2.155.543.223 1.156.426 1.844.61a1.023 1.023 0 0 0-.07.226c-.391 1.891-.477 3.344-.223 4.395.148.629.425 1.14.84 1.508.44.39 1 .582 1.625.582 1.035 0 2.12-.473 3.28-1.364.477-.37.973-.816 1.489-1.336a1.2 1.2 0 0 0 .195.227c1.446 1.285 2.66 2.082 3.7 2.39.617.184 1.195.2 1.722.024.559-.187 1.004-.574 1.317-1.117.515-.894.652-2.074.46-3.527a14.941 14.941 0 0 0-.425-2.012 1.225 1.225 0 0 0 .238-.047c1.828-.61 3.125-1.258 3.91-2.004.465-.441.77-.937.883-1.48.113-.578 0-1.157-.313-1.7-.515-.894-1.464-1.597-2.816-2.156a14.576 14.576 0 0 0-1.906-.625.865.865 0 0 0 .059-.195z" />
|
||||
</svg>
|
||||
<p className="mt-2 font-medium">TanStack Form</p>
|
||||
</LinkedCard>
|
||||
<LinkedCard href="#" className="border border-dashed bg-transparent">
|
||||
<svg
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
className="size-10"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<title>React</title>
|
||||
<path
|
||||
d="M14.23 12.004a2.236 2.236 0 0 1-2.235 2.236 2.236 2.236 0 0 1-2.236-2.236 2.236 2.236 0 0 1 2.235-2.236 2.236 2.236 0 0 1 2.236 2.236zm2.648-10.69c-1.346 0-3.107.96-4.888 2.622-1.78-1.653-3.542-2.602-4.887-2.602-.41 0-.783.093-1.106.278-1.375.793-1.683 3.264-.973 6.365C1.98 8.917 0 10.42 0 12.004c0 1.59 1.99 3.097 5.043 4.03-.704 3.113-.39 5.588.988 6.38.32.187.69.275 1.102.275 1.345 0 3.107-.96 4.888-2.624 1.78 1.654 3.542 2.603 4.887 2.603.41 0 .783-.09 1.106-.275 1.374-.792 1.683-3.263.973-6.365C22.02 15.096 24 13.59 24 12.004c0-1.59-1.99-3.097-5.043-4.032.704-3.11.39-5.587-.988-6.38-.318-.184-.688-.277-1.092-.278zm-.005 1.09v.006c.225 0 .406.044.558.127.666.382.955 1.835.73 3.704-.054.46-.142.945-.25 1.44-.96-.236-2.006-.417-3.107-.534-.66-.905-1.345-1.727-2.035-2.447 1.592-1.48 3.087-2.292 4.105-2.295zm-9.77.02c1.012 0 2.514.808 4.11 2.28-.686.72-1.37 1.537-2.02 2.442-1.107.117-2.154.298-3.113.538-.112-.49-.195-.964-.254-1.42-.23-1.868.054-3.32.714-3.707.19-.09.4-.127.563-.132zm4.882 3.05c.455.468.91.992 1.36 1.564-.44-.02-.89-.034-1.345-.034-.46 0-.915.01-1.36.034.44-.572.895-1.096 1.345-1.565zM12 8.1c.74 0 1.477.034 2.202.093.406.582.802 1.203 1.183 1.86.372.64.71 1.29 1.018 1.946-.308.655-.646 1.31-1.013 1.95-.38.66-.773 1.288-1.18 1.87-.728.063-1.466.098-2.21.098-.74 0-1.477-.035-2.202-.093-.406-.582-.802-1.204-1.183-1.86-.372-.64-.71-1.29-1.018-1.946.303-.657.646-1.313 1.013-1.954.38-.66.773-1.286 1.18-1.868.728-.064 1.466-.098 2.21-.098zm-3.635.254c-.24.377-.48.763-.704 1.16-.225.39-.435.782-.635 1.174-.265-.656-.49-1.31-.676-1.947.64-.15 1.315-.283 2.015-.386zm7.26 0c.695.103 1.365.23 2.006.387-.18.632-.405 1.282-.66 1.933-.2-.39-.41-.783-.64-1.174-.225-.392-.465-.774-.705-1.146zm3.063.675c.484.15.944.317 1.375.498 1.732.74 2.852 1.708 2.852 2.476-.005.768-1.125 1.74-2.857 2.475-.42.18-.88.342-1.355.493-.28-.958-.646-1.956-1.1-2.98.45-1.017.81-2.01 1.085-2.964zm-13.395.004c.278.96.645 1.957 1.1 2.98-.45 1.017-.812 2.01-1.086 2.964-.484-.15-.944-.318-1.37-.5-1.732-.737-2.852-1.706-2.852-2.474 0-.768 1.12-1.742 2.852-2.476.42-.18.88-.342 1.356-.494zm11.678 4.28c.265.657.49 1.312.676 1.948-.64.157-1.316.29-2.016.39.24-.375.48-.762.705-1.158.225-.39.435-.788.636-1.18zm-9.945.02c.2.392.41.783.64 1.175.23.39.465.772.705 1.143-.695-.102-1.365-.23-2.006-.386.18-.63.406-1.282.66-1.933zM17.92 16.32c.112.493.2.968.254 1.423.23 1.868-.054 3.32-.714 3.708-.147.09-.338.128-.563.128-1.012 0-2.514-.807-4.11-2.28.686-.72 1.37-1.536 2.02-2.44 1.107-.118 2.154-.3 3.113-.54zm-11.83.01c.96.234 2.006.415 3.107.532.66.905 1.345 1.727 2.035 2.446-1.595 1.483-3.092 2.295-4.11 2.295-.22-.005-.406-.05-.553-.132-.666-.38-.955-1.834-.73-3.703.054-.46.142-.944.25-1.438zm4.56.64c.44.02.89.034 1.345.034.46 0 .915-.01 1.36-.034-.44.572-.895 1.095-1.345 1.565-.455-.47-.91-.993-1.36-1.565z"
|
||||
/>
|
||||
</svg>
|
||||
<p className="mt-2 font-medium">useActionState</p>
|
||||
<p className="text-muted-foreground mt-1 text-xs">(Coming Soon)</p>
|
||||
|
||||
</LinkedCard>
|
||||
</div>
|
||||
3
apps/v4/content/docs/forms/meta.json
Normal file
3
apps/v4/content/docs/forms/meta.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"pages": ["react-hook-form", "tanstack-form"]
|
||||
}
|
||||
397
apps/v4/content/docs/forms/next.mdx
Normal file
397
apps/v4/content/docs/forms/next.mdx
Normal file
@@ -0,0 +1,397 @@
|
||||
---
|
||||
title: Next.js
|
||||
description: Build forms in React using useActionState and Server Actions.
|
||||
---
|
||||
|
||||
import { InfoIcon } from "lucide-react"
|
||||
|
||||
In this guide, we will take a look at building forms with Next.js using `useActionState` and Server Actions. We'll cover building forms, validation, pending states, accessibility, and more.
|
||||
|
||||
## Demo
|
||||
|
||||
We are going to build the following form with a simple text input and a textarea. On submit, we'll use a server action to validate the form data and update the form state.
|
||||
|
||||
<ComponentPreview
|
||||
name="form-next-demo"
|
||||
className="[&_.preview]:h-[700px] [&_pre]:!h-[700px]"
|
||||
/>
|
||||
|
||||
<Callout icon={<InfoIcon />}>
|
||||
**Note:** The examples on this page intentionally disable browser validation
|
||||
to show how schema validation and form errors work in server actions.
|
||||
</Callout>
|
||||
|
||||
## Approach
|
||||
|
||||
This form leverages Next.js and React's built-in capabilities for form handling. We'll build our form using the `<Field />` component, which gives you **complete flexibility over the markup and styling**.
|
||||
|
||||
- Uses Next.js `<Form />` component for navigation and progressive enhancement.
|
||||
- `<Field />` components for building accessible forms.
|
||||
- `useActionState` for managing form state and errors.
|
||||
- Handles loading states with pending prop.
|
||||
- Server Actions for handling form submissions.
|
||||
- Server-side validation using Zod.
|
||||
|
||||
## Anatomy
|
||||
|
||||
Here's a basic example of a form using the `<Field />` component.
|
||||
|
||||
```tsx showLineNumbers
|
||||
<Form action={formAction}>
|
||||
<FieldGroup>
|
||||
<Field data-invalid={!!formState.errors?.title?.length}>
|
||||
<FieldLabel htmlFor="title">Bug Title</FieldLabel>
|
||||
<Input
|
||||
id="title"
|
||||
name="title"
|
||||
defaultValue={formState.values.title}
|
||||
disabled={pending}
|
||||
aria-invalid={!!formState.errors?.title?.length}
|
||||
placeholder="Login button not working on mobile"
|
||||
autoComplete="off"
|
||||
/>
|
||||
<FieldDescription>
|
||||
Provide a concise title for your bug report.
|
||||
</FieldDescription>
|
||||
{formState.errors?.title && (
|
||||
<FieldError>{formState.errors.title[0]}</FieldError>
|
||||
)}
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
<Button type="submit">Submit</Button>
|
||||
</Form>
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Create a form schema
|
||||
|
||||
We'll start by defining the shape of our form using a Zod schema in a `schema.ts` file.
|
||||
|
||||
<Callout icon={<InfoIcon />}>
|
||||
**Note:** This example uses `zod v3` for schema validation, but you can
|
||||
replace it with any other schema validation library. Make sure your schema
|
||||
library conforms to the Standard Schema specification.
|
||||
</Callout>
|
||||
|
||||
```tsx showLineNumbers title="schema.ts"
|
||||
import { z } from "zod"
|
||||
|
||||
export const formSchema = z.object({
|
||||
title: z
|
||||
.string()
|
||||
.min(5, "Bug title must be at least 5 characters.")
|
||||
.max(32, "Bug title must be at most 32 characters."),
|
||||
description: z
|
||||
.string()
|
||||
.min(20, "Description must be at least 20 characters.")
|
||||
.max(100, "Description must be at most 100 characters."),
|
||||
})
|
||||
```
|
||||
|
||||
### Define the form state type
|
||||
|
||||
Next, we'll create a type for our form state that includes values, errors, and success status. This will be used to type the form state on the client and server.
|
||||
|
||||
```tsx showLineNumbers title="schema.ts"
|
||||
import { z } from "zod"
|
||||
|
||||
export type FormState = {
|
||||
values?: z.infer<typeof formSchema>
|
||||
errors: null | Partial<Record<keyof z.infer<typeof formSchema>, string[]>>
|
||||
success: boolean
|
||||
}
|
||||
```
|
||||
|
||||
**Important:** We define the schema and the `FormState` type in a separate file so we can import them into both the client and server components.
|
||||
|
||||
### Create the Server Action
|
||||
|
||||
A server action is a function that runs on the server and can be called from the client. We'll use it to validate the form data and update the form state.
|
||||
|
||||
<ComponentSource
|
||||
src="/registry/new-york-v4/examples/form-next-demo-action.ts"
|
||||
title="actions.ts"
|
||||
/>
|
||||
|
||||
**Note:** We're returning `values` for error cases. This is because we want to keep the user submitted values in the form state. For success cases, we're returning empty values to reset the form.
|
||||
|
||||
### Build the form
|
||||
|
||||
We can now build the form using the `<Field />` component. We'll use the `useActionState` hook to manage the form state, server action, and pending state.
|
||||
|
||||
<ComponentSource
|
||||
src="/registry/new-york-v4/examples/form-next-demo.tsx"
|
||||
title="form.tsx"
|
||||
/>
|
||||
|
||||
### Done
|
||||
|
||||
That's it. You now have a fully accessible form with client and server-side validation.
|
||||
|
||||
When you submit the form, the `formAction` function will be called on the server. The server action will validate the form data and update the form state.
|
||||
|
||||
If the form data is invalid, the server action will return the errors to the client. If the form data is valid, the server action will return the success status and update the form state.
|
||||
|
||||
## Pending States
|
||||
|
||||
Use the `pending` prop from `useActionState` to show loading indicators and disable form inputs.
|
||||
|
||||
```tsx showLineNumbers {11,26-34}
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import Form from "next/form"
|
||||
|
||||
import { Spinner } from "@/components/ui/spinner"
|
||||
|
||||
import { bugReportFormAction } from "./actions"
|
||||
|
||||
export function BugReportForm() {
|
||||
const [formState, formAction, pending] = React.useActionState(
|
||||
bugReportFormAction,
|
||||
{
|
||||
errors: null,
|
||||
success: false,
|
||||
}
|
||||
)
|
||||
|
||||
return (
|
||||
<Form action={formAction}>
|
||||
<FieldGroup>
|
||||
<Field data-disabled={pending}>
|
||||
<FieldLabel htmlFor="name">Name</FieldLabel>
|
||||
<Input id="name" name="name" disabled={pending} />
|
||||
</Field>
|
||||
<Field>
|
||||
<Button type="submit" disabled={pending}>
|
||||
{pending && <Spinner />} Submit
|
||||
</Button>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Disabled States
|
||||
|
||||
### Submit Button
|
||||
|
||||
To disable the submit button, use the `pending` prop on the button's `disabled` prop.
|
||||
|
||||
```tsx showLineNumbers
|
||||
<Button type="submit" disabled={pending}>
|
||||
{pending && <Spinner />} Submit
|
||||
</Button>
|
||||
```
|
||||
|
||||
### Field
|
||||
|
||||
To apply a disabled state and styling to a `<Field />` component, use the `data-disabled` prop on the `<Field />` component.
|
||||
|
||||
```tsx showLineNumbers
|
||||
<Field data-disabled={pending}>
|
||||
<FieldLabel htmlFor="name">Name</FieldLabel>
|
||||
<Input id="name" name="name" disabled={pending} />
|
||||
</Field>
|
||||
```
|
||||
|
||||
## Validation
|
||||
|
||||
### Server-side Validation
|
||||
|
||||
Use `safeParse()` on your schema in your server action to validate the form data.
|
||||
|
||||
```tsx showLineNumbers title="actions.ts" {12-20}
|
||||
"use server"
|
||||
|
||||
export async function bugReportFormAction(
|
||||
_prevState: FormState,
|
||||
formData: FormData
|
||||
) {
|
||||
const values = {
|
||||
title: formData.get("title") as string,
|
||||
description: formData.get("description") as string,
|
||||
}
|
||||
|
||||
const result = formSchema.safeParse(values)
|
||||
|
||||
if (!result.success) {
|
||||
return {
|
||||
values,
|
||||
success: false,
|
||||
errors: result.error.flatten().fieldErrors,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
errors: null,
|
||||
success: true,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Business Logic Validation
|
||||
|
||||
You can add additional custom validation logic in your server action.
|
||||
|
||||
Make sure to return the values on validation errors. This is to ensure that the form state maintains the user's input.
|
||||
|
||||
```tsx showLineNumbers title="actions.ts" {22-35}
|
||||
"use server"
|
||||
|
||||
export async function bugReportFormAction(
|
||||
_prevState: FormState,
|
||||
formData: FormData
|
||||
) {
|
||||
const values = {
|
||||
title: formData.get("title") as string,
|
||||
description: formData.get("description") as string,
|
||||
}
|
||||
|
||||
const result = formSchema.safeParse(values)
|
||||
|
||||
if (!result.success) {
|
||||
return {
|
||||
values,
|
||||
success: false,
|
||||
errors: result.error.flatten().fieldErrors,
|
||||
}
|
||||
}
|
||||
|
||||
// Check if email already exists in database.
|
||||
const existingUser = await db.user.findUnique({
|
||||
where: { email: result.data.email },
|
||||
})
|
||||
|
||||
if (existingUser) {
|
||||
return {
|
||||
values,
|
||||
success: false,
|
||||
errors: {
|
||||
email: ["This email is already registered"],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
errors: null,
|
||||
success: true,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Displaying Errors
|
||||
|
||||
Display errors next to the field using `<FieldError />`. Make sure to add the `data-invalid` prop to the `<Field />` component and `aria-invalid` prop to the input.
|
||||
|
||||
```tsx showLineNumbers
|
||||
<Field data-invalid={!!formState.errors?.email?.length}>
|
||||
<FieldLabel htmlFor="email">Email</FieldLabel>
|
||||
<Input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
aria-invalid={!!formState.errors?.email?.length}
|
||||
/>
|
||||
{formState.errors?.email && (
|
||||
<FieldError>{formState.errors.email[0]}</FieldError>
|
||||
)}
|
||||
</Field>
|
||||
```
|
||||
|
||||
## Resetting the Form
|
||||
|
||||
When you submit a form with a server action, React will automatically reset the form state to the initial values.
|
||||
|
||||
### Reset on Success
|
||||
|
||||
To reset the form on success, you can omit the `values` from the server action and React will automatically reset the form state to the initial values. This is standard React behavior.
|
||||
|
||||
```tsx showLineNumbers title="actions.ts" {22-26}
|
||||
export async function demoFormAction(
|
||||
_prevState: FormState,
|
||||
formData: FormData
|
||||
) {
|
||||
const values = {
|
||||
title: formData.get("title") as string,
|
||||
description: formData.get("description") as string,
|
||||
}
|
||||
|
||||
// Validation.
|
||||
if (!result.success) {
|
||||
return {
|
||||
values,
|
||||
success: false,
|
||||
errors: result.error.flatten().fieldErrors,
|
||||
}
|
||||
}
|
||||
|
||||
// Business logic.
|
||||
callYourDatabaseOrAPI(values)
|
||||
|
||||
// Omit the values on success to reset the form state.
|
||||
return {
|
||||
errors: null,
|
||||
success: true,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Preserve on Validation Errors
|
||||
|
||||
To prevent the form from being reset on failure, you can return the values in the server action. This is to ensure that the form state maintains the user's input.
|
||||
|
||||
```tsx showLineNumbers title="actions.ts" {12-17}
|
||||
export async function demoFormAction(
|
||||
_prevState: FormState,
|
||||
formData: FormData
|
||||
) {
|
||||
const values = {
|
||||
title: formData.get("title") as string,
|
||||
description: formData.get("description") as string,
|
||||
}
|
||||
|
||||
// Validation.
|
||||
if (!result.success) {
|
||||
return {
|
||||
// Return the values on validation errors.
|
||||
values,
|
||||
success: false,
|
||||
errors: result.error.flatten().fieldErrors,
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Complex Forms
|
||||
|
||||
Here is an example of a more complex form with multiple fields and validation.
|
||||
|
||||
<ComponentPreview
|
||||
name="form-next-complex"
|
||||
className="[&_.preview]:h-[1100px] [&_pre]:!h-[1100px]"
|
||||
hideCode
|
||||
/>
|
||||
|
||||
### Schema
|
||||
|
||||
<ComponentSource
|
||||
src="/registry/new-york-v4/examples/form-next-complex-schema.ts"
|
||||
title="schema.ts"
|
||||
/>
|
||||
|
||||
### Form
|
||||
|
||||
<ComponentSource
|
||||
src="/registry/new-york-v4/examples/form-next-complex.tsx"
|
||||
title="form.tsx"
|
||||
/>
|
||||
|
||||
### Server Action
|
||||
|
||||
<ComponentSource
|
||||
src="/registry/new-york-v4/examples/form-next-complex-action.ts"
|
||||
title="actions.ts"
|
||||
/>
|
||||
629
apps/v4/content/docs/forms/react-hook-form.mdx
Normal file
629
apps/v4/content/docs/forms/react-hook-form.mdx
Normal file
@@ -0,0 +1,629 @@
|
||||
---
|
||||
title: React Hook Form
|
||||
description: Build forms in React using React Hook Form and Zod.
|
||||
links:
|
||||
doc: https://react-hook-form.com
|
||||
---
|
||||
|
||||
import { InfoIcon } from "lucide-react"
|
||||
|
||||
In this guide, we will take a look at building forms with React Hook Form. We'll cover building forms with the `<Field />` component, adding schema validation using Zod, error handling, accessibility, and more.
|
||||
|
||||
## Demo
|
||||
|
||||
We are going to build the following form. It has a simple text input and a textarea. On submit, we'll validate the form data and display any errors.
|
||||
|
||||
<Callout icon={<InfoIcon />}>
|
||||
**Note:** For the purpose of this demo, we have intentionally disabled browser
|
||||
validation to show how schema validation and form errors work in React Hook
|
||||
Form. It is recommended to add basic browser validation in your production
|
||||
code.
|
||||
</Callout>
|
||||
|
||||
<ComponentPreview
|
||||
name="form-rhf-demo"
|
||||
className="sm:[&_.preview]:h-[700px] sm:[&_pre]:!h-[700px]"
|
||||
chromeLessOnMobile
|
||||
/>
|
||||
|
||||
## Approach
|
||||
|
||||
This form leverages React Hook Form for performant, flexible form handling. We'll build our form using the `<Field />` component, which gives you **complete flexibility over the markup and styling**.
|
||||
|
||||
- Uses React Hook Form's `useForm` hook for form state management.
|
||||
- `<Controller />` component for controlled inputs.
|
||||
- `<Field />` components for building accessible forms.
|
||||
- Client-side validation using Zod with `zodResolver`.
|
||||
|
||||
## Anatomy
|
||||
|
||||
Here's a basic example of a form using the `<Controller />` component from React Hook Form and the `<Field />` component.
|
||||
|
||||
```tsx showLineNumbers {5-18}
|
||||
<Controller
|
||||
name="title"
|
||||
control={form.control}
|
||||
render={({ field, fieldState }) => (
|
||||
<Field data-invalid={fieldState.invalid}>
|
||||
<FieldLabel htmlFor={field.name}>Bug Title</FieldLabel>
|
||||
<Input
|
||||
{...field}
|
||||
id={field.name}
|
||||
aria-invalid={fieldState.invalid}
|
||||
placeholder="Login button not working on mobile"
|
||||
autoComplete="off"
|
||||
/>
|
||||
<FieldDescription>
|
||||
Provide a concise title for your bug report.
|
||||
</FieldDescription>
|
||||
{fieldState.invalid && <FieldError errors={[fieldState.error]} />}
|
||||
</Field>
|
||||
)}
|
||||
/>
|
||||
```
|
||||
|
||||
## Form
|
||||
|
||||
### Create a form schema
|
||||
|
||||
We'll start by defining the shape of our form using a Zod schema
|
||||
|
||||
<Callout icon={<InfoIcon />}>
|
||||
**Note:** This example uses `zod v3` for schema validation, but you can
|
||||
replace it with any other Standard Schema validation library supported by
|
||||
React Hook Form.
|
||||
</Callout>
|
||||
|
||||
```tsx showLineNumbers title="form.tsx"
|
||||
import * as z from "zod"
|
||||
|
||||
const formSchema = z.object({
|
||||
title: z
|
||||
.string()
|
||||
.min(5, "Bug title must be at least 5 characters.")
|
||||
.max(32, "Bug title must be at most 32 characters."),
|
||||
description: z
|
||||
.string()
|
||||
.min(20, "Description must be at least 20 characters.")
|
||||
.max(100, "Description must be at most 100 characters."),
|
||||
})
|
||||
```
|
||||
|
||||
### Setup the form
|
||||
|
||||
Next, we'll use the `useForm` hook from React Hook Form to create our form instance. We'll also add the Zod resolver to validate the form data.
|
||||
|
||||
```tsx showLineNumbers title="form.tsx" {17-23}
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { useForm } from "react-hook-form"
|
||||
import * as z from "zod"
|
||||
|
||||
const formSchema = z.object({
|
||||
title: z
|
||||
.string()
|
||||
.min(5, "Bug title must be at least 5 characters.")
|
||||
.max(32, "Bug title must be at most 32 characters."),
|
||||
description: z
|
||||
.string()
|
||||
.min(20, "Description must be at least 20 characters.")
|
||||
.max(100, "Description must be at most 100 characters."),
|
||||
})
|
||||
|
||||
export function BugReportForm() {
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
title: "",
|
||||
description: "",
|
||||
},
|
||||
})
|
||||
|
||||
function onSubmit(data: z.infer<typeof formSchema>) {
|
||||
// Do something with the form values.
|
||||
console.log(data)
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={form.handleSubmit(onSubmit)}>
|
||||
{/* ... */}
|
||||
{/* Build the form here */}
|
||||
{/* ... */}
|
||||
</form>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Build the form
|
||||
|
||||
We can now build the form using the `<Controller />` component from React Hook Form and the `<Field />` component.
|
||||
|
||||
<ComponentSource
|
||||
src="/registry/new-york-v4/examples/form-rhf-demo.tsx"
|
||||
title="form.tsx"
|
||||
/>
|
||||
|
||||
### Done
|
||||
|
||||
That's it. You now have a fully accessible form with client-side validation.
|
||||
|
||||
When you submit the form, the `onSubmit` function will be called with the validated form data. If the form data is invalid, React Hook Form will display the errors next to each field.
|
||||
|
||||
## Validation
|
||||
|
||||
### Client-side Validation
|
||||
|
||||
React Hook Form validates your form data using the Zod schema. Define a schema and pass it to the `resolver` option of the `useForm` hook.
|
||||
|
||||
```tsx showLineNumbers title="example-form.tsx" {5-8,12}
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { useForm } from "react-hook-form"
|
||||
import * as z from "zod"
|
||||
|
||||
const formSchema = z.object({
|
||||
title: z.string(),
|
||||
description: z.string().optional(),
|
||||
})
|
||||
|
||||
export function ExampleForm() {
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
title: "",
|
||||
description: "",
|
||||
},
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### Validation Modes
|
||||
|
||||
React Hook Form supports different validation modes.
|
||||
|
||||
```tsx showLineNumbers title="form.tsx" {3}
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
mode: "onChange",
|
||||
})
|
||||
```
|
||||
|
||||
| Mode | Description |
|
||||
| ------------- | -------------------------------------------------------- |
|
||||
| `"onChange"` | Validation triggers on every change. |
|
||||
| `"onBlur"` | Validation triggers on blur. |
|
||||
| `"onSubmit"` | Validation triggers on submit (default). |
|
||||
| `"onTouched"` | Validation triggers on first blur, then on every change. |
|
||||
| `"all"` | Validation triggers on blur and change. |
|
||||
|
||||
## Displaying Errors
|
||||
|
||||
Display errors next to the field using `<FieldError />`. For styling and accessibility:
|
||||
|
||||
- Add the `data-invalid` prop to the `<Field />` component.
|
||||
- Add the `aria-invalid` prop to the form control such as `<Input />`, `<SelectTrigger />`, `<Checkbox />`, etc.
|
||||
|
||||
```tsx showLineNumbers title="form.tsx" {5,11,13}
|
||||
<Controller
|
||||
name="email"
|
||||
control={form.control}
|
||||
render={({ field, fieldState }) => (
|
||||
<Field data-invalid={fieldState.invalid}>
|
||||
<FieldLabel htmlFor={field.name}>Email</FieldLabel>
|
||||
<Input
|
||||
{...field}
|
||||
id={field.name}
|
||||
type="email"
|
||||
aria-invalid={fieldState.invalid}
|
||||
/>
|
||||
{fieldState.invalid && <FieldError errors={[fieldState.error]} />}
|
||||
</Field>
|
||||
)}
|
||||
/>
|
||||
```
|
||||
|
||||
## Working with Different Field Types
|
||||
|
||||
### Input
|
||||
|
||||
- For input fields, spread the `field` object onto the `<Input />` component.
|
||||
- To show errors, add the `aria-invalid` prop to the `<Input />` component and the `data-invalid` prop to the `<Field />` component.
|
||||
|
||||
<ComponentPreview
|
||||
name="form-rhf-input"
|
||||
className="sm:[&_.preview]:h-[700px] sm:[&_pre]:!h-[700px]"
|
||||
chromeLessOnMobile
|
||||
/>
|
||||
|
||||
For simple text inputs, spread the `field` object onto the input.
|
||||
|
||||
```tsx showLineNumbers title="form.tsx" {5,7,8}
|
||||
<Controller
|
||||
name="name"
|
||||
control={form.control}
|
||||
render={({ field, fieldState }) => (
|
||||
<Field data-invalid={fieldState.invalid}>
|
||||
<FieldLabel htmlFor={field.name}>Name</FieldLabel>
|
||||
<Input {...field} id={field.name} aria-invalid={fieldState.invalid} />
|
||||
{fieldState.invalid && <FieldError errors={[fieldState.error]} />}
|
||||
</Field>
|
||||
)}
|
||||
/>
|
||||
```
|
||||
|
||||
### Textarea
|
||||
|
||||
- For textarea fields, spread the `field` object onto the `<Textarea />` component.
|
||||
- To show errors, add the `aria-invalid` prop to the `<Textarea />` component and the `data-invalid` prop to the `<Field />` component.
|
||||
|
||||
<ComponentPreview
|
||||
name="form-rhf-textarea"
|
||||
className="sm:[&_.preview]:h-[700px] sm:[&_pre]:!h-[700px]"
|
||||
chromeLessOnMobile
|
||||
/>
|
||||
|
||||
For textarea fields, spread the `field` object onto the textarea.
|
||||
|
||||
```tsx showLineNumbers title="form.tsx" {5,10,18}
|
||||
<Controller
|
||||
name="about"
|
||||
control={form.control}
|
||||
render={({ field, fieldState }) => (
|
||||
<Field data-invalid={fieldState.invalid}>
|
||||
<FieldLabel htmlFor="form-rhf-textarea-about">More about you</FieldLabel>
|
||||
<Textarea
|
||||
{...field}
|
||||
id="form-rhf-textarea-about"
|
||||
aria-invalid={fieldState.invalid}
|
||||
placeholder="I'm a software engineer..."
|
||||
className="min-h-[120px]"
|
||||
/>
|
||||
<FieldDescription>
|
||||
Tell us more about yourself. This will be used to help us personalize
|
||||
your experience.
|
||||
</FieldDescription>
|
||||
{fieldState.invalid && <FieldError errors={[fieldState.error]} />}
|
||||
</Field>
|
||||
)}
|
||||
/>
|
||||
```
|
||||
|
||||
### Select
|
||||
|
||||
- For select components, use `field.value` and `field.onChange` on the `<Select />` component.
|
||||
- To show errors, add the `aria-invalid` prop to the `<SelectTrigger />` component and the `data-invalid` prop to the `<Field />` component.
|
||||
|
||||
<ComponentPreview
|
||||
name="form-rhf-select"
|
||||
className="sm:[&_.preview]:h-[500px] sm:[&_pre]:!h-[500px]"
|
||||
chromeLessOnMobile
|
||||
/>
|
||||
|
||||
```tsx showLineNumbers title="form.tsx" {5,13,22}
|
||||
<Controller
|
||||
name="language"
|
||||
control={form.control}
|
||||
render={({ field, fieldState }) => (
|
||||
<Field orientation="responsive" data-invalid={fieldState.invalid}>
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor="form-rhf-select-language">
|
||||
Spoken Language
|
||||
</FieldLabel>
|
||||
<FieldDescription>
|
||||
For best results, select the language you speak.
|
||||
</FieldDescription>
|
||||
{fieldState.invalid && <FieldError errors={[fieldState.error]} />}
|
||||
</FieldContent>
|
||||
<Select
|
||||
name={field.name}
|
||||
value={field.value}
|
||||
onValueChange={field.onChange}
|
||||
>
|
||||
<SelectTrigger
|
||||
id="form-rhf-select-language"
|
||||
aria-invalid={fieldState.invalid}
|
||||
className="min-w-[120px]"
|
||||
>
|
||||
<SelectValue placeholder="Select" />
|
||||
</SelectTrigger>
|
||||
<SelectContent position="item-aligned">
|
||||
<SelectItem value="auto">Auto</SelectItem>
|
||||
<SelectItem value="en">English</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
)}
|
||||
/>
|
||||
```
|
||||
|
||||
### Checkbox
|
||||
|
||||
- For checkbox arrays, use `field.value` and `field.onChange` with array manipulation.
|
||||
- To show errors, add the `aria-invalid` prop to the `<Checkbox />` component and the `data-invalid` prop to the `<Field />` component.
|
||||
- Remember to add `data-slot="checkbox-group"` to the `<FieldGroup />` component for proper styling and spacing.
|
||||
|
||||
<ComponentPreview
|
||||
name="form-rhf-checkbox"
|
||||
className="sm:[&_.preview]:h-[700px] sm:[&_pre]:!h-[700px]"
|
||||
chromeLessOnMobile
|
||||
/>
|
||||
|
||||
```tsx showLineNumbers title="form.tsx" {10,15,20-22,38}
|
||||
<Controller
|
||||
name="tasks"
|
||||
control={form.control}
|
||||
render={({ field, fieldState }) => (
|
||||
<FieldSet>
|
||||
<FieldLegend variant="label">Tasks</FieldLegend>
|
||||
<FieldDescription>
|
||||
Get notified when tasks you've created have updates.
|
||||
</FieldDescription>
|
||||
<FieldGroup data-slot="checkbox-group">
|
||||
{tasks.map((task) => (
|
||||
<Field
|
||||
key={task.id}
|
||||
orientation="horizontal"
|
||||
data-invalid={fieldState.invalid}
|
||||
>
|
||||
<Checkbox
|
||||
id={`form-rhf-checkbox-${task.id}`}
|
||||
name={field.name}
|
||||
aria-invalid={fieldState.invalid}
|
||||
checked={field.value.includes(task.id)}
|
||||
onCheckedChange={(checked) => {
|
||||
const newValue = checked
|
||||
? [...field.value, task.id]
|
||||
: field.value.filter((value) => value !== task.id)
|
||||
field.onChange(newValue)
|
||||
}}
|
||||
/>
|
||||
<FieldLabel
|
||||
htmlFor={`form-rhf-checkbox-${task.id}`}
|
||||
className="font-normal"
|
||||
>
|
||||
{task.label}
|
||||
</FieldLabel>
|
||||
</Field>
|
||||
))}
|
||||
</FieldGroup>
|
||||
{fieldState.invalid && <FieldError errors={[fieldState.error]} />}
|
||||
</FieldSet>
|
||||
)}
|
||||
/>
|
||||
```
|
||||
|
||||
### Radio Group
|
||||
|
||||
- For radio groups, use `field.value` and `field.onChange` on the `<RadioGroup />` component.
|
||||
- To show errors, add the `aria-invalid` prop to the `<RadioGroupItem />` component and the `data-invalid` prop to the `<Field />` component.
|
||||
|
||||
<ComponentPreview
|
||||
name="form-rhf-radiogroup"
|
||||
className="sm:[&_.preview]:h-[700px] sm:[&_pre]:!h-[700px]"
|
||||
chromeLessOnMobile
|
||||
/>
|
||||
|
||||
```tsx showLineNumbers title="form.tsx" {12-13,17,25,31}
|
||||
<Controller
|
||||
name="plan"
|
||||
control={form.control}
|
||||
render={({ field, fieldState }) => (
|
||||
<FieldSet>
|
||||
<FieldLegend>Plan</FieldLegend>
|
||||
<FieldDescription>
|
||||
You can upgrade or downgrade your plan at any time.
|
||||
</FieldDescription>
|
||||
<RadioGroup
|
||||
name={field.name}
|
||||
value={field.value}
|
||||
onValueChange={field.onChange}
|
||||
>
|
||||
{plans.map((plan) => (
|
||||
<FieldLabel key={plan.id} htmlFor={`form-rhf-radiogroup-${plan.id}`}>
|
||||
<Field orientation="horizontal" data-invalid={fieldState.invalid}>
|
||||
<FieldContent>
|
||||
<FieldTitle>{plan.title}</FieldTitle>
|
||||
<FieldDescription>{plan.description}</FieldDescription>
|
||||
</FieldContent>
|
||||
<RadioGroupItem
|
||||
value={plan.id}
|
||||
id={`form-rhf-radiogroup-${plan.id}`}
|
||||
aria-invalid={fieldState.invalid}
|
||||
/>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
))}
|
||||
</RadioGroup>
|
||||
{fieldState.invalid && <FieldError errors={[fieldState.error]} />}
|
||||
</FieldSet>
|
||||
)}
|
||||
/>
|
||||
```
|
||||
|
||||
### Switch
|
||||
|
||||
- For switches, use `field.value` and `field.onChange` on the `<Switch />` component.
|
||||
- To show errors, add the `aria-invalid` prop to the `<Switch />` component and the `data-invalid` prop to the `<Field />` component.
|
||||
|
||||
<ComponentPreview
|
||||
name="form-rhf-switch"
|
||||
className="sm:[&_.preview]:h-[500px] sm:[&_pre]:!h-[500px]"
|
||||
chromeLessOnMobile
|
||||
/>
|
||||
|
||||
```tsx showLineNumbers title="form.tsx" {5,13,18-19}
|
||||
<Controller
|
||||
name="twoFactor"
|
||||
control={form.control}
|
||||
render={({ field, fieldState }) => (
|
||||
<Field orientation="horizontal" data-invalid={fieldState.invalid}>
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor="form-rhf-switch-twoFactor">
|
||||
Multi-factor authentication
|
||||
</FieldLabel>
|
||||
<FieldDescription>
|
||||
Enable multi-factor authentication to secure your account.
|
||||
</FieldDescription>
|
||||
{fieldState.invalid && <FieldError errors={[fieldState.error]} />}
|
||||
</FieldContent>
|
||||
<Switch
|
||||
id="form-rhf-switch-twoFactor"
|
||||
name={field.name}
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
aria-invalid={fieldState.invalid}
|
||||
/>
|
||||
</Field>
|
||||
)}
|
||||
/>
|
||||
```
|
||||
|
||||
### Complex Forms
|
||||
|
||||
Here is an example of a more complex form with multiple fields and validation.
|
||||
|
||||
<ComponentPreview
|
||||
name="form-rhf-complex"
|
||||
className="sm:[&_.preview]:h-[1300px] sm:[&_pre]:!h-[1300px]"
|
||||
chromeLessOnMobile
|
||||
/>
|
||||
|
||||
## Resetting the Form
|
||||
|
||||
Use `form.reset()` to reset the form to its default values.
|
||||
|
||||
```tsx showLineNumbers
|
||||
<Button type="button" variant="outline" onClick={() => form.reset()}>
|
||||
Reset
|
||||
</Button>
|
||||
```
|
||||
|
||||
## Array Fields
|
||||
|
||||
React Hook Form provides a `useFieldArray` hook for managing dynamic array fields. This is useful when you need to add or remove fields dynamically.
|
||||
|
||||
<ComponentPreview
|
||||
name="form-rhf-array"
|
||||
className="sm:[&_.preview]:h-[700px] sm:[&_pre]:!h-[700px]"
|
||||
chromeLessOnMobile
|
||||
/>
|
||||
|
||||
### Using useFieldArray
|
||||
|
||||
Use the `useFieldArray` hook to manage array fields. It provides `fields`, `append`, and `remove` methods.
|
||||
|
||||
```tsx showLineNumbers title="form.tsx" {8-11}
|
||||
import { useFieldArray, useForm } from "react-hook-form"
|
||||
|
||||
export function ExampleForm() {
|
||||
const form = useForm({
|
||||
// ... form config
|
||||
})
|
||||
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
control: form.control,
|
||||
name: "emails",
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### Array Field Structure
|
||||
|
||||
Wrap your array fields in a `<FieldSet />` with a `<FieldLegend />` and `<FieldDescription />`.
|
||||
|
||||
```tsx showLineNumbers title="form.tsx"
|
||||
<FieldSet className="gap-4">
|
||||
<FieldLegend variant="label">Email Addresses</FieldLegend>
|
||||
<FieldDescription>
|
||||
Add up to 5 email addresses where we can contact you.
|
||||
</FieldDescription>
|
||||
<FieldGroup className="gap-4">{/* Array items go here */}</FieldGroup>
|
||||
</FieldSet>
|
||||
```
|
||||
|
||||
### Controller Pattern for Array Items
|
||||
|
||||
Map over the `fields` array and use `<Controller />` for each item. **Make sure to use `field.id` as the key**.
|
||||
|
||||
```tsx showLineNumbers title="form.tsx"
|
||||
{
|
||||
fields.map((field, index) => (
|
||||
<Controller
|
||||
key={field.id}
|
||||
name={`emails.${index}.address`}
|
||||
control={form.control}
|
||||
render={({ field: controllerField, fieldState }) => (
|
||||
<Field orientation="horizontal" data-invalid={fieldState.invalid}>
|
||||
<FieldContent>
|
||||
<InputGroup>
|
||||
<InputGroupInput
|
||||
{...controllerField}
|
||||
id={`form-rhf-array-email-${index}`}
|
||||
aria-invalid={fieldState.invalid}
|
||||
placeholder="name@example.com"
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
/>
|
||||
{/* Remove button */}
|
||||
</InputGroup>
|
||||
{fieldState.invalid && <FieldError errors={[fieldState.error]} />}
|
||||
</FieldContent>
|
||||
</Field>
|
||||
)}
|
||||
/>
|
||||
))
|
||||
}
|
||||
```
|
||||
|
||||
### Adding Items
|
||||
|
||||
Use the `append` method to add new items to the array.
|
||||
|
||||
```tsx showLineNumbers title="form.tsx"
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => append({ address: "" })}
|
||||
disabled={fields.length >= 5}
|
||||
>
|
||||
Add Email Address
|
||||
</Button>
|
||||
```
|
||||
|
||||
### Removing Items
|
||||
|
||||
Use the `remove` method to remove items from the array. Add the remove button conditionally.
|
||||
|
||||
```tsx showLineNumbers title="form.tsx"
|
||||
{
|
||||
fields.length > 1 && (
|
||||
<InputGroupAddon align="inline-end">
|
||||
<InputGroupButton
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon-xs"
|
||||
onClick={() => remove(index)}
|
||||
aria-label={`Remove email ${index + 1}`}
|
||||
>
|
||||
<XIcon />
|
||||
</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Array Validation
|
||||
|
||||
Use Zod's `array` method to validate array fields.
|
||||
|
||||
```tsx showLineNumbers title="form.tsx"
|
||||
const formSchema = z.object({
|
||||
emails: z
|
||||
.array(
|
||||
z.object({
|
||||
address: z.string().email("Enter a valid email address."),
|
||||
})
|
||||
)
|
||||
.min(1, "Add at least one email address.")
|
||||
.max(5, "You can add up to 5 email addresses."),
|
||||
})
|
||||
```
|
||||
698
apps/v4/content/docs/forms/tanstack-form.mdx
Normal file
698
apps/v4/content/docs/forms/tanstack-form.mdx
Normal file
@@ -0,0 +1,698 @@
|
||||
---
|
||||
title: TanStack Form
|
||||
description: Build forms in React using TanStack Form and Zod.
|
||||
links:
|
||||
doc: https://tanstack.com/form
|
||||
---
|
||||
|
||||
import { InfoIcon } from "lucide-react"
|
||||
|
||||
This guide explores how to build forms using TanStack Form. You'll learn to create forms with the `<Field />` component, implement schema validation with Zod, handle errors, and ensure accessibility.
|
||||
|
||||
## Demo
|
||||
|
||||
We'll start by building the following form. It has a simple text input and a textarea. On submit, we'll validate the form data and display any errors.
|
||||
|
||||
<Callout icon={<InfoIcon />}>
|
||||
**Note:** For the purpose of this demo, we have intentionally disabled browser
|
||||
validation to show how schema validation and form errors work in TanStack
|
||||
Form. It is recommended to add basic browser validation in your production
|
||||
code.
|
||||
</Callout>
|
||||
|
||||
<ComponentPreview
|
||||
name="form-tanstack-demo"
|
||||
className="sm:[&_.preview]:h-[700px] sm:[&_pre]:!h-[700px]"
|
||||
chromeLessOnMobile
|
||||
/>
|
||||
|
||||
## Approach
|
||||
|
||||
This form leverages TanStack Form for powerful, headless form handling. We'll build our form using the `<Field />` component, which gives you **complete flexibility over the markup and styling**.
|
||||
|
||||
- Uses TanStack Form's `useForm` hook for form state management.
|
||||
- `form.Field` component with render prop pattern for controlled inputs.
|
||||
- `<Field />` components for building accessible forms.
|
||||
- Client-side validation using Zod.
|
||||
- Real-time validation feedback.
|
||||
|
||||
## Anatomy
|
||||
|
||||
Here's a basic example of a form using TanStack Form with the `<Field />` component.
|
||||
|
||||
```tsx showLineNumbers {15-31}
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault()
|
||||
form.handleSubmit()
|
||||
}}
|
||||
>
|
||||
<FieldGroup>
|
||||
<form.Field
|
||||
name="title"
|
||||
children={(field) => {
|
||||
const isInvalid =
|
||||
field.state.meta.isTouched && !field.state.meta.isValid
|
||||
return (
|
||||
<Field data-invalid={isInvalid}>
|
||||
<FieldLabel htmlFor={field.name}>Bug Title</FieldLabel>
|
||||
<Input
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
value={field.state.value}
|
||||
onBlur={field.handleBlur}
|
||||
onChange={(e) => field.handleChange(e.target.value)}
|
||||
aria-invalid={isInvalid}
|
||||
placeholder="Login button not working on mobile"
|
||||
autoComplete="off"
|
||||
/>
|
||||
<FieldDescription>
|
||||
Provide a concise title for your bug report.
|
||||
</FieldDescription>
|
||||
{isInvalid && <FieldError errors={field.state.meta.errors} />}
|
||||
</Field>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</FieldGroup>
|
||||
<Button type="submit">Submit</Button>
|
||||
</form>
|
||||
```
|
||||
|
||||
## Form
|
||||
|
||||
### Create a schema
|
||||
|
||||
We'll start by defining the shape of our form using a Zod schema.
|
||||
|
||||
<Callout icon={<InfoIcon />}>
|
||||
**Note:** This example uses `zod v3` for schema validation. TanStack Form
|
||||
integrates seamlessly with Zod and other Standard Schema validation libraries
|
||||
through its validators API.
|
||||
</Callout>
|
||||
|
||||
```tsx showLineNumbers title="form.tsx"
|
||||
import * as z from "zod"
|
||||
|
||||
const formSchema = z.object({
|
||||
title: z
|
||||
.string()
|
||||
.min(5, "Bug title must be at least 5 characters.")
|
||||
.max(32, "Bug title must be at most 32 characters."),
|
||||
description: z
|
||||
.string()
|
||||
.min(20, "Description must be at least 20 characters.")
|
||||
.max(100, "Description must be at most 100 characters."),
|
||||
})
|
||||
```
|
||||
|
||||
### Setup the form
|
||||
|
||||
Use the `useForm` hook from TanStack Form to create your form instance with Zod validation.
|
||||
|
||||
```tsx showLineNumbers title="form.tsx" {10-21}
|
||||
import { useForm } from "@tanstack/react-form"
|
||||
import { toast } from "sonner"
|
||||
import * as z from "zod"
|
||||
|
||||
const formSchema = z.object({
|
||||
// ...
|
||||
})
|
||||
|
||||
export function BugReportForm() {
|
||||
const form = useForm({
|
||||
defaultValues: {
|
||||
title: "",
|
||||
description: "",
|
||||
},
|
||||
validators: {
|
||||
onSubmit: formSchema,
|
||||
},
|
||||
onSubmit: async ({ value }) => {
|
||||
toast.success("Form submitted successfully")
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault()
|
||||
form.handleSubmit()
|
||||
}}
|
||||
>
|
||||
{/* ... */}
|
||||
</form>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
We are using `onSubmit` to validate the form data here. TanStack Form supports other validation modes, which you can read about in the [documentation](https://tanstack.com/form/latest/docs/framework/react/guides/dynamic-validation).
|
||||
|
||||
### Build the form
|
||||
|
||||
We can now build the form using the `form.Field` component from TanStack Form and the `<Field />` component.
|
||||
|
||||
<ComponentSource
|
||||
src="/registry/new-york-v4/examples/form-tanstack-demo.tsx"
|
||||
title="form.tsx"
|
||||
/>
|
||||
|
||||
### Done
|
||||
|
||||
That's it. You now have a fully accessible form with client-side validation.
|
||||
|
||||
When you submit the form, the `onSubmit` function will be called with the validated form data. If the form data is invalid, TanStack Form will display the errors next to each field.
|
||||
|
||||
## Validation
|
||||
|
||||
### Client-side Validation
|
||||
|
||||
TanStack Form validates your form data using the Zod schema. Validation happens in real-time as the user types.
|
||||
|
||||
```tsx showLineNumbers title="form.tsx" {13-15}
|
||||
import { useForm } from "@tanstack/react-form"
|
||||
|
||||
const formSchema = z.object({
|
||||
// ...
|
||||
})
|
||||
|
||||
export function BugReportForm() {
|
||||
const form = useForm({
|
||||
defaultValues: {
|
||||
title: "",
|
||||
description: "",
|
||||
},
|
||||
validators: {
|
||||
onSubmit: formSchema,
|
||||
},
|
||||
onSubmit: async ({ value }) => {
|
||||
console.log(value)
|
||||
},
|
||||
})
|
||||
|
||||
return <form onSubmit={/* ... */}>{/* ... */}</form>
|
||||
}
|
||||
```
|
||||
|
||||
### Validation Modes
|
||||
|
||||
TanStack Form supports different validation strategies through the `validators` option:
|
||||
|
||||
| Mode | Description |
|
||||
| ------------ | ------------------------------------ |
|
||||
| `"onChange"` | Validation triggers on every change. |
|
||||
| `"onBlur"` | Validation triggers on blur. |
|
||||
| `"onSubmit"` | Validation triggers on submit. |
|
||||
|
||||
```tsx showLineNumbers title="form.tsx" {6-9}
|
||||
const form = useForm({
|
||||
defaultValues: {
|
||||
title: "",
|
||||
description: "",
|
||||
},
|
||||
validators: {
|
||||
onSubmit: formSchema,
|
||||
onChange: formSchema,
|
||||
onBlur: formSchema,
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## Displaying Errors
|
||||
|
||||
Display errors next to the field using `<FieldError />`. For styling and accessibility:
|
||||
|
||||
- Add the `data-invalid` prop to the `<Field />` component.
|
||||
- Add the `aria-invalid` prop to the form control such as `<Input />`, `<SelectTrigger />`, `<Checkbox />`, etc.
|
||||
|
||||
```tsx showLineNumbers title="form.tsx" {4,18}
|
||||
<form.Field
|
||||
name="email"
|
||||
children={(field) => {
|
||||
const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid
|
||||
|
||||
return (
|
||||
<Field data-invalid={isInvalid}>
|
||||
<FieldLabel htmlFor={field.name}>Email</FieldLabel>
|
||||
<Input
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
value={field.state.value}
|
||||
onBlur={field.handleBlur}
|
||||
onChange={(e) => field.handleChange(e.target.value)}
|
||||
type="email"
|
||||
aria-invalid={isInvalid}
|
||||
/>
|
||||
{isInvalid && <FieldError errors={field.state.meta.errors} />}
|
||||
</Field>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
## Working with Different Field Types
|
||||
|
||||
### Input
|
||||
|
||||
- For input fields, use `field.state.value` and `field.handleChange` on the `<Input />` component.
|
||||
- To show errors, add the `aria-invalid` prop to the `<Input />` component and the `data-invalid` prop to the `<Field />` component.
|
||||
|
||||
<ComponentPreview
|
||||
name="form-tanstack-input"
|
||||
className="sm:[&_.preview]:h-[700px] sm:[&_pre]:!h-[700px]"
|
||||
chromeLessOnMobile
|
||||
/>
|
||||
|
||||
```tsx showLineNumbers title="form.tsx" {6,11-14,22}
|
||||
<form.Field
|
||||
name="username"
|
||||
children={(field) => {
|
||||
const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid
|
||||
return (
|
||||
<Field data-invalid={isInvalid}>
|
||||
<FieldLabel htmlFor="form-tanstack-input-username">Username</FieldLabel>
|
||||
<Input
|
||||
id="form-tanstack-input-username"
|
||||
name={field.name}
|
||||
value={field.state.value}
|
||||
onBlur={field.handleBlur}
|
||||
onChange={(e) => field.handleChange(e.target.value)}
|
||||
aria-invalid={isInvalid}
|
||||
placeholder="shadcn"
|
||||
autoComplete="username"
|
||||
/>
|
||||
<FieldDescription>
|
||||
This is your public display name. Must be between 3 and 10 characters.
|
||||
Must only contain letters, numbers, and underscores.
|
||||
</FieldDescription>
|
||||
{isInvalid && <FieldError errors={field.state.meta.errors} />}
|
||||
</Field>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
### Textarea
|
||||
|
||||
- For textarea fields, use `field.state.value` and `field.handleChange` on the `<Textarea />` component.
|
||||
- To show errors, add the `aria-invalid` prop to the `<Textarea />` component and the `data-invalid` prop to the `<Field />` component.
|
||||
|
||||
<ComponentPreview
|
||||
name="form-tanstack-textarea"
|
||||
className="sm:[&_.preview]:h-[700px] sm:[&_pre]:!h-[700px]"
|
||||
chromeLessOnMobile
|
||||
/>
|
||||
|
||||
```tsx showLineNumbers title="form.tsx" {6,13-16,24}
|
||||
<form.Field
|
||||
name="about"
|
||||
children={(field) => {
|
||||
const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid
|
||||
return (
|
||||
<Field data-invalid={isInvalid}>
|
||||
<FieldLabel htmlFor="form-tanstack-textarea-about">
|
||||
More about you
|
||||
</FieldLabel>
|
||||
<Textarea
|
||||
id="form-tanstack-textarea-about"
|
||||
name={field.name}
|
||||
value={field.state.value}
|
||||
onBlur={field.handleBlur}
|
||||
onChange={(e) => field.handleChange(e.target.value)}
|
||||
aria-invalid={isInvalid}
|
||||
placeholder="I'm a software engineer..."
|
||||
className="min-h-[120px]"
|
||||
/>
|
||||
<FieldDescription>
|
||||
Tell us more about yourself. This will be used to help us personalize
|
||||
your experience.
|
||||
</FieldDescription>
|
||||
{isInvalid && <FieldError errors={field.state.meta.errors} />}
|
||||
</Field>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
### Select
|
||||
|
||||
- For select components, use `field.state.value` and `field.handleChange` on the `<Select />` component.
|
||||
- To show errors, add the `aria-invalid` prop to the `<SelectTrigger />` component and the `data-invalid` prop to the `<Field />` component.
|
||||
|
||||
<ComponentPreview
|
||||
name="form-tanstack-select"
|
||||
className="sm:[&_.preview]:h-[700px] sm:[&_pre]:!h-[700px]"
|
||||
chromeLessOnMobile
|
||||
/>
|
||||
|
||||
```tsx showLineNumbers title="form.tsx" {6,18-19,23}
|
||||
<form.Field
|
||||
name="language"
|
||||
children={(field) => {
|
||||
const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid
|
||||
return (
|
||||
<Field orientation="responsive" data-invalid={isInvalid}>
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor="form-tanstack-select-language">
|
||||
Spoken Language
|
||||
</FieldLabel>
|
||||
<FieldDescription>
|
||||
For best results, select the language you speak.
|
||||
</FieldDescription>
|
||||
{isInvalid && <FieldError errors={field.state.meta.errors} />}
|
||||
</FieldContent>
|
||||
<Select
|
||||
name={field.name}
|
||||
value={field.state.value}
|
||||
onValueChange={field.handleChange}
|
||||
>
|
||||
<SelectTrigger
|
||||
id="form-tanstack-select-language"
|
||||
aria-invalid={isInvalid}
|
||||
className="min-w-[120px]"
|
||||
>
|
||||
<SelectValue placeholder="Select" />
|
||||
</SelectTrigger>
|
||||
<SelectContent position="item-aligned">
|
||||
<SelectItem value="auto">Auto</SelectItem>
|
||||
<SelectItem value="en">English</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
### Checkbox
|
||||
|
||||
- For checkbox, use `field.state.value` and `field.handleChange` on the `<Checkbox />` component.
|
||||
- To show errors, add the `aria-invalid` prop to the `<Checkbox />` component and the `data-invalid` prop to the `<Field />` component.
|
||||
- For checkbox arrays, use `mode="array"` on the `<form.Field />` component and TanStack Form's array helpers.
|
||||
- Remember to add `data-slot="checkbox-group"` to the `<FieldGroup />` component for proper styling and spacing.
|
||||
|
||||
<ComponentPreview
|
||||
name="form-tanstack-checkbox"
|
||||
className="sm:[&_.preview]:h-[700px] sm:[&_pre]:!h-[700px]"
|
||||
chromeLessOnMobile
|
||||
/>
|
||||
|
||||
```tsx showLineNumbers title="form.tsx" {12,17,22-24,44}
|
||||
<form.Field
|
||||
name="tasks"
|
||||
mode="array"
|
||||
children={(field) => {
|
||||
const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid
|
||||
return (
|
||||
<FieldSet>
|
||||
<FieldLegend variant="label">Tasks</FieldLegend>
|
||||
<FieldDescription>
|
||||
Get notified when tasks you've created have updates.
|
||||
</FieldDescription>
|
||||
<FieldGroup data-slot="checkbox-group">
|
||||
{tasks.map((task) => (
|
||||
<Field
|
||||
key={task.id}
|
||||
orientation="horizontal"
|
||||
data-invalid={isInvalid}
|
||||
>
|
||||
<Checkbox
|
||||
id={`form-tanstack-checkbox-${task.id}`}
|
||||
name={field.name}
|
||||
aria-invalid={isInvalid}
|
||||
checked={field.state.value.includes(task.id)}
|
||||
onCheckedChange={(checked) => {
|
||||
if (checked) {
|
||||
field.pushValue(task.id)
|
||||
} else {
|
||||
const index = field.state.value.indexOf(task.id)
|
||||
if (index > -1) {
|
||||
field.removeValue(index)
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<FieldLabel
|
||||
htmlFor={`form-tanstack-checkbox-${task.id}`}
|
||||
className="font-normal"
|
||||
>
|
||||
{task.label}
|
||||
</FieldLabel>
|
||||
</Field>
|
||||
))}
|
||||
</FieldGroup>
|
||||
{isInvalid && <FieldError errors={field.state.meta.errors} />}
|
||||
</FieldSet>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
### Radio Group
|
||||
|
||||
- For radio groups, use `field.state.value` and `field.handleChange` on the `<RadioGroup />` component.
|
||||
- To show errors, add the `aria-invalid` prop to the `<RadioGroupItem />` component and the `data-invalid` prop to the `<Field />` component.
|
||||
|
||||
<ComponentPreview
|
||||
name="form-tanstack-radiogroup"
|
||||
className="sm:[&_.preview]:h-[700px] sm:[&_pre]:!h-[700px]"
|
||||
chromeLessOnMobile
|
||||
/>
|
||||
|
||||
```tsx showLineNumbers title="form.tsx" {21,29,35}
|
||||
<form.Field
|
||||
name="plan"
|
||||
children={(field) => {
|
||||
const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid
|
||||
return (
|
||||
<FieldSet>
|
||||
<FieldLegend>Plan</FieldLegend>
|
||||
<FieldDescription>
|
||||
You can upgrade or downgrade your plan at any time.
|
||||
</FieldDescription>
|
||||
<RadioGroup
|
||||
name={field.name}
|
||||
value={field.state.value}
|
||||
onValueChange={field.handleChange}
|
||||
>
|
||||
{plans.map((plan) => (
|
||||
<FieldLabel
|
||||
key={plan.id}
|
||||
htmlFor={`form-tanstack-radiogroup-${plan.id}`}
|
||||
>
|
||||
<Field orientation="horizontal" data-invalid={isInvalid}>
|
||||
<FieldContent>
|
||||
<FieldTitle>{plan.title}</FieldTitle>
|
||||
<FieldDescription>{plan.description}</FieldDescription>
|
||||
</FieldContent>
|
||||
<RadioGroupItem
|
||||
value={plan.id}
|
||||
id={`form-tanstack-radiogroup-${plan.id}`}
|
||||
aria-invalid={isInvalid}
|
||||
/>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
))}
|
||||
</RadioGroup>
|
||||
{isInvalid && <FieldError errors={field.state.meta.errors} />}
|
||||
</FieldSet>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
### Switch
|
||||
|
||||
- For switches, use `field.state.value` and `field.handleChange` on the `<Switch />` component.
|
||||
- To show errors, add the `aria-invalid` prop to the `<Switch />` component and the `data-invalid` prop to the `<Field />` component.
|
||||
|
||||
<ComponentPreview
|
||||
name="form-tanstack-switch"
|
||||
className="sm:[&_.preview]:h-[500px] sm:[&_pre]:!h-[500px]"
|
||||
chromeLessOnMobile
|
||||
/>
|
||||
|
||||
```tsx showLineNumbers title="form.tsx" {6,14,19-21}
|
||||
<form.Field
|
||||
name="twoFactor"
|
||||
children={(field) => {
|
||||
const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid
|
||||
return (
|
||||
<Field orientation="horizontal" data-invalid={isInvalid}>
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor="form-tanstack-switch-twoFactor">
|
||||
Multi-factor authentication
|
||||
</FieldLabel>
|
||||
<FieldDescription>
|
||||
Enable multi-factor authentication to secure your account.
|
||||
</FieldDescription>
|
||||
{isInvalid && <FieldError errors={field.state.meta.errors} />}
|
||||
</FieldContent>
|
||||
<Switch
|
||||
id="form-tanstack-switch-twoFactor"
|
||||
name={field.name}
|
||||
checked={field.state.value}
|
||||
onCheckedChange={field.handleChange}
|
||||
aria-invalid={isInvalid}
|
||||
/>
|
||||
</Field>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
### Complex Forms
|
||||
|
||||
Here is an example of a more complex form with multiple fields and validation.
|
||||
|
||||
<ComponentPreview
|
||||
name="form-tanstack-complex"
|
||||
className="sm:[&_.preview]:h-[1100px] sm:[&_pre]:!h-[1100px]"
|
||||
chromeLessOnMobile
|
||||
/>
|
||||
|
||||
## Resetting the Form
|
||||
|
||||
Use `form.reset()` to reset the form to its default values.
|
||||
|
||||
```tsx showLineNumbers
|
||||
<Button type="button" variant="outline" onClick={() => form.reset()}>
|
||||
Reset
|
||||
</Button>
|
||||
```
|
||||
|
||||
## Array Fields
|
||||
|
||||
TanStack Form provides powerful array field management with `mode="array"`. This allows you to dynamically add, remove, and update array items with full validation support.
|
||||
|
||||
<ComponentPreview
|
||||
name="form-tanstack-array"
|
||||
className="sm:[&_.preview]:h-[700px] sm:[&_pre]:!h-[700px]"
|
||||
chromeLessOnMobile
|
||||
/>
|
||||
|
||||
This example demonstrates managing multiple email addresses with array fields. Users can add up to 5 email addresses, remove individual addresses, and each address is validated independently.
|
||||
|
||||
### Array Field Structure
|
||||
|
||||
Use `mode="array"` on the parent field to enable array field management.
|
||||
|
||||
```tsx showLineNumbers title="form.tsx" {3,12-14}
|
||||
<form.Field
|
||||
name="emails"
|
||||
mode="array"
|
||||
children={(field) => {
|
||||
return (
|
||||
<FieldSet>
|
||||
<FieldLegend variant="label">Email Addresses</FieldLegend>
|
||||
<FieldDescription>
|
||||
Add up to 5 email addresses where we can contact you.
|
||||
</FieldDescription>
|
||||
<FieldGroup>
|
||||
{field.state.value.map((_, index) => (
|
||||
// Nested field for each array item
|
||||
))}
|
||||
</FieldGroup>
|
||||
</FieldSet>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
### Nested Fields
|
||||
|
||||
Access individual array items using bracket notation: `fieldName[index].propertyName`. This example uses `InputGroup` to display the remove button inline with the input.
|
||||
|
||||
```tsx showLineNumbers title="form.tsx"
|
||||
<form.Field
|
||||
name={`emails[${index}].address`}
|
||||
children={(subField) => {
|
||||
const isSubFieldInvalid =
|
||||
subField.state.meta.isTouched && !subField.state.meta.isValid
|
||||
return (
|
||||
<Field orientation="horizontal" data-invalid={isSubFieldInvalid}>
|
||||
<FieldContent>
|
||||
<InputGroup>
|
||||
<InputGroupInput
|
||||
id={`form-tanstack-array-email-${index}`}
|
||||
name={subField.name}
|
||||
value={subField.state.value}
|
||||
onBlur={subField.handleBlur}
|
||||
onChange={(e) => subField.handleChange(e.target.value)}
|
||||
aria-invalid={isSubFieldInvalid}
|
||||
placeholder="name@example.com"
|
||||
type="email"
|
||||
/>
|
||||
{field.state.value.length > 1 && (
|
||||
<InputGroupAddon align="inline-end">
|
||||
<InputGroupButton
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon-xs"
|
||||
onClick={() => field.removeValue(index)}
|
||||
aria-label={`Remove email ${index + 1}`}
|
||||
>
|
||||
<XIcon />
|
||||
</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
)}
|
||||
</InputGroup>
|
||||
{isSubFieldInvalid && (
|
||||
<FieldError errors={subField.state.meta.errors} />
|
||||
)}
|
||||
</FieldContent>
|
||||
</Field>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
### Adding Items
|
||||
|
||||
Use `field.pushValue(item)` to add items to an array field. You can disable the button when the array reaches its maximum length.
|
||||
|
||||
```tsx showLineNumbers title="form.tsx"
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => field.pushValue({ address: "" })}
|
||||
disabled={field.state.value.length >= 5}
|
||||
>
|
||||
Add Email Address
|
||||
</Button>
|
||||
```
|
||||
|
||||
### Removing Items
|
||||
|
||||
Use `field.removeValue(index)` to remove items from an array field. You can conditionally show the remove button only when there's more than one item.
|
||||
|
||||
```tsx showLineNumbers title="form.tsx"
|
||||
{
|
||||
field.state.value.length > 1 && (
|
||||
<InputGroupButton
|
||||
onClick={() => field.removeValue(index)}
|
||||
aria-label={`Remove email ${index + 1}`}
|
||||
>
|
||||
<XIcon />
|
||||
</InputGroupButton>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Array Validation
|
||||
|
||||
Validate array fields using Zod's array methods.
|
||||
|
||||
```tsx showLineNumbers title="form.tsx"
|
||||
const formSchema = z.object({
|
||||
emails: z
|
||||
.array(
|
||||
z.object({
|
||||
address: z.string().email("Enter a valid email address."),
|
||||
})
|
||||
)
|
||||
.min(1, "Add at least one email address.")
|
||||
.max(5, "You can add up to 5 email addresses."),
|
||||
})
|
||||
```
|
||||
@@ -4,6 +4,7 @@
|
||||
"(root)",
|
||||
"changelog",
|
||||
"components",
|
||||
"forms",
|
||||
"installation",
|
||||
"dark-mode",
|
||||
"registry"
|
||||
|
||||
@@ -42,16 +42,6 @@ const nextConfig = {
|
||||
destination: "/docs/figma",
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: "/docs/forms",
|
||||
destination: "/docs/components/form",
|
||||
permanent: false,
|
||||
},
|
||||
{
|
||||
source: "/docs/forms/react-hook-form",
|
||||
destination: "/docs/components/form",
|
||||
permanent: false,
|
||||
},
|
||||
{
|
||||
source: "/sidebar",
|
||||
destination: "/docs/components/sidebar",
|
||||
|
||||
@@ -89,7 +89,7 @@
|
||||
"recharts": "2.15.1",
|
||||
"rehype-pretty-code": "^0.14.1",
|
||||
"rimraf": "^6.0.1",
|
||||
"shadcn": "3.4.0",
|
||||
"shadcn": "3.4.1",
|
||||
"shiki": "^1.10.1",
|
||||
"sonner": "^2.0.0",
|
||||
"tailwind-merge": "^3.0.1",
|
||||
|
||||
145
apps/v4/public/llms.txt
Normal file
145
apps/v4/public/llms.txt
Normal file
@@ -0,0 +1,145 @@
|
||||
# shadcn/ui
|
||||
|
||||
> shadcn/ui is a collection of beautifully-designed, accessible components and a code distribution platform. It is built with TypeScript, Tailwind CSS, and Radix UI primitives. It supports multiple frameworks including Next.js, Vite, Remix, Astro, and more. Open Source. Open Code. AI-Ready. It also comes with a command-line tool to install and manage components and a registry system to publish and distribute code.
|
||||
|
||||
## Overview
|
||||
|
||||
- [Introduction](https://ui.shadcn.com/docs): Core principles—Open Code, Composition, Distribution, Beautiful Defaults, and AI-Ready design.
|
||||
- [CLI](https://ui.shadcn.com/docs/cli): Command-line tool for installing and managing components.
|
||||
- [components.json](https://ui.shadcn.com/docs/components-json): Configuration file for customizing the CLI and component installation.
|
||||
- [Theming](https://ui.shadcn.com/docs/theming): Guide to customizing colors, typography, and design tokens.
|
||||
- [Changelog](https://ui.shadcn.com/docs/changelog): Release notes and version history.
|
||||
- [About](https://ui.shadcn.com/docs/about): Credits and project information.
|
||||
|
||||
## Installation
|
||||
|
||||
- [Next.js](https://ui.shadcn.com/docs/installation/next): Install shadcn/ui in a Next.js project.
|
||||
- [Vite](https://ui.shadcn.com/docs/installation/vite): Install shadcn/ui in a Vite project.
|
||||
- [Remix](https://ui.shadcn.com/docs/installation/remix): Install shadcn/ui in a Remix project.
|
||||
- [Astro](https://ui.shadcn.com/docs/installation/astro): Install shadcn/ui in an Astro project.
|
||||
- [Laravel](https://ui.shadcn.com/docs/installation/laravel): Install shadcn/ui in a Laravel project.
|
||||
- [Gatsby](https://ui.shadcn.com/docs/installation/gatsby): Install shadcn/ui in a Gatsby project.
|
||||
- [React Router](https://ui.shadcn.com/docs/installation/react-router): Install shadcn/ui in a React Router project.
|
||||
- [TanStack Router](https://ui.shadcn.com/docs/installation/tanstack-router): Install shadcn/ui in a TanStack Router project.
|
||||
- [TanStack Start](https://ui.shadcn.com/docs/installation/tanstack): Install shadcn/ui in a TanStack Start project.
|
||||
- [Manual Installation](https://ui.shadcn.com/docs/installation/manual): Manually install shadcn/ui without the CLI.
|
||||
|
||||
## Components
|
||||
|
||||
### Form & Input
|
||||
|
||||
- [Form](https://ui.shadcn.com/docs/components/form): Building forms with React Hook Form and Zod validation.
|
||||
- [Field](https://ui.shadcn.com/docs/components/field): Field component for form inputs with labels and error messages.
|
||||
- [Button](https://ui.shadcn.com/docs/components/button): Button component with multiple variants.
|
||||
- [Button Group](https://ui.shadcn.com/docs/components/button-group): Group multiple buttons together.
|
||||
- [Input](https://ui.shadcn.com/docs/components/input): Text input component.
|
||||
- [Input Group](https://ui.shadcn.com/docs/components/input-group): Input component with prefix and suffix addons.
|
||||
- [Input OTP](https://ui.shadcn.com/docs/components/input-otp): One-time password input component.
|
||||
- [Textarea](https://ui.shadcn.com/docs/components/textarea): Multi-line text input component.
|
||||
- [Checkbox](https://ui.shadcn.com/docs/components/checkbox): Checkbox input component.
|
||||
- [Radio Group](https://ui.shadcn.com/docs/components/radio-group): Radio button group component.
|
||||
- [Select](https://ui.shadcn.com/docs/components/select): Select dropdown component.
|
||||
- [Switch](https://ui.shadcn.com/docs/components/switch): Toggle switch component.
|
||||
- [Slider](https://ui.shadcn.com/docs/components/slider): Slider input component.
|
||||
- [Calendar](https://ui.shadcn.com/docs/components/calendar): Calendar component for date selection.
|
||||
- [Date Picker](https://ui.shadcn.com/docs/components/date-picker): Date picker component combining input and calendar.
|
||||
- [Combobox](https://ui.shadcn.com/docs/components/combobox): Searchable select component with autocomplete.
|
||||
- [Label](https://ui.shadcn.com/docs/components/label): Form label component.
|
||||
|
||||
### Layout & Navigation
|
||||
|
||||
- [Accordion](https://ui.shadcn.com/docs/components/accordion): Collapsible accordion component.
|
||||
- [Breadcrumb](https://ui.shadcn.com/docs/components/breadcrumb): Breadcrumb navigation component.
|
||||
- [Navigation Menu](https://ui.shadcn.com/docs/components/navigation-menu): Accessible navigation menu with dropdowns.
|
||||
- [Sidebar](https://ui.shadcn.com/docs/components/sidebar): Collapsible sidebar component for app layouts.
|
||||
- [Tabs](https://ui.shadcn.com/docs/components/tabs): Tabbed interface component.
|
||||
- [Separator](https://ui.shadcn.com/docs/components/separator): Visual divider between content sections.
|
||||
- [Scroll Area](https://ui.shadcn.com/docs/components/scroll-area): Custom scrollable area with styled scrollbars.
|
||||
- [Resizable](https://ui.shadcn.com/docs/components/resizable): Resizable panel layout component.
|
||||
|
||||
### Overlays & Dialogs
|
||||
|
||||
- [Dialog](https://ui.shadcn.com/docs/components/dialog): Modal dialog component.
|
||||
- [Alert Dialog](https://ui.shadcn.com/docs/components/alert-dialog): Alert dialog for confirmation prompts.
|
||||
- [Sheet](https://ui.shadcn.com/docs/components/sheet): Slide-out panel component (drawer).
|
||||
- [Drawer](https://ui.shadcn.com/docs/components/drawer): Mobile-friendly drawer component using Vaul.
|
||||
- [Popover](https://ui.shadcn.com/docs/components/popover): Floating popover component.
|
||||
- [Tooltip](https://ui.shadcn.com/docs/components/tooltip): Tooltip component for additional context.
|
||||
- [Hover Card](https://ui.shadcn.com/docs/components/hover-card): Card that appears on hover.
|
||||
- [Context Menu](https://ui.shadcn.com/docs/components/context-menu): Right-click context menu.
|
||||
- [Dropdown Menu](https://ui.shadcn.com/docs/components/dropdown-menu): Dropdown menu component.
|
||||
- [Menubar](https://ui.shadcn.com/docs/components/menubar): Horizontal menubar component.
|
||||
- [Command](https://ui.shadcn.com/docs/components/command): Command palette component (cmdk).
|
||||
|
||||
### Feedback & Status
|
||||
|
||||
- [Alert](https://ui.shadcn.com/docs/components/alert): Alert component for messages and notifications.
|
||||
- [Toast](https://ui.shadcn.com/docs/components/toast): Toast notification component using Sonner.
|
||||
- [Progress](https://ui.shadcn.com/docs/components/progress): Progress bar component.
|
||||
- [Spinner](https://ui.shadcn.com/docs/components/spinner): Loading spinner component.
|
||||
- [Skeleton](https://ui.shadcn.com/docs/components/skeleton): Skeleton loading placeholder.
|
||||
- [Badge](https://ui.shadcn.com/docs/components/badge): Badge component for labels and status indicators.
|
||||
- [Empty](https://ui.shadcn.com/docs/components/empty): Empty state component for no data scenarios.
|
||||
|
||||
### Display & Media
|
||||
|
||||
- [Avatar](https://ui.shadcn.com/docs/components/avatar): Avatar component for user profiles.
|
||||
- [Card](https://ui.shadcn.com/docs/components/card): Card container component.
|
||||
- [Table](https://ui.shadcn.com/docs/components/table): Table component for displaying data.
|
||||
- [Data Table](https://ui.shadcn.com/docs/components/data-table): Advanced data table with sorting, filtering, and pagination.
|
||||
- [Chart](https://ui.shadcn.com/docs/components/chart): Chart components using Recharts.
|
||||
- [Carousel](https://ui.shadcn.com/docs/components/carousel): Carousel component using Embla Carousel.
|
||||
- [Aspect Ratio](https://ui.shadcn.com/docs/components/aspect-ratio): Container that maintains aspect ratio.
|
||||
- [Typography](https://ui.shadcn.com/docs/components/typography): Typography styles and components.
|
||||
- [Item](https://ui.shadcn.com/docs/components/item): Generic item component for lists and menus.
|
||||
- [Kbd](https://ui.shadcn.com/docs/components/kbd): Keyboard shortcut display component.
|
||||
|
||||
### Misc
|
||||
|
||||
- [Collapsible](https://ui.shadcn.com/docs/components/collapsible): Collapsible container component.
|
||||
- [Toggle](https://ui.shadcn.com/docs/components/toggle): Toggle button component.
|
||||
- [Toggle Group](https://ui.shadcn.com/docs/components/toggle-group): Group of toggle buttons.
|
||||
- [Pagination](https://ui.shadcn.com/docs/components/pagination): Pagination component for lists and tables.
|
||||
|
||||
## Dark Mode
|
||||
|
||||
- [Dark Mode](https://ui.shadcn.com/docs/dark-mode): Overview of dark mode implementation.
|
||||
- [Dark Mode - Next.js](https://ui.shadcn.com/docs/dark-mode/next): Dark mode setup for Next.js.
|
||||
- [Dark Mode - Vite](https://ui.shadcn.com/docs/dark-mode/vite): Dark mode setup for Vite.
|
||||
- [Dark Mode - Astro](https://ui.shadcn.com/docs/dark-mode/astro): Dark mode setup for Astro.
|
||||
- [Dark Mode - Remix](https://ui.shadcn.com/docs/dark-mode/remix): Dark mode setup for Remix.
|
||||
|
||||
## Forms
|
||||
|
||||
- [Forms Overview](https://ui.shadcn.com/docs/forms): Guide to building forms with shadcn/ui.
|
||||
- [React Hook Form](https://ui.shadcn.com/docs/forms/react-hook-form): Using shadcn/ui with React Hook Form.
|
||||
- [TanStack Form](https://ui.shadcn.com/docs/forms/tanstack-form): Using shadcn/ui with TanStack Form.
|
||||
- [Forms - Next.js](https://ui.shadcn.com/docs/forms/next): Building forms in Next.js with Server Actions.
|
||||
|
||||
## Advanced
|
||||
|
||||
- [Monorepo](https://ui.shadcn.com/docs/monorepo): Using shadcn/ui in a monorepo setup.
|
||||
- [React 19](https://ui.shadcn.com/docs/react-19): React 19 support and migration guide.
|
||||
- [Tailwind CSS v4](https://ui.shadcn.com/docs/tailwind-v4): Tailwind CSS v4 support and setup.
|
||||
- [JavaScript](https://ui.shadcn.com/docs/javascript): Using shadcn/ui with JavaScript (no TypeScript).
|
||||
- [Figma](https://ui.shadcn.com/docs/figma): Figma design resources.
|
||||
- [v0](https://ui.shadcn.com/docs/v0): Generating UI with v0 by Vercel.
|
||||
|
||||
## MCP Server
|
||||
|
||||
- [MCP Server](https://ui.shadcn.com/docs/mcp): Model Context Protocol server for AI integrations. Allows AI assistants to browse, search, and install components from registries using natural language. Works with Claude Code, Cursor, VS Code (GitHub Copilot), Codex and more.
|
||||
|
||||
## Registry
|
||||
|
||||
- [Registry Overview](https://ui.shadcn.com/docs/registry): Creating and publishing your own component registry.
|
||||
- [Getting Started](https://ui.shadcn.com/docs/registry/getting-started): Set up your own registry.
|
||||
- [Examples](https://ui.shadcn.com/docs/registry/examples): Example registries.
|
||||
- [FAQ](https://ui.shadcn.com/docs/registry/faq): Common questions about registries.
|
||||
- [Authentication](https://ui.shadcn.com/docs/registry/authentication): Adding authentication to your registry.
|
||||
- [Registry MCP](https://ui.shadcn.com/docs/registry/mcp): MCP integration for registries.
|
||||
|
||||
### Registry Schemas
|
||||
|
||||
- [Registry Schema](https://ui.shadcn.com/schema/registry.json): JSON Schema for registry index files. Defines the structure for a collection of components, hooks, pages, etc. Requires name, homepage, and items array.
|
||||
- [Registry Item Schema](https://ui.shadcn.com/schema/registry-item.json): JSON Schema for individual registry items. Defines components, hooks, themes, and other distributable code with properties for dependencies, files, Tailwind config, CSS variables, and more.
|
||||
|
||||
@@ -1,43 +1,52 @@
|
||||
{
|
||||
"@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",
|
||||
"@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",
|
||||
"@cult-ui": "https://cult-ui.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",
|
||||
"@intentui": "https://intentui.com/r/{name}",
|
||||
"@kibo-ui": "https://www.kibo-ui.com/r/{name}.json",
|
||||
"@kokonutui": "https://kokonutui.com/r/{name}.json",
|
||||
"@limeplay": "https://limeplay.winoffrg.dev/r/{name}.json",
|
||||
"@magicui": "https://magicui.design/r/{name}.json",
|
||||
"@motion-primitives": "https://motion-primitives.com/c/{name}.json",
|
||||
"@originui": "https://originui.com/r/{name}.json",
|
||||
"@prompt-kit": "https://prompt-kit.com/c/{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",
|
||||
"@ncdai": "https://chanhdai.com/r/{name}.json",
|
||||
"@paceui-ui": "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",
|
||||
"@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",
|
||||
"@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",
|
||||
"@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",
|
||||
"@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",
|
||||
"@tailark": "https://tailark.com/r/{name}.json",
|
||||
"@tweakcn": "https://tweakcn.com/r/themes/{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"
|
||||
"@zippystarter": "https://zippystarter.com/r/{name}.json"
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"files": [
|
||||
{
|
||||
"path": "ui/checkbox.tsx",
|
||||
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as CheckboxPrimitive from \"@radix-ui/react-checkbox\"\nimport { Check } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst Checkbox = React.forwardRef<\n React.ElementRef<typeof CheckboxPrimitive.Root>,\n React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>\n>(({ className, ...props }, ref) => (\n <CheckboxPrimitive.Root\n ref={ref}\n className={cn(\n \"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground\",\n className\n )}\n {...props}\n >\n <CheckboxPrimitive.Indicator\n className={cn(\"flex items-center justify-center text-current\")}\n >\n <Check className=\"h-4 w-4\" />\n </CheckboxPrimitive.Indicator>\n </CheckboxPrimitive.Root>\n))\nCheckbox.displayName = CheckboxPrimitive.Root.displayName\n\nexport { Checkbox }\n",
|
||||
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as CheckboxPrimitive from \"@radix-ui/react-checkbox\"\nimport { Check } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst Checkbox = React.forwardRef<\n React.ElementRef<typeof CheckboxPrimitive.Root>,\n React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>\n>(({ className, ...props }, ref) => (\n <CheckboxPrimitive.Root\n ref={ref}\n className={cn(\n \"grid place-content-center peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground\",\n className\n )}\n {...props}\n >\n <CheckboxPrimitive.Indicator\n className={cn(\"grid place-content-center text-current\")}\n >\n <Check className=\"h-4 w-4\" />\n </CheckboxPrimitive.Indicator>\n </CheckboxPrimitive.Root>\n))\nCheckbox.displayName = CheckboxPrimitive.Root.displayName\n\nexport { Checkbox }\n",
|
||||
"type": "registry:ui",
|
||||
"target": ""
|
||||
}
|
||||
|
||||
@@ -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
@@ -10,7 +10,7 @@
|
||||
"files": [
|
||||
{
|
||||
"path": "ui/sonner.tsx",
|
||||
"content": "\"use client\"\n\nimport { useTheme } from \"next-themes\"\nimport { Toaster as Sonner } from \"sonner\"\n\ntype ToasterProps = React.ComponentProps<typeof Sonner>\n\nconst Toaster = ({ ...props }: ToasterProps) => {\n const { theme = \"system\" } = useTheme()\n\n return (\n <Sonner\n theme={theme as ToasterProps[\"theme\"]}\n className=\"toaster group\"\n toastOptions={{\n classNames: {\n toast:\n \"group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg\",\n description: \"group-[.toast]:text-muted-foreground\",\n actionButton:\n \"group-[.toast]:bg-primary group-[.toast]:text-primary-foreground\",\n cancelButton:\n \"group-[.toast]:bg-muted group-[.toast]:text-muted-foreground\",\n },\n }}\n {...props}\n />\n )\n}\n\nexport { Toaster }\n",
|
||||
"content": "\"use client\"\n\nimport {\n CircleCheck,\n Info,\n LoaderCircle,\n OctagonX,\n TriangleAlert,\n} from \"lucide-react\"\nimport { useTheme } from \"next-themes\"\nimport { Toaster as Sonner } from \"sonner\"\n\ntype ToasterProps = React.ComponentProps<typeof Sonner>\n\nconst Toaster = ({ ...props }: ToasterProps) => {\n const { theme = \"system\" } = useTheme()\n\n return (\n <Sonner\n theme={theme as ToasterProps[\"theme\"]}\n className=\"toaster group\"\n icons={{\n success: <CircleCheck className=\"h-4 w-4\" />,\n info: <Info className=\"h-4 w-4\" />,\n warning: <TriangleAlert className=\"h-4 w-4\" />,\n error: <OctagonX className=\"h-4 w-4\" />,\n loading: <LoaderCircle className=\"h-4 w-4 animate-spin\" />,\n }}\n toastOptions={{\n classNames: {\n toast:\n \"group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg\",\n description: \"group-[.toast]:text-muted-foreground\",\n actionButton:\n \"group-[.toast]:bg-primary group-[.toast]:text-primary-foreground\",\n cancelButton:\n \"group-[.toast]:bg-muted group-[.toast]:text-muted-foreground\",\n },\n }}\n {...props}\n />\n )\n}\n\nexport { Toaster }\n",
|
||||
"type": "registry:ui",
|
||||
"target": ""
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/new-york-v4/ui/checkbox.tsx",
|
||||
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as CheckboxPrimitive from \"@radix-ui/react-checkbox\"\nimport { CheckIcon } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\nfunction Checkbox({\n className,\n ...props\n}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {\n return (\n <CheckboxPrimitive.Root\n data-slot=\"checkbox\"\n className={cn(\n \"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50\",\n className\n )}\n {...props}\n >\n <CheckboxPrimitive.Indicator\n data-slot=\"checkbox-indicator\"\n className=\"flex items-center justify-center text-current transition-none\"\n >\n <CheckIcon className=\"size-3.5\" />\n </CheckboxPrimitive.Indicator>\n </CheckboxPrimitive.Root>\n )\n}\n\nexport { Checkbox }\n",
|
||||
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as CheckboxPrimitive from \"@radix-ui/react-checkbox\"\nimport { CheckIcon } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\nfunction Checkbox({\n className,\n ...props\n}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {\n return (\n <CheckboxPrimitive.Root\n data-slot=\"checkbox\"\n className={cn(\n \"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50\",\n className\n )}\n {...props}\n >\n <CheckboxPrimitive.Indicator\n data-slot=\"checkbox-indicator\"\n className=\"grid place-content-center text-current transition-none\"\n >\n <CheckIcon className=\"size-3.5\" />\n </CheckboxPrimitive.Indicator>\n </CheckboxPrimitive.Root>\n )\n}\n\nexport { Checkbox }\n",
|
||||
"type": "registry:ui"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
||||
"name": "dropdown-menu-dialog",
|
||||
"type": "registry:example",
|
||||
"registryDependencies": [
|
||||
"dropdown-menu",
|
||||
"dialog",
|
||||
"button",
|
||||
"input",
|
||||
"label"
|
||||
],
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/new-york-v4/examples/dropdown-menu-dialog.tsx",
|
||||
"content": "\"use client\"\n\nimport { useState } from \"react\"\nimport { MoreHorizontalIcon } from \"lucide-react\"\n\nimport { Button } from \"@/registry/new-york-v4/ui/button\"\nimport {\n Dialog,\n DialogClose,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n} from \"@/registry/new-york-v4/ui/dialog\"\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuGroup,\n DropdownMenuItem,\n DropdownMenuLabel,\n DropdownMenuTrigger,\n} from \"@/registry/new-york-v4/ui/dropdown-menu\"\nimport { Field, FieldGroup, FieldLabel } from \"@/registry/new-york-v4/ui/field\"\nimport { Input } from \"@/registry/new-york-v4/ui/input\"\nimport { Label } from \"@/registry/new-york-v4/ui/label\"\nimport { Textarea } from \"@/registry/new-york-v4/ui/textarea\"\n\nexport default function DropdownMenuDialog() {\n const [showNewDialog, setShowNewDialog] = useState(false)\n const [showShareDialog, setShowShareDialog] = useState(false)\n\n return (\n <>\n <DropdownMenu modal={false}>\n <DropdownMenuTrigger asChild>\n <Button variant=\"outline\" aria-label=\"Open menu\" size=\"icon-sm\">\n <MoreHorizontalIcon />\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent className=\"w-40\" align=\"end\">\n <DropdownMenuLabel>File Actions</DropdownMenuLabel>\n <DropdownMenuGroup>\n <DropdownMenuItem onSelect={() => setShowNewDialog(true)}>\n New File...\n </DropdownMenuItem>\n <DropdownMenuItem onSelect={() => setShowShareDialog(true)}>\n Share...\n </DropdownMenuItem>\n <DropdownMenuItem disabled>Download</DropdownMenuItem>\n </DropdownMenuGroup>\n </DropdownMenuContent>\n </DropdownMenu>\n <Dialog open={showNewDialog} onOpenChange={setShowNewDialog}>\n <DialogContent className=\"sm:max-w-[425px]\">\n <DialogHeader>\n <DialogTitle>Create New File</DialogTitle>\n <DialogDescription>\n Provide a name for your new file. Click create when you're\n done.\n </DialogDescription>\n </DialogHeader>\n <FieldGroup className=\"pb-3\">\n <Field>\n <FieldLabel htmlFor=\"filename\">File Name</FieldLabel>\n <Input id=\"filename\" name=\"filename\" placeholder=\"document.txt\" />\n </Field>\n </FieldGroup>\n <DialogFooter>\n <DialogClose asChild>\n <Button variant=\"outline\">Cancel</Button>\n </DialogClose>\n <Button type=\"submit\">Create</Button>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n <Dialog open={showShareDialog} onOpenChange={setShowShareDialog}>\n <DialogContent className=\"sm:max-w-[425px]\">\n <DialogHeader>\n <DialogTitle>Share File</DialogTitle>\n <DialogDescription>\n Anyone with the link will be able to view this file.\n </DialogDescription>\n </DialogHeader>\n <FieldGroup className=\"py-3\">\n <Field>\n <Label htmlFor=\"email\">Email Address</Label>\n <Input\n id=\"email\"\n name=\"email\"\n type=\"email\"\n placeholder=\"shadcn@vercel.com\"\n autoComplete=\"off\"\n />\n </Field>\n <Field>\n <FieldLabel htmlFor=\"message\">Message (Optional)</FieldLabel>\n <Textarea\n id=\"message\"\n name=\"message\"\n placeholder=\"Check out this file\"\n />\n </Field>\n </FieldGroup>\n <DialogFooter>\n <DialogClose asChild>\n <Button variant=\"outline\">Cancel</Button>\n </DialogClose>\n <Button type=\"submit\">Send Invite</Button>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n </>\n )\n}\n",
|
||||
"type": "registry:example"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -9,7 +9,7 @@
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/new-york-v4/examples/empty-demo.tsx",
|
||||
"content": "import { IconFolderCode } from \"@tabler/icons-react\"\nimport { ArrowUpRightIcon } from \"lucide-react\"\n\nimport { Button } from \"@/registry/new-york-v4/ui/button\"\nimport {\n Empty,\n EmptyContent,\n EmptyDescription,\n EmptyHeader,\n EmptyMedia,\n EmptyTitle,\n} from \"@/registry/new-york-v4/ui/empty\"\n\nexport default function EmptyDemo() {\n return (\n <Empty>\n <EmptyHeader>\n <EmptyMedia variant=\"icon\">\n <IconFolderCode />\n </EmptyMedia>\n <EmptyTitle>No Projects Yet</EmptyTitle>\n <EmptyDescription>\n You haven't created any projects yet. Get started by creating\n your first project.\n </EmptyDescription>\n </EmptyHeader>\n <EmptyContent>\n <div className=\"flex gap-2\">\n <Button size=\"sm\">Create Project</Button>\n <Button variant=\"outline\">Import Project</Button>\n </div>\n </EmptyContent>\n <Button\n variant=\"link\"\n asChild\n className=\"text-muted-foreground\"\n size=\"sm\"\n >\n <a href=\"#\">\n Learn More <ArrowUpRightIcon />\n </a>\n </Button>\n </Empty>\n )\n}\n",
|
||||
"content": "import { IconFolderCode } from \"@tabler/icons-react\"\nimport { ArrowUpRightIcon } from \"lucide-react\"\n\nimport { Button } from \"@/registry/new-york-v4/ui/button\"\nimport {\n Empty,\n EmptyContent,\n EmptyDescription,\n EmptyHeader,\n EmptyMedia,\n EmptyTitle,\n} from \"@/registry/new-york-v4/ui/empty\"\n\nexport default function EmptyDemo() {\n return (\n <Empty>\n <EmptyHeader>\n <EmptyMedia variant=\"icon\">\n <IconFolderCode />\n </EmptyMedia>\n <EmptyTitle>No Projects Yet</EmptyTitle>\n <EmptyDescription>\n You haven't created any projects yet. Get started by creating\n your first project.\n </EmptyDescription>\n </EmptyHeader>\n <EmptyContent>\n <div className=\"flex gap-2\">\n <Button>Create Project</Button>\n <Button variant=\"outline\">Import Project</Button>\n </div>\n </EmptyContent>\n <Button\n variant=\"link\"\n asChild\n className=\"text-muted-foreground\"\n size=\"sm\"\n >\n <a href=\"#\">\n Learn More <ArrowUpRightIcon />\n </a>\n </Button>\n </Empty>\n )\n}\n",
|
||||
"type": "registry:example"
|
||||
}
|
||||
]
|
||||
|
||||
File diff suppressed because one or more lines are too long
35
apps/v4/public/r/styles/new-york-v4/form-next-complex.json
Normal file
35
apps/v4/public/r/styles/new-york-v4/form-next-complex.json
Normal file
File diff suppressed because one or more lines are too long
20
apps/v4/public/r/styles/new-york-v4/form-next-demo.json
Normal file
20
apps/v4/public/r/styles/new-york-v4/form-next-demo.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
||||
"name": "form-next-demo",
|
||||
"type": "registry:example",
|
||||
"registryDependencies": [
|
||||
"field",
|
||||
"input",
|
||||
"textarea",
|
||||
"button",
|
||||
"card",
|
||||
"spinner"
|
||||
],
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/new-york-v4/examples/form-next-demo.tsx",
|
||||
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport Form from \"next/form\"\nimport { toast } from \"sonner\"\n\nimport { Button } from \"@/registry/new-york-v4/ui/button\"\nimport {\n Card,\n CardContent,\n CardDescription,\n CardFooter,\n CardHeader,\n CardTitle,\n} from \"@/registry/new-york-v4/ui/card\"\nimport {\n Field,\n FieldDescription,\n FieldError,\n FieldGroup,\n FieldLabel,\n} from \"@/registry/new-york-v4/ui/field\"\nimport { Input } from \"@/registry/new-york-v4/ui/input\"\nimport {\n InputGroup,\n InputGroupAddon,\n InputGroupText,\n InputGroupTextarea,\n} from \"@/registry/new-york-v4/ui/input-group\"\nimport { Spinner } from \"@/registry/new-york-v4/ui/spinner\"\n\nimport { demoFormAction } from \"./form-next-demo-action\"\nimport { type FormState } from \"./form-next-demo-schema\"\n\nexport default function FormNextDemo() {\n const [formState, formAction, pending] = React.useActionState<\n FormState,\n FormData\n >(demoFormAction, {\n values: {\n title: \"\",\n description: \"\",\n },\n errors: null,\n success: false,\n })\n const [descriptionLength, setDescriptionLength] = React.useState(0)\n\n React.useEffect(() => {\n if (formState.success) {\n toast(\"Thank you for your feedback\", {\n description: \"We'll review your report and get back to you soon.\",\n })\n }\n }, [formState.success])\n\n React.useEffect(() => {\n setDescriptionLength(formState.values.description.length)\n }, [formState.values.description])\n\n return (\n <Card className=\"w-full max-w-md\">\n <CardHeader>\n <CardTitle>Bug Report</CardTitle>\n <CardDescription>\n Help us improve by reporting bugs you encounter.\n </CardDescription>\n </CardHeader>\n <CardContent>\n <Form action={formAction} id=\"bug-report-form\">\n <FieldGroup>\n <Field data-invalid={!!formState.errors?.title?.length}>\n <FieldLabel htmlFor=\"title\">Bug Title</FieldLabel>\n <Input\n id=\"title\"\n name=\"title\"\n defaultValue={formState.values.title}\n disabled={pending}\n aria-invalid={!!formState.errors?.title?.length}\n placeholder=\"Login button not working on mobile\"\n autoComplete=\"off\"\n />\n {formState.errors?.title && (\n <FieldError>{formState.errors.title[0]}</FieldError>\n )}\n </Field>\n <Field data-invalid={!!formState.errors?.description?.length}>\n <FieldLabel htmlFor=\"description\">Description</FieldLabel>\n <InputGroup>\n <InputGroupTextarea\n id=\"description\"\n name=\"description\"\n defaultValue={formState.values.description}\n placeholder=\"I'm having an issue with the login button on mobile.\"\n rows={6}\n className=\"min-h-24 resize-none\"\n disabled={pending}\n aria-invalid={!!formState.errors?.description?.length}\n onChange={(e) => setDescriptionLength(e.target.value.length)}\n />\n <InputGroupAddon align=\"block-end\">\n <InputGroupText className=\"tabular-nums\">\n {descriptionLength}/100 characters\n </InputGroupText>\n </InputGroupAddon>\n </InputGroup>\n <FieldDescription>\n Include steps to reproduce, expected behavior, and what actually\n happened.\n </FieldDescription>\n {formState.errors?.description && (\n <FieldError>{formState.errors.description[0]}</FieldError>\n )}\n </Field>\n </FieldGroup>\n </Form>\n </CardContent>\n <CardFooter>\n <Field orientation=\"horizontal\">\n <Button type=\"submit\" disabled={pending} form=\"bug-report-form\">\n {pending && <Spinner />}\n Submit\n </Button>\n </Field>\n </CardFooter>\n </Card>\n )\n}\n",
|
||||
"type": "registry:example"
|
||||
}
|
||||
]
|
||||
}
|
||||
24
apps/v4/public/r/styles/new-york-v4/form-rhf-array.json
Normal file
24
apps/v4/public/r/styles/new-york-v4/form-rhf-array.json
Normal file
File diff suppressed because one or more lines are too long
23
apps/v4/public/r/styles/new-york-v4/form-rhf-checkbox.json
Normal file
23
apps/v4/public/r/styles/new-york-v4/form-rhf-checkbox.json
Normal file
File diff suppressed because one or more lines are too long
26
apps/v4/public/r/styles/new-york-v4/form-rhf-complex.json
Normal file
26
apps/v4/public/r/styles/new-york-v4/form-rhf-complex.json
Normal file
File diff suppressed because one or more lines are too long
24
apps/v4/public/r/styles/new-york-v4/form-rhf-demo.json
Normal file
24
apps/v4/public/r/styles/new-york-v4/form-rhf-demo.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
||||
"name": "form-rhf-demo",
|
||||
"type": "registry:example",
|
||||
"dependencies": [
|
||||
"react-hook-form",
|
||||
"@hookform/resolvers",
|
||||
"zod"
|
||||
],
|
||||
"registryDependencies": [
|
||||
"field",
|
||||
"input",
|
||||
"input-group",
|
||||
"button",
|
||||
"card"
|
||||
],
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/new-york-v4/examples/form-rhf-demo.tsx",
|
||||
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { zodResolver } from \"@hookform/resolvers/zod\"\nimport { Controller, useForm } from \"react-hook-form\"\nimport { toast } from \"sonner\"\nimport * as z from \"zod\"\n\nimport { Button } from \"@/registry/new-york-v4/ui/button\"\nimport {\n Card,\n CardContent,\n CardDescription,\n CardFooter,\n CardHeader,\n CardTitle,\n} from \"@/registry/new-york-v4/ui/card\"\nimport {\n Field,\n FieldDescription,\n FieldError,\n FieldGroup,\n FieldLabel,\n} from \"@/registry/new-york-v4/ui/field\"\nimport { Input } from \"@/registry/new-york-v4/ui/input\"\nimport {\n InputGroup,\n InputGroupAddon,\n InputGroupText,\n InputGroupTextarea,\n} from \"@/registry/new-york-v4/ui/input-group\"\n\nconst formSchema = z.object({\n title: z\n .string()\n .min(5, \"Bug title must be at least 5 characters.\")\n .max(32, \"Bug title must be at most 32 characters.\"),\n description: z\n .string()\n .min(20, \"Description must be at least 20 characters.\")\n .max(100, \"Description must be at most 100 characters.\"),\n})\n\nexport default function BugReportForm() {\n const form = useForm<z.infer<typeof formSchema>>({\n resolver: zodResolver(formSchema),\n defaultValues: {\n title: \"\",\n description: \"\",\n },\n })\n\n function onSubmit(data: z.infer<typeof formSchema>) {\n toast(\"You submitted the following values:\", {\n description: (\n <pre className=\"bg-code text-code-foreground mt-2 w-[320px] overflow-x-auto rounded-md p-4\">\n <code>{JSON.stringify(data, null, 2)}</code>\n </pre>\n ),\n position: \"bottom-right\",\n classNames: {\n content: \"flex flex-col gap-2\",\n },\n style: {\n \"--border-radius\": \"calc(var(--radius) + 4px)\",\n } as React.CSSProperties,\n })\n }\n\n return (\n <Card className=\"w-full sm:max-w-md\">\n <CardHeader>\n <CardTitle>Bug Report</CardTitle>\n <CardDescription>\n Help us improve by reporting bugs you encounter.\n </CardDescription>\n </CardHeader>\n <CardContent>\n <form id=\"form-rhf-demo\" onSubmit={form.handleSubmit(onSubmit)}>\n <FieldGroup>\n <Controller\n name=\"title\"\n control={form.control}\n render={({ field, fieldState }) => (\n <Field data-invalid={fieldState.invalid}>\n <FieldLabel htmlFor=\"form-rhf-demo-title\">\n Bug Title\n </FieldLabel>\n <Input\n {...field}\n id=\"form-rhf-demo-title\"\n aria-invalid={fieldState.invalid}\n placeholder=\"Login button not working on mobile\"\n autoComplete=\"off\"\n />\n {fieldState.invalid && (\n <FieldError errors={[fieldState.error]} />\n )}\n </Field>\n )}\n />\n <Controller\n name=\"description\"\n control={form.control}\n render={({ field, fieldState }) => (\n <Field data-invalid={fieldState.invalid}>\n <FieldLabel htmlFor=\"form-rhf-demo-description\">\n Description\n </FieldLabel>\n <InputGroup>\n <InputGroupTextarea\n {...field}\n id=\"form-rhf-demo-description\"\n placeholder=\"I'm having an issue with the login button on mobile.\"\n rows={6}\n className=\"min-h-24 resize-none\"\n aria-invalid={fieldState.invalid}\n />\n <InputGroupAddon align=\"block-end\">\n <InputGroupText className=\"tabular-nums\">\n {field.value.length}/100 characters\n </InputGroupText>\n </InputGroupAddon>\n </InputGroup>\n <FieldDescription>\n Include steps to reproduce, expected behavior, and what\n actually happened.\n </FieldDescription>\n {fieldState.invalid && (\n <FieldError errors={[fieldState.error]} />\n )}\n </Field>\n )}\n />\n </FieldGroup>\n </form>\n </CardContent>\n <CardFooter>\n <Field orientation=\"horizontal\">\n <Button type=\"button\" variant=\"outline\" onClick={() => form.reset()}>\n Reset\n </Button>\n <Button type=\"submit\" form=\"form-rhf-demo\">\n Submit\n </Button>\n </Field>\n </CardFooter>\n </Card>\n )\n}\n",
|
||||
"type": "registry:example"
|
||||
}
|
||||
]
|
||||
}
|
||||
23
apps/v4/public/r/styles/new-york-v4/form-rhf-input.json
Normal file
23
apps/v4/public/r/styles/new-york-v4/form-rhf-input.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
||||
"name": "form-rhf-input",
|
||||
"type": "registry:example",
|
||||
"dependencies": [
|
||||
"react-hook-form",
|
||||
"@hookform/resolvers",
|
||||
"zod"
|
||||
],
|
||||
"registryDependencies": [
|
||||
"field",
|
||||
"input",
|
||||
"button",
|
||||
"card"
|
||||
],
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/new-york-v4/examples/form-rhf-input.tsx",
|
||||
"content": "\"use client\"\n\nimport { zodResolver } from \"@hookform/resolvers/zod\"\nimport { Controller, useForm } from \"react-hook-form\"\nimport { toast } from \"sonner\"\nimport * as z from \"zod\"\n\nimport { Button } from \"@/registry/new-york-v4/ui/button\"\nimport {\n Card,\n CardContent,\n CardDescription,\n CardFooter,\n CardHeader,\n CardTitle,\n} from \"@/registry/new-york-v4/ui/card\"\nimport {\n Field,\n FieldDescription,\n FieldError,\n FieldGroup,\n FieldLabel,\n} from \"@/registry/new-york-v4/ui/field\"\nimport { Input } from \"@/registry/new-york-v4/ui/input\"\n\nconst formSchema = z.object({\n username: z\n .string()\n .min(3, \"Username must be at least 3 characters.\")\n .max(10, \"Username must be at most 10 characters.\")\n .regex(\n /^[a-zA-Z0-9_]+$/,\n \"Username can only contain letters, numbers, and underscores.\"\n ),\n})\n\nexport default function FormRhfInput() {\n const form = useForm<z.infer<typeof formSchema>>({\n resolver: zodResolver(formSchema),\n defaultValues: {\n username: \"\",\n },\n })\n\n function onSubmit(data: z.infer<typeof formSchema>) {\n toast(\"You submitted the following values:\", {\n description: (\n <pre className=\"bg-code text-code-foreground mt-2 w-[320px] overflow-x-auto rounded-md p-4\">\n <code>{JSON.stringify(data, null, 2)}</code>\n </pre>\n ),\n position: \"bottom-right\",\n classNames: {\n content: \"flex flex-col gap-2\",\n },\n style: {\n \"--border-radius\": \"calc(var(--radius) + 4px)\",\n } as React.CSSProperties,\n })\n }\n\n return (\n <Card className=\"w-full sm:max-w-md\">\n <CardHeader>\n <CardTitle>Profile Settings</CardTitle>\n <CardDescription>\n Update your profile information below.\n </CardDescription>\n </CardHeader>\n <CardContent>\n <form id=\"form-rhf-input\" onSubmit={form.handleSubmit(onSubmit)}>\n <FieldGroup>\n <Controller\n name=\"username\"\n control={form.control}\n render={({ field, fieldState }) => (\n <Field data-invalid={fieldState.invalid}>\n <FieldLabel htmlFor=\"form-rhf-input-username\">\n Username\n </FieldLabel>\n <Input\n {...field}\n id=\"form-rhf-input-username\"\n aria-invalid={fieldState.invalid}\n placeholder=\"shadcn\"\n autoComplete=\"username\"\n />\n <FieldDescription>\n This is your public display name. Must be between 3 and 10\n characters. Must only contain letters, numbers, and\n underscores.\n </FieldDescription>\n {fieldState.invalid && (\n <FieldError errors={[fieldState.error]} />\n )}\n </Field>\n )}\n />\n </FieldGroup>\n </form>\n </CardContent>\n <CardFooter>\n <Field orientation=\"horizontal\">\n <Button type=\"button\" variant=\"outline\" onClick={() => form.reset()}>\n Reset\n </Button>\n <Button type=\"submit\" form=\"form-rhf-input\">\n Save\n </Button>\n </Field>\n </CardFooter>\n </Card>\n )\n}\n",
|
||||
"type": "registry:example"
|
||||
}
|
||||
]
|
||||
}
|
||||
24
apps/v4/public/r/styles/new-york-v4/form-rhf-password.json
Normal file
24
apps/v4/public/r/styles/new-york-v4/form-rhf-password.json
Normal file
File diff suppressed because one or more lines are too long
23
apps/v4/public/r/styles/new-york-v4/form-rhf-radiogroup.json
Normal file
23
apps/v4/public/r/styles/new-york-v4/form-rhf-radiogroup.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
||||
"name": "form-rhf-radiogroup",
|
||||
"type": "registry:example",
|
||||
"dependencies": [
|
||||
"react-hook-form",
|
||||
"@hookform/resolvers",
|
||||
"zod"
|
||||
],
|
||||
"registryDependencies": [
|
||||
"field",
|
||||
"radio-group",
|
||||
"button",
|
||||
"card"
|
||||
],
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/new-york-v4/examples/form-rhf-radiogroup.tsx",
|
||||
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { zodResolver } from \"@hookform/resolvers/zod\"\nimport { Controller, useForm } from \"react-hook-form\"\nimport { toast } from \"sonner\"\nimport * as z from \"zod\"\n\nimport { Button } from \"@/registry/new-york-v4/ui/button\"\nimport {\n Card,\n CardContent,\n CardDescription,\n CardFooter,\n CardHeader,\n CardTitle,\n} from \"@/registry/new-york-v4/ui/card\"\nimport {\n Field,\n FieldContent,\n FieldDescription,\n FieldError,\n FieldGroup,\n FieldLabel,\n FieldLegend,\n FieldSet,\n FieldTitle,\n} from \"@/registry/new-york-v4/ui/field\"\nimport {\n RadioGroup,\n RadioGroupItem,\n} from \"@/registry/new-york-v4/ui/radio-group\"\n\nconst plans = [\n {\n id: \"starter\",\n title: \"Starter (100K tokens/month)\",\n description: \"For everyday use with basic features.\",\n },\n {\n id: \"pro\",\n title: \"Pro (1M tokens/month)\",\n description: \"For advanced AI usage with more features.\",\n },\n {\n id: \"enterprise\",\n title: \"Enterprise (Unlimited tokens)\",\n description: \"For large teams and heavy usage.\",\n },\n] as const\n\nconst formSchema = z.object({\n plan: z.string().min(1, \"You must select a subscription plan to continue.\"),\n})\n\nexport default function FormRhfRadioGroup() {\n const form = useForm<z.infer<typeof formSchema>>({\n resolver: zodResolver(formSchema),\n defaultValues: {\n plan: \"\",\n },\n })\n\n function onSubmit(data: z.infer<typeof formSchema>) {\n toast(\"You submitted the following values:\", {\n description: (\n <pre className=\"bg-code text-code-foreground mt-2 w-[320px] overflow-x-auto rounded-md p-4\">\n <code>{JSON.stringify(data, null, 2)}</code>\n </pre>\n ),\n position: \"bottom-right\",\n classNames: {\n content: \"flex flex-col gap-2\",\n },\n style: {\n \"--border-radius\": \"calc(var(--radius) + 4px)\",\n } as React.CSSProperties,\n })\n }\n\n return (\n <Card className=\"w-full sm:max-w-md\">\n <CardHeader>\n <CardTitle>Subscription Plan</CardTitle>\n <CardDescription>\n See pricing and features for each plan.\n </CardDescription>\n </CardHeader>\n <CardContent>\n <form id=\"form-rhf-radiogroup\" onSubmit={form.handleSubmit(onSubmit)}>\n <FieldGroup>\n <Controller\n name=\"plan\"\n control={form.control}\n render={({ field, fieldState }) => (\n <FieldSet data-invalid={fieldState.invalid}>\n <FieldLegend>Plan</FieldLegend>\n <FieldDescription>\n You can upgrade or downgrade your plan at any time.\n </FieldDescription>\n <RadioGroup\n name={field.name}\n value={field.value}\n onValueChange={field.onChange}\n aria-invalid={fieldState.invalid}\n >\n {plans.map((plan) => (\n <FieldLabel\n key={plan.id}\n htmlFor={`form-rhf-radiogroup-${plan.id}`}\n >\n <Field\n orientation=\"horizontal\"\n data-invalid={fieldState.invalid}\n >\n <FieldContent>\n <FieldTitle>{plan.title}</FieldTitle>\n <FieldDescription>\n {plan.description}\n </FieldDescription>\n </FieldContent>\n <RadioGroupItem\n value={plan.id}\n id={`form-rhf-radiogroup-${plan.id}`}\n aria-invalid={fieldState.invalid}\n />\n </Field>\n </FieldLabel>\n ))}\n </RadioGroup>\n {fieldState.invalid && (\n <FieldError errors={[fieldState.error]} />\n )}\n </FieldSet>\n )}\n />\n </FieldGroup>\n </form>\n </CardContent>\n <CardFooter>\n <Field orientation=\"horizontal\">\n <Button type=\"button\" variant=\"outline\" onClick={() => form.reset()}>\n Reset\n </Button>\n <Button type=\"submit\" form=\"form-rhf-radiogroup\">\n Save\n </Button>\n </Field>\n </CardFooter>\n </Card>\n )\n}\n",
|
||||
"type": "registry:example"
|
||||
}
|
||||
]
|
||||
}
|
||||
23
apps/v4/public/r/styles/new-york-v4/form-rhf-select.json
Normal file
23
apps/v4/public/r/styles/new-york-v4/form-rhf-select.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
||||
"name": "form-rhf-select",
|
||||
"type": "registry:example",
|
||||
"dependencies": [
|
||||
"react-hook-form",
|
||||
"@hookform/resolvers",
|
||||
"zod"
|
||||
],
|
||||
"registryDependencies": [
|
||||
"field",
|
||||
"select",
|
||||
"button",
|
||||
"card"
|
||||
],
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/new-york-v4/examples/form-rhf-select.tsx",
|
||||
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { zodResolver } from \"@hookform/resolvers/zod\"\nimport { Controller, useForm } from \"react-hook-form\"\nimport { toast } from \"sonner\"\nimport * as z from \"zod\"\n\nimport { Button } from \"@/registry/new-york-v4/ui/button\"\nimport {\n Card,\n CardContent,\n CardDescription,\n CardFooter,\n CardHeader,\n CardTitle,\n} from \"@/registry/new-york-v4/ui/card\"\nimport {\n Field,\n FieldContent,\n FieldDescription,\n FieldError,\n FieldGroup,\n FieldLabel,\n} from \"@/registry/new-york-v4/ui/field\"\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectSeparator,\n SelectTrigger,\n SelectValue,\n} from \"@/registry/new-york-v4/ui/select\"\n\nconst spokenLanguages = [\n { label: \"English\", value: \"en\" },\n { label: \"Spanish\", value: \"es\" },\n { label: \"French\", value: \"fr\" },\n { label: \"German\", value: \"de\" },\n { label: \"Italian\", value: \"it\" },\n { label: \"Chinese\", value: \"zh\" },\n { label: \"Japanese\", value: \"ja\" },\n] as const\n\nconst formSchema = z.object({\n language: z\n .string()\n .min(1, \"Please select your spoken language.\")\n .refine((val) => val !== \"auto\", {\n message:\n \"Auto-detection is not allowed. Please select a specific language.\",\n }),\n})\n\nexport default function FormRhfSelect() {\n const form = useForm<z.infer<typeof formSchema>>({\n resolver: zodResolver(formSchema),\n defaultValues: {\n language: \"\",\n },\n })\n\n function onSubmit(data: z.infer<typeof formSchema>) {\n toast(\"You submitted the following values:\", {\n description: (\n <pre className=\"bg-code text-code-foreground mt-2 w-[320px] overflow-x-auto rounded-md p-4\">\n <code>{JSON.stringify(data, null, 2)}</code>\n </pre>\n ),\n position: \"bottom-right\",\n classNames: {\n content: \"flex flex-col gap-2\",\n },\n style: {\n \"--border-radius\": \"calc(var(--radius) + 4px)\",\n } as React.CSSProperties,\n })\n }\n\n return (\n <Card className=\"w-full sm:max-w-lg\">\n <CardHeader>\n <CardTitle>Language Preferences</CardTitle>\n <CardDescription>\n Select your preferred spoken language.\n </CardDescription>\n </CardHeader>\n <CardContent>\n <form id=\"form-rhf-select\" onSubmit={form.handleSubmit(onSubmit)}>\n <FieldGroup>\n <Controller\n name=\"language\"\n control={form.control}\n render={({ field, fieldState }) => (\n <Field\n orientation=\"responsive\"\n data-invalid={fieldState.invalid}\n >\n <FieldContent>\n <FieldLabel htmlFor=\"form-rhf-select-language\">\n Spoken Language\n </FieldLabel>\n <FieldDescription>\n For best results, select the language you speak.\n </FieldDescription>\n {fieldState.invalid && (\n <FieldError errors={[fieldState.error]} />\n )}\n </FieldContent>\n <Select\n name={field.name}\n value={field.value}\n onValueChange={field.onChange}\n >\n <SelectTrigger\n id=\"form-rhf-select-language\"\n aria-invalid={fieldState.invalid}\n className=\"min-w-[120px]\"\n >\n <SelectValue placeholder=\"Select\" />\n </SelectTrigger>\n <SelectContent position=\"item-aligned\">\n <SelectItem value=\"auto\">Auto</SelectItem>\n <SelectSeparator />\n {spokenLanguages.map((language) => (\n <SelectItem key={language.value} value={language.value}>\n {language.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </Field>\n )}\n />\n </FieldGroup>\n </form>\n </CardContent>\n <CardFooter>\n <Field orientation=\"horizontal\">\n <Button type=\"button\" variant=\"outline\" onClick={() => form.reset()}>\n Reset\n </Button>\n <Button type=\"submit\" form=\"form-rhf-select\">\n Save\n </Button>\n </Field>\n </CardFooter>\n </Card>\n )\n}\n",
|
||||
"type": "registry:example"
|
||||
}
|
||||
]
|
||||
}
|
||||
23
apps/v4/public/r/styles/new-york-v4/form-rhf-switch.json
Normal file
23
apps/v4/public/r/styles/new-york-v4/form-rhf-switch.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
||||
"name": "form-rhf-switch",
|
||||
"type": "registry:example",
|
||||
"dependencies": [
|
||||
"react-hook-form",
|
||||
"@hookform/resolvers",
|
||||
"zod"
|
||||
],
|
||||
"registryDependencies": [
|
||||
"field",
|
||||
"switch",
|
||||
"button",
|
||||
"card"
|
||||
],
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/new-york-v4/examples/form-rhf-switch.tsx",
|
||||
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { zodResolver } from \"@hookform/resolvers/zod\"\nimport { Controller, useForm } from \"react-hook-form\"\nimport { toast } from \"sonner\"\nimport * as z from \"zod\"\n\nimport { Button } from \"@/registry/new-york-v4/ui/button\"\nimport {\n Card,\n CardContent,\n CardDescription,\n CardFooter,\n CardHeader,\n CardTitle,\n} from \"@/registry/new-york-v4/ui/card\"\nimport {\n Field,\n FieldContent,\n FieldDescription,\n FieldError,\n FieldGroup,\n FieldLabel,\n} from \"@/registry/new-york-v4/ui/field\"\nimport { Switch } from \"@/registry/new-york-v4/ui/switch\"\n\nconst formSchema = z.object({\n twoFactor: z.boolean().refine((val) => val === true, {\n message: \"It is highly recommended to enable two-factor authentication.\",\n }),\n})\n\nexport default function FormRhfSwitch() {\n const form = useForm<z.infer<typeof formSchema>>({\n resolver: zodResolver(formSchema),\n defaultValues: {\n twoFactor: false,\n },\n })\n\n function onSubmit(data: z.infer<typeof formSchema>) {\n toast(\"You submitted the following values:\", {\n description: (\n <pre className=\"bg-code text-code-foreground mt-2 w-[320px] overflow-x-auto rounded-md p-4\">\n <code>{JSON.stringify(data, null, 2)}</code>\n </pre>\n ),\n position: \"bottom-right\",\n classNames: {\n content: \"flex flex-col gap-2\",\n },\n style: {\n \"--border-radius\": \"calc(var(--radius) + 4px)\",\n } as React.CSSProperties,\n })\n }\n\n return (\n <Card className=\"w-full sm:max-w-md\">\n <CardHeader>\n <CardTitle>Security Settings</CardTitle>\n <CardDescription>\n Manage your account security preferences.\n </CardDescription>\n </CardHeader>\n <CardContent>\n <form id=\"form-rhf-switch\" onSubmit={form.handleSubmit(onSubmit)}>\n <FieldGroup>\n <Controller\n name=\"twoFactor\"\n control={form.control}\n render={({ field, fieldState }) => (\n <Field\n orientation=\"horizontal\"\n data-invalid={fieldState.invalid}\n >\n <FieldContent>\n <FieldLabel htmlFor=\"form-rhf-switch-twoFactor\">\n Multi-factor authentication\n </FieldLabel>\n <FieldDescription>\n Enable multi-factor authentication to secure your account.\n </FieldDescription>\n {fieldState.invalid && (\n <FieldError errors={[fieldState.error]} />\n )}\n </FieldContent>\n <Switch\n id=\"form-rhf-switch-twoFactor\"\n name={field.name}\n checked={field.value}\n onCheckedChange={field.onChange}\n aria-invalid={fieldState.invalid}\n />\n </Field>\n )}\n />\n </FieldGroup>\n </form>\n </CardContent>\n <CardFooter>\n <Field orientation=\"horizontal\">\n <Button type=\"button\" variant=\"outline\" onClick={() => form.reset()}>\n Reset\n </Button>\n <Button type=\"submit\" form=\"form-rhf-switch\">\n Save\n </Button>\n </Field>\n </CardFooter>\n </Card>\n )\n}\n",
|
||||
"type": "registry:example"
|
||||
}
|
||||
]
|
||||
}
|
||||
23
apps/v4/public/r/styles/new-york-v4/form-rhf-textarea.json
Normal file
23
apps/v4/public/r/styles/new-york-v4/form-rhf-textarea.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
||||
"name": "form-rhf-textarea",
|
||||
"type": "registry:example",
|
||||
"dependencies": [
|
||||
"react-hook-form",
|
||||
"@hookform/resolvers",
|
||||
"zod"
|
||||
],
|
||||
"registryDependencies": [
|
||||
"field",
|
||||
"textarea",
|
||||
"button",
|
||||
"card"
|
||||
],
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/new-york-v4/examples/form-rhf-textarea.tsx",
|
||||
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { zodResolver } from \"@hookform/resolvers/zod\"\nimport { Controller, useForm } from \"react-hook-form\"\nimport { toast } from \"sonner\"\nimport * as z from \"zod\"\n\nimport { Button } from \"@/registry/new-york-v4/ui/button\"\nimport {\n Card,\n CardContent,\n CardDescription,\n CardFooter,\n CardHeader,\n CardTitle,\n} from \"@/registry/new-york-v4/ui/card\"\nimport {\n Field,\n FieldDescription,\n FieldError,\n FieldGroup,\n FieldLabel,\n} from \"@/registry/new-york-v4/ui/field\"\nimport { Textarea } from \"@/registry/new-york-v4/ui/textarea\"\n\nconst formSchema = z.object({\n about: z\n .string()\n .min(10, \"Please provide at least 10 characters.\")\n .max(200, \"Please keep it under 200 characters.\"),\n})\n\nexport default function FormRhfTextarea() {\n const form = useForm<z.infer<typeof formSchema>>({\n resolver: zodResolver(formSchema),\n defaultValues: {\n about: \"\",\n },\n })\n\n function onSubmit(data: z.infer<typeof formSchema>) {\n toast(\"You submitted the following values:\", {\n description: (\n <pre className=\"bg-code text-code-foreground mt-2 w-[320px] overflow-x-auto rounded-md p-4\">\n <code>{JSON.stringify(data, null, 2)}</code>\n </pre>\n ),\n position: \"bottom-right\",\n classNames: {\n content: \"flex flex-col gap-2\",\n },\n style: {\n \"--border-radius\": \"calc(var(--radius) + 4px)\",\n } as React.CSSProperties,\n })\n }\n\n return (\n <Card className=\"w-full sm:max-w-md\">\n <CardHeader>\n <CardTitle>Personalization</CardTitle>\n <CardDescription>\n Customize your experience by telling us more about yourself.\n </CardDescription>\n </CardHeader>\n <CardContent>\n <form id=\"form-rhf-textarea\" onSubmit={form.handleSubmit(onSubmit)}>\n <FieldGroup>\n <Controller\n name=\"about\"\n control={form.control}\n render={({ field, fieldState }) => (\n <Field data-invalid={fieldState.invalid}>\n <FieldLabel htmlFor=\"form-rhf-textarea-about\">\n More about you\n </FieldLabel>\n <Textarea\n {...field}\n id=\"form-rhf-textarea-about\"\n aria-invalid={fieldState.invalid}\n placeholder=\"I'm a software engineer...\"\n className=\"min-h-[120px]\"\n />\n <FieldDescription>\n Tell us more about yourself. This will be used to help us\n personalize your experience.\n </FieldDescription>\n {fieldState.invalid && (\n <FieldError errors={[fieldState.error]} />\n )}\n </Field>\n )}\n />\n </FieldGroup>\n </form>\n </CardContent>\n <CardFooter>\n <Field orientation=\"horizontal\">\n <Button type=\"button\" variant=\"outline\" onClick={() => form.reset()}>\n Reset\n </Button>\n <Button type=\"submit\" form=\"form-rhf-textarea\">\n Save\n </Button>\n </Field>\n </CardFooter>\n </Card>\n )\n}\n",
|
||||
"type": "registry:example"
|
||||
}
|
||||
]
|
||||
}
|
||||
23
apps/v4/public/r/styles/new-york-v4/form-tanstack-array.json
Normal file
23
apps/v4/public/r/styles/new-york-v4/form-tanstack-array.json
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
23
apps/v4/public/r/styles/new-york-v4/form-tanstack-demo.json
Normal file
23
apps/v4/public/r/styles/new-york-v4/form-tanstack-demo.json
Normal file
File diff suppressed because one or more lines are too long
22
apps/v4/public/r/styles/new-york-v4/form-tanstack-input.json
Normal file
22
apps/v4/public/r/styles/new-york-v4/form-tanstack-input.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
||||
"name": "form-tanstack-input",
|
||||
"type": "registry:example",
|
||||
"dependencies": [
|
||||
"@tanstack/react-form",
|
||||
"zod"
|
||||
],
|
||||
"registryDependencies": [
|
||||
"field",
|
||||
"input",
|
||||
"button",
|
||||
"card"
|
||||
],
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/new-york-v4/examples/form-tanstack-input.tsx",
|
||||
"content": "/* eslint-disable react/no-children-prop */\n\"use client\"\n\nimport { useForm } from \"@tanstack/react-form\"\nimport { toast } from \"sonner\"\nimport * as z from \"zod\"\n\nimport { Button } from \"@/registry/new-york-v4/ui/button\"\nimport {\n Card,\n CardContent,\n CardDescription,\n CardFooter,\n CardHeader,\n CardTitle,\n} from \"@/registry/new-york-v4/ui/card\"\nimport {\n Field,\n FieldDescription,\n FieldError,\n FieldGroup,\n FieldLabel,\n} from \"@/registry/new-york-v4/ui/field\"\nimport { Input } from \"@/registry/new-york-v4/ui/input\"\n\nconst formSchema = z.object({\n username: z\n .string()\n .min(3, \"Username must be at least 3 characters.\")\n .max(10, \"Username must be at most 10 characters.\")\n .regex(\n /^[a-zA-Z0-9_]+$/,\n \"Username can only contain letters, numbers, and underscores.\"\n ),\n})\n\nexport default function FormTanstackInput() {\n const form = useForm({\n defaultValues: {\n username: \"\",\n },\n validators: {\n onSubmit: formSchema,\n },\n onSubmit: async ({ value }) => {\n toast(\"You submitted the following values:\", {\n description: (\n <pre className=\"bg-code text-code-foreground mt-2 w-[320px] overflow-x-auto rounded-md p-4\">\n <code>{JSON.stringify(value, null, 2)}</code>\n </pre>\n ),\n position: \"bottom-right\",\n classNames: {\n content: \"flex flex-col gap-2\",\n },\n style: {\n \"--border-radius\": \"calc(var(--radius) + 4px)\",\n } as React.CSSProperties,\n })\n },\n })\n\n return (\n <Card className=\"w-full sm:max-w-md\">\n <CardHeader>\n <CardTitle>Profile Settings</CardTitle>\n <CardDescription>\n Update your profile information below.\n </CardDescription>\n </CardHeader>\n <CardContent>\n <form\n id=\"form-tanstack-input\"\n onSubmit={(e) => {\n e.preventDefault()\n form.handleSubmit()\n }}\n >\n <FieldGroup>\n <form.Field\n name=\"username\"\n children={(field) => {\n const isInvalid =\n field.state.meta.isTouched && !field.state.meta.isValid\n return (\n <Field data-invalid={isInvalid}>\n <FieldLabel htmlFor=\"form-tanstack-input-username\">\n Username\n </FieldLabel>\n <Input\n id=\"form-tanstack-input-username\"\n name={field.name}\n value={field.state.value}\n onBlur={field.handleBlur}\n onChange={(e) => field.handleChange(e.target.value)}\n aria-invalid={isInvalid}\n placeholder=\"shadcn\"\n autoComplete=\"username\"\n />\n <FieldDescription>\n This is your public display name. Must be between 3 and 10\n characters. Must only contain letters, numbers, and\n underscores.\n </FieldDescription>\n {isInvalid && (\n <FieldError errors={field.state.meta.errors} />\n )}\n </Field>\n )\n }}\n />\n </FieldGroup>\n </form>\n </CardContent>\n <CardFooter>\n <Field orientation=\"horizontal\">\n <Button type=\"button\" variant=\"outline\" onClick={() => form.reset()}>\n Reset\n </Button>\n <Button type=\"submit\" form=\"form-tanstack-input\">\n Save\n </Button>\n </Field>\n </CardFooter>\n </Card>\n )\n}\n",
|
||||
"type": "registry:example"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
||||
"name": "form-tanstack-radiogroup",
|
||||
"type": "registry:example",
|
||||
"dependencies": [
|
||||
"@tanstack/react-form",
|
||||
"zod"
|
||||
],
|
||||
"registryDependencies": [
|
||||
"field",
|
||||
"radio-group",
|
||||
"button",
|
||||
"card"
|
||||
],
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/new-york-v4/examples/form-tanstack-radiogroup.tsx",
|
||||
"content": "/* eslint-disable react/no-children-prop */\n\"use client\"\n\nimport { useForm } from \"@tanstack/react-form\"\nimport { toast } from \"sonner\"\nimport * as z from \"zod\"\n\nimport { Button } from \"@/registry/new-york-v4/ui/button\"\nimport {\n Card,\n CardContent,\n CardDescription,\n CardFooter,\n CardHeader,\n CardTitle,\n} from \"@/registry/new-york-v4/ui/card\"\nimport {\n Field,\n FieldContent,\n FieldDescription,\n FieldError,\n FieldGroup,\n FieldLabel,\n FieldLegend,\n FieldSet,\n FieldTitle,\n} from \"@/registry/new-york-v4/ui/field\"\nimport {\n RadioGroup,\n RadioGroupItem,\n} from \"@/registry/new-york-v4/ui/radio-group\"\n\nconst plans = [\n {\n id: \"starter\",\n title: \"Starter (100K tokens/month)\",\n description: \"For everyday use with basic features.\",\n },\n {\n id: \"pro\",\n title: \"Pro (1M tokens/month)\",\n description: \"For advanced AI usage with more features.\",\n },\n {\n id: \"enterprise\",\n title: \"Enterprise (Unlimited tokens)\",\n description: \"For large teams and heavy usage.\",\n },\n] as const\n\nconst formSchema = z.object({\n plan: z.string().min(1, \"You must select a subscription plan to continue.\"),\n})\n\nexport default function FormTanstackRadioGroup() {\n const form = useForm({\n defaultValues: {\n plan: \"\",\n },\n validators: {\n onSubmit: formSchema,\n },\n onSubmit: async ({ value }) => {\n toast(\"You submitted the following values:\", {\n description: (\n <pre className=\"bg-code text-code-foreground mt-2 w-[320px] overflow-x-auto rounded-md p-4\">\n <code>{JSON.stringify(value, null, 2)}</code>\n </pre>\n ),\n position: \"bottom-right\",\n classNames: {\n content: \"flex flex-col gap-2\",\n },\n style: {\n \"--border-radius\": \"calc(var(--radius) + 4px)\",\n } as React.CSSProperties,\n })\n },\n })\n\n return (\n <Card className=\"w-full sm:max-w-md\">\n <CardHeader>\n <CardTitle>Subscription Plan</CardTitle>\n <CardDescription>\n See pricing and features for each plan.\n </CardDescription>\n </CardHeader>\n <CardContent>\n <form\n id=\"form-tanstack-radiogroup\"\n onSubmit={(e) => {\n e.preventDefault()\n form.handleSubmit()\n }}\n >\n <FieldGroup>\n <form.Field\n name=\"plan\"\n children={(field) => {\n const isInvalid =\n field.state.meta.isTouched && !field.state.meta.isValid\n return (\n <FieldSet>\n <FieldLegend>Plan</FieldLegend>\n <FieldDescription>\n You can upgrade or downgrade your plan at any time.\n </FieldDescription>\n <RadioGroup\n name={field.name}\n value={field.state.value}\n onValueChange={field.handleChange}\n >\n {plans.map((plan) => (\n <FieldLabel\n key={plan.id}\n htmlFor={`form-tanstack-radiogroup-${plan.id}`}\n >\n <Field\n orientation=\"horizontal\"\n data-invalid={isInvalid}\n >\n <FieldContent>\n <FieldTitle>{plan.title}</FieldTitle>\n <FieldDescription>\n {plan.description}\n </FieldDescription>\n </FieldContent>\n <RadioGroupItem\n value={plan.id}\n id={`form-tanstack-radiogroup-${plan.id}`}\n aria-invalid={isInvalid}\n />\n </Field>\n </FieldLabel>\n ))}\n </RadioGroup>\n {isInvalid && (\n <FieldError errors={field.state.meta.errors} />\n )}\n </FieldSet>\n )\n }}\n />\n </FieldGroup>\n </form>\n </CardContent>\n <CardFooter>\n <Field orientation=\"horizontal\">\n <Button type=\"button\" variant=\"outline\" onClick={() => form.reset()}>\n Reset\n </Button>\n <Button type=\"submit\" form=\"form-tanstack-radiogroup\">\n Save\n </Button>\n </Field>\n </CardFooter>\n </Card>\n )\n}\n",
|
||||
"type": "registry:example"
|
||||
}
|
||||
]
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
||||
"name": "form-tanstack-switch",
|
||||
"type": "registry:example",
|
||||
"dependencies": [
|
||||
"@tanstack/react-form",
|
||||
"zod"
|
||||
],
|
||||
"registryDependencies": [
|
||||
"field",
|
||||
"switch",
|
||||
"button",
|
||||
"card"
|
||||
],
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/new-york-v4/examples/form-tanstack-switch.tsx",
|
||||
"content": "/* eslint-disable react/no-children-prop */\n\"use client\"\n\nimport { useForm } from \"@tanstack/react-form\"\nimport { toast } from \"sonner\"\nimport * as z from \"zod\"\n\nimport { Button } from \"@/registry/new-york-v4/ui/button\"\nimport {\n Card,\n CardContent,\n CardDescription,\n CardFooter,\n CardHeader,\n CardTitle,\n} from \"@/registry/new-york-v4/ui/card\"\nimport {\n Field,\n FieldContent,\n FieldDescription,\n FieldError,\n FieldGroup,\n FieldLabel,\n} from \"@/registry/new-york-v4/ui/field\"\nimport { Switch } from \"@/registry/new-york-v4/ui/switch\"\n\nconst formSchema = z.object({\n twoFactor: z.boolean().refine((val) => val === true, {\n message: \"It is highly recommended to enable two-factor authentication.\",\n }),\n})\n\nexport default function FormTanstackSwitch() {\n const form = useForm({\n defaultValues: {\n twoFactor: false,\n },\n validators: {\n onSubmit: formSchema,\n },\n onSubmit: async ({ value }) => {\n toast(\"You submitted the following values:\", {\n description: (\n <pre className=\"bg-code text-code-foreground mt-2 w-[320px] overflow-x-auto rounded-md p-4\">\n <code>{JSON.stringify(value, null, 2)}</code>\n </pre>\n ),\n position: \"bottom-right\",\n classNames: {\n content: \"flex flex-col gap-2\",\n },\n style: {\n \"--border-radius\": \"calc(var(--radius) + 4px)\",\n } as React.CSSProperties,\n })\n },\n })\n\n return (\n <Card className=\"w-full sm:max-w-md\">\n <CardHeader>\n <CardTitle>Security Settings</CardTitle>\n <CardDescription>\n Manage your account security preferences.\n </CardDescription>\n </CardHeader>\n <CardContent>\n <form\n id=\"form-tanstack-switch\"\n onSubmit={(e) => {\n e.preventDefault()\n form.handleSubmit()\n }}\n >\n <FieldGroup>\n <form.Field\n name=\"twoFactor\"\n children={(field) => {\n const isInvalid =\n field.state.meta.isTouched && !field.state.meta.isValid\n return (\n <Field orientation=\"horizontal\" data-invalid={isInvalid}>\n <FieldContent>\n <FieldLabel htmlFor=\"form-tanstack-switch-twoFactor\">\n Multi-factor authentication\n </FieldLabel>\n <FieldDescription>\n Enable multi-factor authentication to secure your\n account.\n </FieldDescription>\n {isInvalid && (\n <FieldError errors={field.state.meta.errors} />\n )}\n </FieldContent>\n <Switch\n id=\"form-tanstack-switch-twoFactor\"\n name={field.name}\n checked={field.state.value}\n onCheckedChange={field.handleChange}\n aria-invalid={isInvalid}\n />\n </Field>\n )\n }}\n />\n </FieldGroup>\n </form>\n </CardContent>\n <CardFooter>\n <Field orientation=\"horizontal\">\n <Button type=\"button\" variant=\"outline\" onClick={() => form.reset()}>\n Reset\n </Button>\n <Button type=\"submit\" form=\"form-tanstack-switch\">\n Save\n </Button>\n </Field>\n </CardFooter>\n </Card>\n )\n}\n",
|
||||
"type": "registry:example"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
||||
"name": "form-tanstack-textarea",
|
||||
"type": "registry:example",
|
||||
"dependencies": [
|
||||
"@tanstack/react-form",
|
||||
"zod"
|
||||
],
|
||||
"registryDependencies": [
|
||||
"field",
|
||||
"textarea",
|
||||
"button",
|
||||
"card"
|
||||
],
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/new-york-v4/examples/form-tanstack-textarea.tsx",
|
||||
"content": "/* eslint-disable react/no-children-prop */\n\"use client\"\n\nimport { useForm } from \"@tanstack/react-form\"\nimport { toast } from \"sonner\"\nimport * as z from \"zod\"\n\nimport { Button } from \"@/registry/new-york-v4/ui/button\"\nimport {\n Card,\n CardContent,\n CardDescription,\n CardFooter,\n CardHeader,\n CardTitle,\n} from \"@/registry/new-york-v4/ui/card\"\nimport {\n Field,\n FieldDescription,\n FieldError,\n FieldGroup,\n FieldLabel,\n} from \"@/registry/new-york-v4/ui/field\"\nimport { Textarea } from \"@/registry/new-york-v4/ui/textarea\"\n\nconst formSchema = z.object({\n about: z\n .string()\n .min(10, \"Please provide at least 10 characters.\")\n .max(200, \"Please keep it under 200 characters.\"),\n})\n\nexport default function FormTanstackTextarea() {\n const form = useForm({\n defaultValues: {\n about: \"\",\n },\n validators: {\n onSubmit: formSchema,\n },\n onSubmit: async ({ value }) => {\n toast(\"You submitted the following values:\", {\n description: (\n <pre className=\"bg-code text-code-foreground mt-2 w-[320px] overflow-x-auto rounded-md p-4\">\n <code>{JSON.stringify(value, null, 2)}</code>\n </pre>\n ),\n position: \"bottom-right\",\n classNames: {\n content: \"flex flex-col gap-2\",\n },\n style: {\n \"--border-radius\": \"calc(var(--radius) + 4px)\",\n } as React.CSSProperties,\n })\n },\n })\n\n return (\n <Card className=\"w-full sm:max-w-md\">\n <CardHeader>\n <CardTitle>Personalization</CardTitle>\n <CardDescription>\n Customize your experience by telling us more about yourself.\n </CardDescription>\n </CardHeader>\n <CardContent>\n <form\n id=\"form-tanstack-textarea\"\n onSubmit={(e) => {\n e.preventDefault()\n form.handleSubmit()\n }}\n >\n <FieldGroup>\n <form.Field\n name=\"about\"\n children={(field) => {\n const isInvalid =\n field.state.meta.isTouched && !field.state.meta.isValid\n return (\n <Field data-invalid={isInvalid}>\n <FieldLabel htmlFor=\"form-tanstack-textarea-about\">\n More about you\n </FieldLabel>\n <Textarea\n id=\"form-tanstack-textarea-about\"\n name={field.name}\n value={field.state.value}\n onBlur={field.handleBlur}\n onChange={(e) => field.handleChange(e.target.value)}\n aria-invalid={isInvalid}\n placeholder=\"I'm a software engineer...\"\n className=\"min-h-[120px]\"\n />\n <FieldDescription>\n Tell us more about yourself. This will be used to help us\n personalize your experience.\n </FieldDescription>\n {isInvalid && (\n <FieldError errors={field.state.meta.errors} />\n )}\n </Field>\n )\n }}\n />\n </FieldGroup>\n </form>\n </CardContent>\n <CardFooter>\n <Field orientation=\"horizontal\">\n <Button type=\"button\" variant=\"outline\" onClick={() => form.reset()}>\n Reset\n </Button>\n <Button type=\"submit\" form=\"form-tanstack-textarea\">\n Save\n </Button>\n </Field>\n </CardFooter>\n </Card>\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
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
15
apps/v4/public/r/styles/new-york-v4/sonner-types.json
Normal file
15
apps/v4/public/r/styles/new-york-v4/sonner-types.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
||||
"name": "sonner-types",
|
||||
"type": "registry:example",
|
||||
"registryDependencies": [
|
||||
"sonner"
|
||||
],
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/new-york-v4/examples/sonner-types.tsx",
|
||||
"content": "\"use client\"\n\nimport { toast } from \"sonner\"\n\nimport { Button } from \"@/registry/new-york-v4/ui/button\"\n\nexport default function SonnerTypes() {\n return (\n <div className=\"flex flex-wrap gap-2\">\n <Button variant=\"outline\" onClick={() => toast(\"Event has been created\")}>\n Default\n </Button>\n <Button\n variant=\"outline\"\n onClick={() => toast.success(\"Event has been created\")}\n >\n Success\n </Button>\n <Button\n variant=\"outline\"\n onClick={() =>\n toast.info(\"Be at the area 10 minutes before the event time\")\n }\n >\n Info\n </Button>\n <Button\n variant=\"outline\"\n onClick={() =>\n toast.warning(\"Event start time cannot be earlier than 8am\")\n }\n >\n Warning\n </Button>\n <Button\n variant=\"outline\"\n onClick={() => toast.error(\"Event has not been created\")}\n >\n Error\n </Button>\n <Button\n variant=\"outline\"\n onClick={() => {\n toast.promise<{ name: string }>(\n () =>\n new Promise((resolve) =>\n setTimeout(() => resolve({ name: \"Event\" }), 2000)\n ),\n {\n loading: \"Loading...\",\n success: (data) => `${data.name} has been created`,\n error: \"Error\",\n }\n )\n }}\n >\n Promise\n </Button>\n </div>\n )\n}\n",
|
||||
"type": "registry:example"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -9,7 +9,7 @@
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/new-york-v4/ui/sonner.tsx",
|
||||
"content": "\"use client\"\n\nimport { useTheme } from \"next-themes\"\nimport { Toaster as Sonner, ToasterProps } from \"sonner\"\n\nconst Toaster = ({ ...props }: ToasterProps) => {\n const { theme = \"system\" } = useTheme()\n\n return (\n <Sonner\n theme={theme as ToasterProps[\"theme\"]}\n className=\"toaster group\"\n style={\n {\n \"--normal-bg\": \"var(--popover)\",\n \"--normal-text\": \"var(--popover-foreground)\",\n \"--normal-border\": \"var(--border)\",\n } as React.CSSProperties\n }\n {...props}\n />\n )\n}\n\nexport { Toaster }\n",
|
||||
"content": "\"use client\"\n\nimport {\n CircleCheckIcon,\n InfoIcon,\n Loader2Icon,\n OctagonXIcon,\n TriangleAlertIcon,\n} from \"lucide-react\"\nimport { useTheme } from \"next-themes\"\nimport { Toaster as Sonner, type ToasterProps } from \"sonner\"\n\nconst Toaster = ({ ...props }: ToasterProps) => {\n const { theme = \"system\" } = useTheme()\n\n return (\n <Sonner\n theme={theme as ToasterProps[\"theme\"]}\n className=\"toaster group\"\n icons={{\n success: <CircleCheckIcon className=\"size-4\" />,\n info: <InfoIcon className=\"size-4\" />,\n warning: <TriangleAlertIcon className=\"size-4\" />,\n error: <OctagonXIcon className=\"size-4\" />,\n loading: <Loader2Icon className=\"size-4 animate-spin\" />,\n }}\n style={\n {\n \"--normal-bg\": \"var(--popover)\",\n \"--normal-text\": \"var(--popover-foreground)\",\n \"--normal-border\": \"var(--border)\",\n \"--border-radius\": \"var(--radius)\",\n } as React.CSSProperties\n }\n {...props}\n />\n )\n}\n\nexport { Toaster }\n",
|
||||
"type": "registry:ui"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"files": [
|
||||
{
|
||||
"path": "ui/checkbox.tsx",
|
||||
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as CheckboxPrimitive from \"@radix-ui/react-checkbox\"\nimport { Check } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst Checkbox = React.forwardRef<\n React.ElementRef<typeof CheckboxPrimitive.Root>,\n React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>\n>(({ className, ...props }, ref) => (\n <CheckboxPrimitive.Root\n ref={ref}\n className={cn(\n \"peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground\",\n className\n )}\n {...props}\n >\n <CheckboxPrimitive.Indicator\n className={cn(\"flex items-center justify-center text-current\")}\n >\n <Check className=\"h-4 w-4\" />\n </CheckboxPrimitive.Indicator>\n </CheckboxPrimitive.Root>\n))\nCheckbox.displayName = CheckboxPrimitive.Root.displayName\n\nexport { Checkbox }\n",
|
||||
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as CheckboxPrimitive from \"@radix-ui/react-checkbox\"\nimport { Check } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst Checkbox = React.forwardRef<\n React.ElementRef<typeof CheckboxPrimitive.Root>,\n React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>\n>(({ className, ...props }, ref) => (\n <CheckboxPrimitive.Root\n ref={ref}\n className={cn(\n \"grid place-content-center peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground\",\n className\n )}\n {...props}\n >\n <CheckboxPrimitive.Indicator\n className={cn(\"grid place-content-center text-current\")}\n >\n <Check className=\"h-4 w-4\" />\n </CheckboxPrimitive.Indicator>\n </CheckboxPrimitive.Root>\n))\nCheckbox.displayName = CheckboxPrimitive.Root.displayName\n\nexport { Checkbox }\n",
|
||||
"type": "registry:ui",
|
||||
"target": ""
|
||||
}
|
||||
|
||||
@@ -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/new-york/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-[0.8rem] 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-[0.8rem] 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/new-york/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-[0.8rem] 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-[0.8rem] 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
@@ -2161,141 +2161,6 @@
|
||||
],
|
||||
"categories": ["authentication", "otp"]
|
||||
},
|
||||
{
|
||||
"name": "new-components-01",
|
||||
"type": "registry:block",
|
||||
"description": "New components",
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/new-york-v4/blocks/new-components-01/page.tsx",
|
||||
"type": "registry:page",
|
||||
"target": "app/page.tsx"
|
||||
},
|
||||
{
|
||||
"path": "registry/new-york-v4/blocks/new-components-01/components/ui/field.tsx",
|
||||
"type": "registry:ui",
|
||||
"target": "components/ui/field.tsx"
|
||||
},
|
||||
{
|
||||
"path": "registry/new-york-v4/blocks/new-components-01/components/ui/button-group.tsx",
|
||||
"type": "registry:ui",
|
||||
"target": "components/ui/button-group.tsx"
|
||||
},
|
||||
{
|
||||
"path": "registry/new-york-v4/blocks/new-components-01/components/ui/input-group.tsx",
|
||||
"type": "registry:ui",
|
||||
"target": "components/ui/input-group.tsx"
|
||||
},
|
||||
{
|
||||
"path": "registry/new-york-v4/blocks/new-components-01/components/ui/empty.tsx",
|
||||
"type": "registry:ui",
|
||||
"target": "components/ui/empty.tsx"
|
||||
},
|
||||
{
|
||||
"path": "registry/new-york-v4/blocks/new-components-01/components/ui/item.tsx",
|
||||
"type": "registry:ui",
|
||||
"target": "components/ui/item.tsx"
|
||||
},
|
||||
{
|
||||
"path": "registry/new-york-v4/blocks/new-components-01/components/ui/spinner.tsx",
|
||||
"type": "registry:ui",
|
||||
"target": "components/ui/spinner.tsx"
|
||||
},
|
||||
{
|
||||
"path": "registry/new-york-v4/blocks/new-components-01/components/ui/button.tsx",
|
||||
"type": "registry:ui",
|
||||
"target": "components/ui/button.tsx"
|
||||
},
|
||||
{
|
||||
"path": "registry/new-york-v4/blocks/new-components-01/components/ui/input.tsx",
|
||||
"type": "registry:ui",
|
||||
"target": "components/ui/input.tsx"
|
||||
},
|
||||
{
|
||||
"path": "registry/new-york-v4/blocks/new-components-01/components/ui/tooltip.tsx",
|
||||
"type": "registry:ui",
|
||||
"target": "components/ui/tooltip.tsx"
|
||||
},
|
||||
{
|
||||
"path": "registry/new-york-v4/blocks/new-components-01/components/ui/dropdown-menu.tsx",
|
||||
"type": "registry:ui",
|
||||
"target": "components/ui/dropdown-menu.tsx"
|
||||
},
|
||||
{
|
||||
"path": "registry/new-york-v4/blocks/new-components-01/components/appearance-settings.tsx",
|
||||
"type": "registry:component"
|
||||
},
|
||||
{
|
||||
"path": "registry/new-york-v4/blocks/new-components-01/components/button-group-demo.tsx",
|
||||
"type": "registry:component"
|
||||
},
|
||||
{
|
||||
"path": "registry/new-york-v4/blocks/new-components-01/components/button-group-input-group.tsx",
|
||||
"type": "registry:component"
|
||||
},
|
||||
{
|
||||
"path": "registry/new-york-v4/blocks/new-components-01/components/button-group-nested.tsx",
|
||||
"type": "registry:component"
|
||||
},
|
||||
{
|
||||
"path": "registry/new-york-v4/blocks/new-components-01/components/button-group-popover.tsx",
|
||||
"type": "registry:component"
|
||||
},
|
||||
{
|
||||
"path": "registry/new-york-v4/blocks/new-components-01/components/empty-avatar-group.tsx",
|
||||
"type": "registry:component"
|
||||
},
|
||||
{
|
||||
"path": "registry/new-york-v4/blocks/new-components-01/components/empty-input-group.tsx",
|
||||
"type": "registry:component"
|
||||
},
|
||||
{
|
||||
"path": "registry/new-york-v4/blocks/new-components-01/components/field-choice-card.tsx",
|
||||
"type": "registry:component"
|
||||
},
|
||||
{
|
||||
"path": "registry/new-york-v4/blocks/new-components-01/components/field-demo.tsx",
|
||||
"type": "registry:component"
|
||||
},
|
||||
{
|
||||
"path": "registry/new-york-v4/blocks/new-components-01/components/field-slider.tsx",
|
||||
"type": "registry:component"
|
||||
},
|
||||
{
|
||||
"path": "registry/new-york-v4/blocks/new-components-01/components/input-group-button.tsx",
|
||||
"type": "registry:component"
|
||||
},
|
||||
{
|
||||
"path": "registry/new-york-v4/blocks/new-components-01/components/input-group-demo.tsx",
|
||||
"type": "registry:component"
|
||||
},
|
||||
{
|
||||
"path": "registry/new-york-v4/blocks/new-components-01/components/input-group-textarea.tsx",
|
||||
"type": "registry:component"
|
||||
},
|
||||
{
|
||||
"path": "registry/new-york-v4/blocks/new-components-01/components/item-avatar.tsx",
|
||||
"type": "registry:component"
|
||||
},
|
||||
{
|
||||
"path": "registry/new-york-v4/blocks/new-components-01/components/item-demo.tsx",
|
||||
"type": "registry:component"
|
||||
},
|
||||
{
|
||||
"path": "registry/new-york-v4/blocks/new-components-01/components/notion-prompt-form.tsx",
|
||||
"type": "registry:component"
|
||||
},
|
||||
{
|
||||
"path": "registry/new-york-v4/blocks/new-components-01/components/spinner-badge.tsx",
|
||||
"type": "registry:component"
|
||||
},
|
||||
{
|
||||
"path": "registry/new-york-v4/blocks/new-components-01/components/spinner-empty.tsx",
|
||||
"type": "registry:component"
|
||||
}
|
||||
],
|
||||
"categories": ["components", "showcase"]
|
||||
},
|
||||
{
|
||||
"name": "chart-area-axes",
|
||||
"type": "registry:block",
|
||||
@@ -4527,6 +4392,280 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "form-rhf-demo",
|
||||
"type": "registry:example",
|
||||
"dependencies": ["react-hook-form", "@hookform/resolvers", "zod"],
|
||||
"registryDependencies": [
|
||||
"field",
|
||||
"input",
|
||||
"input-group",
|
||||
"button",
|
||||
"card"
|
||||
],
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/new-york-v4/examples/form-rhf-demo.tsx",
|
||||
"type": "registry:example"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "form-rhf-input",
|
||||
"type": "registry:example",
|
||||
"dependencies": ["react-hook-form", "@hookform/resolvers", "zod"],
|
||||
"registryDependencies": ["field", "input", "button", "card"],
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/new-york-v4/examples/form-rhf-input.tsx",
|
||||
"type": "registry:example"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "form-rhf-select",
|
||||
"type": "registry:example",
|
||||
"dependencies": ["react-hook-form", "@hookform/resolvers", "zod"],
|
||||
"registryDependencies": ["field", "select", "button", "card"],
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/new-york-v4/examples/form-rhf-select.tsx",
|
||||
"type": "registry:example"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "form-rhf-checkbox",
|
||||
"type": "registry:example",
|
||||
"dependencies": ["react-hook-form", "@hookform/resolvers", "zod"],
|
||||
"registryDependencies": ["field", "checkbox", "button", "card"],
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/new-york-v4/examples/form-rhf-checkbox.tsx",
|
||||
"type": "registry:example"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "form-rhf-switch",
|
||||
"type": "registry:example",
|
||||
"dependencies": ["react-hook-form", "@hookform/resolvers", "zod"],
|
||||
"registryDependencies": ["field", "switch", "button", "card"],
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/new-york-v4/examples/form-rhf-switch.tsx",
|
||||
"type": "registry:example"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "form-rhf-textarea",
|
||||
"type": "registry:example",
|
||||
"dependencies": ["react-hook-form", "@hookform/resolvers", "zod"],
|
||||
"registryDependencies": ["field", "textarea", "button", "card"],
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/new-york-v4/examples/form-rhf-textarea.tsx",
|
||||
"type": "registry:example"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "form-rhf-radiogroup",
|
||||
"type": "registry:example",
|
||||
"dependencies": ["react-hook-form", "@hookform/resolvers", "zod"],
|
||||
"registryDependencies": ["field", "radio-group", "button", "card"],
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/new-york-v4/examples/form-rhf-radiogroup.tsx",
|
||||
"type": "registry:example"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "form-rhf-array",
|
||||
"type": "registry:example",
|
||||
"dependencies": ["react-hook-form", "@hookform/resolvers", "zod"],
|
||||
"registryDependencies": [
|
||||
"field",
|
||||
"input",
|
||||
"input-group",
|
||||
"button",
|
||||
"card"
|
||||
],
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/new-york-v4/examples/form-rhf-array.tsx",
|
||||
"type": "registry:example"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "form-rhf-complex",
|
||||
"type": "registry:example",
|
||||
"dependencies": ["react-hook-form", "@hookform/resolvers", "zod"],
|
||||
"registryDependencies": [
|
||||
"field",
|
||||
"button",
|
||||
"card",
|
||||
"checkbox",
|
||||
"radio-group",
|
||||
"select",
|
||||
"switch"
|
||||
],
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/new-york-v4/examples/form-rhf-complex.tsx",
|
||||
"type": "registry:example"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "form-rhf-password",
|
||||
"type": "registry:example",
|
||||
"dependencies": ["react-hook-form", "@hookform/resolvers", "zod"],
|
||||
"registryDependencies": [
|
||||
"field",
|
||||
"input-group",
|
||||
"progress",
|
||||
"button",
|
||||
"card"
|
||||
],
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/new-york-v4/examples/form-rhf-password.tsx",
|
||||
"type": "registry:example"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "form-tanstack-demo",
|
||||
"type": "registry:example",
|
||||
"dependencies": ["@tanstack/react-form", "zod"],
|
||||
"registryDependencies": [
|
||||
"field",
|
||||
"input",
|
||||
"input-group",
|
||||
"button",
|
||||
"card"
|
||||
],
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/new-york-v4/examples/form-tanstack-demo.tsx",
|
||||
"type": "registry:example"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "form-tanstack-input",
|
||||
"type": "registry:example",
|
||||
"dependencies": ["@tanstack/react-form", "zod"],
|
||||
"registryDependencies": ["field", "input", "button", "card"],
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/new-york-v4/examples/form-tanstack-input.tsx",
|
||||
"type": "registry:example"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "form-tanstack-textarea",
|
||||
"type": "registry:example",
|
||||
"dependencies": ["@tanstack/react-form", "zod"],
|
||||
"registryDependencies": ["field", "textarea", "button", "card"],
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/new-york-v4/examples/form-tanstack-textarea.tsx",
|
||||
"type": "registry:example"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "form-tanstack-select",
|
||||
"type": "registry:example",
|
||||
"dependencies": ["@tanstack/react-form", "zod"],
|
||||
"registryDependencies": ["field", "select", "button", "card"],
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/new-york-v4/examples/form-tanstack-select.tsx",
|
||||
"type": "registry:example"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "form-tanstack-checkbox",
|
||||
"type": "registry:example",
|
||||
"dependencies": ["@tanstack/react-form", "zod"],
|
||||
"registryDependencies": ["field", "checkbox", "button", "card"],
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/new-york-v4/examples/form-tanstack-checkbox.tsx",
|
||||
"type": "registry:example"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "form-tanstack-switch",
|
||||
"type": "registry:example",
|
||||
"dependencies": ["@tanstack/react-form", "zod"],
|
||||
"registryDependencies": ["field", "switch", "button", "card"],
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/new-york-v4/examples/form-tanstack-switch.tsx",
|
||||
"type": "registry:example"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "form-tanstack-radiogroup",
|
||||
"type": "registry:example",
|
||||
"dependencies": ["@tanstack/react-form", "zod"],
|
||||
"registryDependencies": ["field", "radio-group", "button", "card"],
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/new-york-v4/examples/form-tanstack-radiogroup.tsx",
|
||||
"type": "registry:example"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "form-tanstack-array",
|
||||
"type": "registry:example",
|
||||
"dependencies": ["@tanstack/react-form", "zod"],
|
||||
"registryDependencies": [
|
||||
"field",
|
||||
"input",
|
||||
"input-group",
|
||||
"button",
|
||||
"card"
|
||||
],
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/new-york-v4/examples/form-tanstack-array.tsx",
|
||||
"type": "registry:example"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "form-tanstack-complex",
|
||||
"type": "registry:example",
|
||||
"dependencies": ["@tanstack/react-form", "zod"],
|
||||
"registryDependencies": [
|
||||
"field",
|
||||
"button",
|
||||
"card",
|
||||
"checkbox",
|
||||
"radio-group",
|
||||
"select",
|
||||
"switch"
|
||||
],
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/new-york-v4/examples/form-tanstack-complex.tsx",
|
||||
"type": "registry:example"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "drawer-dialog",
|
||||
"type": "registry:example",
|
||||
@@ -4571,6 +4710,23 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "dropdown-menu-dialog",
|
||||
"type": "registry:example",
|
||||
"registryDependencies": [
|
||||
"dropdown-menu",
|
||||
"dialog",
|
||||
"button",
|
||||
"input",
|
||||
"label"
|
||||
],
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/new-york-v4/examples/dropdown-menu-dialog.tsx",
|
||||
"type": "registry:example"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "hover-card-demo",
|
||||
"type": "registry:example",
|
||||
@@ -5270,6 +5426,17 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "sonner-types",
|
||||
"type": "registry:example",
|
||||
"registryDependencies": ["sonner"],
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/new-york-v4/examples/sonner-types.tsx",
|
||||
"type": "registry:example"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "spinner-demo",
|
||||
"type": "registry:example",
|
||||
|
||||
@@ -318,10 +318,5 @@
|
||||
"name": "otp-05",
|
||||
"description": "A simple OTP form with social providers.",
|
||||
"categories": ["authentication", "otp"]
|
||||
},
|
||||
{
|
||||
"name": "new-components-01",
|
||||
"description": "New components",
|
||||
"categories": ["components", "showcase"]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -2456,136 +2456,6 @@ export const Index: Record<string, any> = {
|
||||
categories: ["authentication","otp"],
|
||||
meta: undefined,
|
||||
},
|
||||
"new-components-01": {
|
||||
name: "new-components-01",
|
||||
description: "New components",
|
||||
type: "registry:block",
|
||||
registryDependencies: undefined,
|
||||
files: [{
|
||||
path: "registry/new-york-v4/blocks/new-components-01/page.tsx",
|
||||
type: "registry:page",
|
||||
target: "app/page.tsx"
|
||||
},{
|
||||
path: "registry/new-york-v4/blocks/new-components-01/components/ui/field.tsx",
|
||||
type: "registry:ui",
|
||||
target: "components/ui/field.tsx"
|
||||
},{
|
||||
path: "registry/new-york-v4/blocks/new-components-01/components/ui/button-group.tsx",
|
||||
type: "registry:ui",
|
||||
target: "components/ui/button-group.tsx"
|
||||
},{
|
||||
path: "registry/new-york-v4/blocks/new-components-01/components/ui/input-group.tsx",
|
||||
type: "registry:ui",
|
||||
target: "components/ui/input-group.tsx"
|
||||
},{
|
||||
path: "registry/new-york-v4/blocks/new-components-01/components/ui/empty.tsx",
|
||||
type: "registry:ui",
|
||||
target: "components/ui/empty.tsx"
|
||||
},{
|
||||
path: "registry/new-york-v4/blocks/new-components-01/components/ui/item.tsx",
|
||||
type: "registry:ui",
|
||||
target: "components/ui/item.tsx"
|
||||
},{
|
||||
path: "registry/new-york-v4/blocks/new-components-01/components/ui/spinner.tsx",
|
||||
type: "registry:ui",
|
||||
target: "components/ui/spinner.tsx"
|
||||
},{
|
||||
path: "registry/new-york-v4/blocks/new-components-01/components/ui/button.tsx",
|
||||
type: "registry:ui",
|
||||
target: "components/ui/button.tsx"
|
||||
},{
|
||||
path: "registry/new-york-v4/blocks/new-components-01/components/ui/input.tsx",
|
||||
type: "registry:ui",
|
||||
target: "components/ui/input.tsx"
|
||||
},{
|
||||
path: "registry/new-york-v4/blocks/new-components-01/components/ui/tooltip.tsx",
|
||||
type: "registry:ui",
|
||||
target: "components/ui/tooltip.tsx"
|
||||
},{
|
||||
path: "registry/new-york-v4/blocks/new-components-01/components/ui/dropdown-menu.tsx",
|
||||
type: "registry:ui",
|
||||
target: "components/ui/dropdown-menu.tsx"
|
||||
},{
|
||||
path: "registry/new-york-v4/blocks/new-components-01/components/appearance-settings.tsx",
|
||||
type: "registry:component",
|
||||
target: ""
|
||||
},{
|
||||
path: "registry/new-york-v4/blocks/new-components-01/components/button-group-demo.tsx",
|
||||
type: "registry:component",
|
||||
target: ""
|
||||
},{
|
||||
path: "registry/new-york-v4/blocks/new-components-01/components/button-group-input-group.tsx",
|
||||
type: "registry:component",
|
||||
target: ""
|
||||
},{
|
||||
path: "registry/new-york-v4/blocks/new-components-01/components/button-group-nested.tsx",
|
||||
type: "registry:component",
|
||||
target: ""
|
||||
},{
|
||||
path: "registry/new-york-v4/blocks/new-components-01/components/button-group-popover.tsx",
|
||||
type: "registry:component",
|
||||
target: ""
|
||||
},{
|
||||
path: "registry/new-york-v4/blocks/new-components-01/components/empty-avatar-group.tsx",
|
||||
type: "registry:component",
|
||||
target: ""
|
||||
},{
|
||||
path: "registry/new-york-v4/blocks/new-components-01/components/empty-input-group.tsx",
|
||||
type: "registry:component",
|
||||
target: ""
|
||||
},{
|
||||
path: "registry/new-york-v4/blocks/new-components-01/components/field-choice-card.tsx",
|
||||
type: "registry:component",
|
||||
target: ""
|
||||
},{
|
||||
path: "registry/new-york-v4/blocks/new-components-01/components/field-demo.tsx",
|
||||
type: "registry:component",
|
||||
target: ""
|
||||
},{
|
||||
path: "registry/new-york-v4/blocks/new-components-01/components/field-slider.tsx",
|
||||
type: "registry:component",
|
||||
target: ""
|
||||
},{
|
||||
path: "registry/new-york-v4/blocks/new-components-01/components/input-group-button.tsx",
|
||||
type: "registry:component",
|
||||
target: ""
|
||||
},{
|
||||
path: "registry/new-york-v4/blocks/new-components-01/components/input-group-demo.tsx",
|
||||
type: "registry:component",
|
||||
target: ""
|
||||
},{
|
||||
path: "registry/new-york-v4/blocks/new-components-01/components/input-group-textarea.tsx",
|
||||
type: "registry:component",
|
||||
target: ""
|
||||
},{
|
||||
path: "registry/new-york-v4/blocks/new-components-01/components/item-avatar.tsx",
|
||||
type: "registry:component",
|
||||
target: ""
|
||||
},{
|
||||
path: "registry/new-york-v4/blocks/new-components-01/components/item-demo.tsx",
|
||||
type: "registry:component",
|
||||
target: ""
|
||||
},{
|
||||
path: "registry/new-york-v4/blocks/new-components-01/components/notion-prompt-form.tsx",
|
||||
type: "registry:component",
|
||||
target: ""
|
||||
},{
|
||||
path: "registry/new-york-v4/blocks/new-components-01/components/spinner-badge.tsx",
|
||||
type: "registry:component",
|
||||
target: ""
|
||||
},{
|
||||
path: "registry/new-york-v4/blocks/new-components-01/components/spinner-empty.tsx",
|
||||
type: "registry:component",
|
||||
target: ""
|
||||
}],
|
||||
component: React.lazy(async () => {
|
||||
const mod = await import("@/registry/new-york-v4/blocks/new-components-01/page.tsx")
|
||||
const exportName = Object.keys(mod).find(key => typeof mod[key] === 'function' || typeof mod[key] === 'object') || item.name
|
||||
return { default: mod.default || mod[exportName] }
|
||||
}),
|
||||
categories: ["components","showcase"],
|
||||
meta: undefined,
|
||||
},
|
||||
"chart-area-axes": {
|
||||
name: "chart-area-axes",
|
||||
description: "",
|
||||
@@ -5520,6 +5390,348 @@ export const Index: Record<string, any> = {
|
||||
categories: undefined,
|
||||
meta: undefined,
|
||||
},
|
||||
"form-rhf-demo": {
|
||||
name: "form-rhf-demo",
|
||||
description: "",
|
||||
type: "registry:example",
|
||||
registryDependencies: ["field","input","input-group","button","card"],
|
||||
files: [{
|
||||
path: "registry/new-york-v4/examples/form-rhf-demo.tsx",
|
||||
type: "registry:example",
|
||||
target: ""
|
||||
}],
|
||||
component: React.lazy(async () => {
|
||||
const mod = await import("@/registry/new-york-v4/examples/form-rhf-demo.tsx")
|
||||
const exportName = Object.keys(mod).find(key => typeof mod[key] === 'function' || typeof mod[key] === 'object') || item.name
|
||||
return { default: mod.default || mod[exportName] }
|
||||
}),
|
||||
categories: undefined,
|
||||
meta: undefined,
|
||||
},
|
||||
"form-rhf-input": {
|
||||
name: "form-rhf-input",
|
||||
description: "",
|
||||
type: "registry:example",
|
||||
registryDependencies: ["field","input","button","card"],
|
||||
files: [{
|
||||
path: "registry/new-york-v4/examples/form-rhf-input.tsx",
|
||||
type: "registry:example",
|
||||
target: ""
|
||||
}],
|
||||
component: React.lazy(async () => {
|
||||
const mod = await import("@/registry/new-york-v4/examples/form-rhf-input.tsx")
|
||||
const exportName = Object.keys(mod).find(key => typeof mod[key] === 'function' || typeof mod[key] === 'object') || item.name
|
||||
return { default: mod.default || mod[exportName] }
|
||||
}),
|
||||
categories: undefined,
|
||||
meta: undefined,
|
||||
},
|
||||
"form-rhf-select": {
|
||||
name: "form-rhf-select",
|
||||
description: "",
|
||||
type: "registry:example",
|
||||
registryDependencies: ["field","select","button","card"],
|
||||
files: [{
|
||||
path: "registry/new-york-v4/examples/form-rhf-select.tsx",
|
||||
type: "registry:example",
|
||||
target: ""
|
||||
}],
|
||||
component: React.lazy(async () => {
|
||||
const mod = await import("@/registry/new-york-v4/examples/form-rhf-select.tsx")
|
||||
const exportName = Object.keys(mod).find(key => typeof mod[key] === 'function' || typeof mod[key] === 'object') || item.name
|
||||
return { default: mod.default || mod[exportName] }
|
||||
}),
|
||||
categories: undefined,
|
||||
meta: undefined,
|
||||
},
|
||||
"form-rhf-checkbox": {
|
||||
name: "form-rhf-checkbox",
|
||||
description: "",
|
||||
type: "registry:example",
|
||||
registryDependencies: ["field","checkbox","button","card"],
|
||||
files: [{
|
||||
path: "registry/new-york-v4/examples/form-rhf-checkbox.tsx",
|
||||
type: "registry:example",
|
||||
target: ""
|
||||
}],
|
||||
component: React.lazy(async () => {
|
||||
const mod = await import("@/registry/new-york-v4/examples/form-rhf-checkbox.tsx")
|
||||
const exportName = Object.keys(mod).find(key => typeof mod[key] === 'function' || typeof mod[key] === 'object') || item.name
|
||||
return { default: mod.default || mod[exportName] }
|
||||
}),
|
||||
categories: undefined,
|
||||
meta: undefined,
|
||||
},
|
||||
"form-rhf-switch": {
|
||||
name: "form-rhf-switch",
|
||||
description: "",
|
||||
type: "registry:example",
|
||||
registryDependencies: ["field","switch","button","card"],
|
||||
files: [{
|
||||
path: "registry/new-york-v4/examples/form-rhf-switch.tsx",
|
||||
type: "registry:example",
|
||||
target: ""
|
||||
}],
|
||||
component: React.lazy(async () => {
|
||||
const mod = await import("@/registry/new-york-v4/examples/form-rhf-switch.tsx")
|
||||
const exportName = Object.keys(mod).find(key => typeof mod[key] === 'function' || typeof mod[key] === 'object') || item.name
|
||||
return { default: mod.default || mod[exportName] }
|
||||
}),
|
||||
categories: undefined,
|
||||
meta: undefined,
|
||||
},
|
||||
"form-rhf-textarea": {
|
||||
name: "form-rhf-textarea",
|
||||
description: "",
|
||||
type: "registry:example",
|
||||
registryDependencies: ["field","textarea","button","card"],
|
||||
files: [{
|
||||
path: "registry/new-york-v4/examples/form-rhf-textarea.tsx",
|
||||
type: "registry:example",
|
||||
target: ""
|
||||
}],
|
||||
component: React.lazy(async () => {
|
||||
const mod = await import("@/registry/new-york-v4/examples/form-rhf-textarea.tsx")
|
||||
const exportName = Object.keys(mod).find(key => typeof mod[key] === 'function' || typeof mod[key] === 'object') || item.name
|
||||
return { default: mod.default || mod[exportName] }
|
||||
}),
|
||||
categories: undefined,
|
||||
meta: undefined,
|
||||
},
|
||||
"form-rhf-radiogroup": {
|
||||
name: "form-rhf-radiogroup",
|
||||
description: "",
|
||||
type: "registry:example",
|
||||
registryDependencies: ["field","radio-group","button","card"],
|
||||
files: [{
|
||||
path: "registry/new-york-v4/examples/form-rhf-radiogroup.tsx",
|
||||
type: "registry:example",
|
||||
target: ""
|
||||
}],
|
||||
component: React.lazy(async () => {
|
||||
const mod = await import("@/registry/new-york-v4/examples/form-rhf-radiogroup.tsx")
|
||||
const exportName = Object.keys(mod).find(key => typeof mod[key] === 'function' || typeof mod[key] === 'object') || item.name
|
||||
return { default: mod.default || mod[exportName] }
|
||||
}),
|
||||
categories: undefined,
|
||||
meta: undefined,
|
||||
},
|
||||
"form-rhf-array": {
|
||||
name: "form-rhf-array",
|
||||
description: "",
|
||||
type: "registry:example",
|
||||
registryDependencies: ["field","input","input-group","button","card"],
|
||||
files: [{
|
||||
path: "registry/new-york-v4/examples/form-rhf-array.tsx",
|
||||
type: "registry:example",
|
||||
target: ""
|
||||
}],
|
||||
component: React.lazy(async () => {
|
||||
const mod = await import("@/registry/new-york-v4/examples/form-rhf-array.tsx")
|
||||
const exportName = Object.keys(mod).find(key => typeof mod[key] === 'function' || typeof mod[key] === 'object') || item.name
|
||||
return { default: mod.default || mod[exportName] }
|
||||
}),
|
||||
categories: undefined,
|
||||
meta: undefined,
|
||||
},
|
||||
"form-rhf-complex": {
|
||||
name: "form-rhf-complex",
|
||||
description: "",
|
||||
type: "registry:example",
|
||||
registryDependencies: ["field","button","card","checkbox","radio-group","select","switch"],
|
||||
files: [{
|
||||
path: "registry/new-york-v4/examples/form-rhf-complex.tsx",
|
||||
type: "registry:example",
|
||||
target: ""
|
||||
}],
|
||||
component: React.lazy(async () => {
|
||||
const mod = await import("@/registry/new-york-v4/examples/form-rhf-complex.tsx")
|
||||
const exportName = Object.keys(mod).find(key => typeof mod[key] === 'function' || typeof mod[key] === 'object') || item.name
|
||||
return { default: mod.default || mod[exportName] }
|
||||
}),
|
||||
categories: undefined,
|
||||
meta: undefined,
|
||||
},
|
||||
"form-rhf-password": {
|
||||
name: "form-rhf-password",
|
||||
description: "",
|
||||
type: "registry:example",
|
||||
registryDependencies: ["field","input-group","progress","button","card"],
|
||||
files: [{
|
||||
path: "registry/new-york-v4/examples/form-rhf-password.tsx",
|
||||
type: "registry:example",
|
||||
target: ""
|
||||
}],
|
||||
component: React.lazy(async () => {
|
||||
const mod = await import("@/registry/new-york-v4/examples/form-rhf-password.tsx")
|
||||
const exportName = Object.keys(mod).find(key => typeof mod[key] === 'function' || typeof mod[key] === 'object') || item.name
|
||||
return { default: mod.default || mod[exportName] }
|
||||
}),
|
||||
categories: undefined,
|
||||
meta: undefined,
|
||||
},
|
||||
"form-tanstack-demo": {
|
||||
name: "form-tanstack-demo",
|
||||
description: "",
|
||||
type: "registry:example",
|
||||
registryDependencies: ["field","input","input-group","button","card"],
|
||||
files: [{
|
||||
path: "registry/new-york-v4/examples/form-tanstack-demo.tsx",
|
||||
type: "registry:example",
|
||||
target: ""
|
||||
}],
|
||||
component: React.lazy(async () => {
|
||||
const mod = await import("@/registry/new-york-v4/examples/form-tanstack-demo.tsx")
|
||||
const exportName = Object.keys(mod).find(key => typeof mod[key] === 'function' || typeof mod[key] === 'object') || item.name
|
||||
return { default: mod.default || mod[exportName] }
|
||||
}),
|
||||
categories: undefined,
|
||||
meta: undefined,
|
||||
},
|
||||
"form-tanstack-input": {
|
||||
name: "form-tanstack-input",
|
||||
description: "",
|
||||
type: "registry:example",
|
||||
registryDependencies: ["field","input","button","card"],
|
||||
files: [{
|
||||
path: "registry/new-york-v4/examples/form-tanstack-input.tsx",
|
||||
type: "registry:example",
|
||||
target: ""
|
||||
}],
|
||||
component: React.lazy(async () => {
|
||||
const mod = await import("@/registry/new-york-v4/examples/form-tanstack-input.tsx")
|
||||
const exportName = Object.keys(mod).find(key => typeof mod[key] === 'function' || typeof mod[key] === 'object') || item.name
|
||||
return { default: mod.default || mod[exportName] }
|
||||
}),
|
||||
categories: undefined,
|
||||
meta: undefined,
|
||||
},
|
||||
"form-tanstack-textarea": {
|
||||
name: "form-tanstack-textarea",
|
||||
description: "",
|
||||
type: "registry:example",
|
||||
registryDependencies: ["field","textarea","button","card"],
|
||||
files: [{
|
||||
path: "registry/new-york-v4/examples/form-tanstack-textarea.tsx",
|
||||
type: "registry:example",
|
||||
target: ""
|
||||
}],
|
||||
component: React.lazy(async () => {
|
||||
const mod = await import("@/registry/new-york-v4/examples/form-tanstack-textarea.tsx")
|
||||
const exportName = Object.keys(mod).find(key => typeof mod[key] === 'function' || typeof mod[key] === 'object') || item.name
|
||||
return { default: mod.default || mod[exportName] }
|
||||
}),
|
||||
categories: undefined,
|
||||
meta: undefined,
|
||||
},
|
||||
"form-tanstack-select": {
|
||||
name: "form-tanstack-select",
|
||||
description: "",
|
||||
type: "registry:example",
|
||||
registryDependencies: ["field","select","button","card"],
|
||||
files: [{
|
||||
path: "registry/new-york-v4/examples/form-tanstack-select.tsx",
|
||||
type: "registry:example",
|
||||
target: ""
|
||||
}],
|
||||
component: React.lazy(async () => {
|
||||
const mod = await import("@/registry/new-york-v4/examples/form-tanstack-select.tsx")
|
||||
const exportName = Object.keys(mod).find(key => typeof mod[key] === 'function' || typeof mod[key] === 'object') || item.name
|
||||
return { default: mod.default || mod[exportName] }
|
||||
}),
|
||||
categories: undefined,
|
||||
meta: undefined,
|
||||
},
|
||||
"form-tanstack-checkbox": {
|
||||
name: "form-tanstack-checkbox",
|
||||
description: "",
|
||||
type: "registry:example",
|
||||
registryDependencies: ["field","checkbox","button","card"],
|
||||
files: [{
|
||||
path: "registry/new-york-v4/examples/form-tanstack-checkbox.tsx",
|
||||
type: "registry:example",
|
||||
target: ""
|
||||
}],
|
||||
component: React.lazy(async () => {
|
||||
const mod = await import("@/registry/new-york-v4/examples/form-tanstack-checkbox.tsx")
|
||||
const exportName = Object.keys(mod).find(key => typeof mod[key] === 'function' || typeof mod[key] === 'object') || item.name
|
||||
return { default: mod.default || mod[exportName] }
|
||||
}),
|
||||
categories: undefined,
|
||||
meta: undefined,
|
||||
},
|
||||
"form-tanstack-switch": {
|
||||
name: "form-tanstack-switch",
|
||||
description: "",
|
||||
type: "registry:example",
|
||||
registryDependencies: ["field","switch","button","card"],
|
||||
files: [{
|
||||
path: "registry/new-york-v4/examples/form-tanstack-switch.tsx",
|
||||
type: "registry:example",
|
||||
target: ""
|
||||
}],
|
||||
component: React.lazy(async () => {
|
||||
const mod = await import("@/registry/new-york-v4/examples/form-tanstack-switch.tsx")
|
||||
const exportName = Object.keys(mod).find(key => typeof mod[key] === 'function' || typeof mod[key] === 'object') || item.name
|
||||
return { default: mod.default || mod[exportName] }
|
||||
}),
|
||||
categories: undefined,
|
||||
meta: undefined,
|
||||
},
|
||||
"form-tanstack-radiogroup": {
|
||||
name: "form-tanstack-radiogroup",
|
||||
description: "",
|
||||
type: "registry:example",
|
||||
registryDependencies: ["field","radio-group","button","card"],
|
||||
files: [{
|
||||
path: "registry/new-york-v4/examples/form-tanstack-radiogroup.tsx",
|
||||
type: "registry:example",
|
||||
target: ""
|
||||
}],
|
||||
component: React.lazy(async () => {
|
||||
const mod = await import("@/registry/new-york-v4/examples/form-tanstack-radiogroup.tsx")
|
||||
const exportName = Object.keys(mod).find(key => typeof mod[key] === 'function' || typeof mod[key] === 'object') || item.name
|
||||
return { default: mod.default || mod[exportName] }
|
||||
}),
|
||||
categories: undefined,
|
||||
meta: undefined,
|
||||
},
|
||||
"form-tanstack-array": {
|
||||
name: "form-tanstack-array",
|
||||
description: "",
|
||||
type: "registry:example",
|
||||
registryDependencies: ["field","input","input-group","button","card"],
|
||||
files: [{
|
||||
path: "registry/new-york-v4/examples/form-tanstack-array.tsx",
|
||||
type: "registry:example",
|
||||
target: ""
|
||||
}],
|
||||
component: React.lazy(async () => {
|
||||
const mod = await import("@/registry/new-york-v4/examples/form-tanstack-array.tsx")
|
||||
const exportName = Object.keys(mod).find(key => typeof mod[key] === 'function' || typeof mod[key] === 'object') || item.name
|
||||
return { default: mod.default || mod[exportName] }
|
||||
}),
|
||||
categories: undefined,
|
||||
meta: undefined,
|
||||
},
|
||||
"form-tanstack-complex": {
|
||||
name: "form-tanstack-complex",
|
||||
description: "",
|
||||
type: "registry:example",
|
||||
registryDependencies: ["field","button","card","checkbox","radio-group","select","switch"],
|
||||
files: [{
|
||||
path: "registry/new-york-v4/examples/form-tanstack-complex.tsx",
|
||||
type: "registry:example",
|
||||
target: ""
|
||||
}],
|
||||
component: React.lazy(async () => {
|
||||
const mod = await import("@/registry/new-york-v4/examples/form-tanstack-complex.tsx")
|
||||
const exportName = Object.keys(mod).find(key => typeof mod[key] === 'function' || typeof mod[key] === 'object') || item.name
|
||||
return { default: mod.default || mod[exportName] }
|
||||
}),
|
||||
categories: undefined,
|
||||
meta: undefined,
|
||||
},
|
||||
"drawer-dialog": {
|
||||
name: "drawer-dialog",
|
||||
description: "",
|
||||
@@ -5592,6 +5804,24 @@ export const Index: Record<string, any> = {
|
||||
categories: undefined,
|
||||
meta: undefined,
|
||||
},
|
||||
"dropdown-menu-dialog": {
|
||||
name: "dropdown-menu-dialog",
|
||||
description: "",
|
||||
type: "registry:example",
|
||||
registryDependencies: ["dropdown-menu","dialog","button","input","label"],
|
||||
files: [{
|
||||
path: "registry/new-york-v4/examples/dropdown-menu-dialog.tsx",
|
||||
type: "registry:example",
|
||||
target: ""
|
||||
}],
|
||||
component: React.lazy(async () => {
|
||||
const mod = await import("@/registry/new-york-v4/examples/dropdown-menu-dialog.tsx")
|
||||
const exportName = Object.keys(mod).find(key => typeof mod[key] === 'function' || typeof mod[key] === 'object') || item.name
|
||||
return { default: mod.default || mod[exportName] }
|
||||
}),
|
||||
categories: undefined,
|
||||
meta: undefined,
|
||||
},
|
||||
"hover-card-demo": {
|
||||
name: "hover-card-demo",
|
||||
description: "",
|
||||
@@ -6726,6 +6956,24 @@ export const Index: Record<string, any> = {
|
||||
categories: undefined,
|
||||
meta: undefined,
|
||||
},
|
||||
"sonner-types": {
|
||||
name: "sonner-types",
|
||||
description: "",
|
||||
type: "registry:example",
|
||||
registryDependencies: ["sonner"],
|
||||
files: [{
|
||||
path: "registry/new-york-v4/examples/sonner-types.tsx",
|
||||
type: "registry:example",
|
||||
target: ""
|
||||
}],
|
||||
component: React.lazy(async () => {
|
||||
const mod = await import("@/registry/new-york-v4/examples/sonner-types.tsx")
|
||||
const exportName = Object.keys(mod).find(key => typeof mod[key] === 'function' || typeof mod[key] === 'object') || item.name
|
||||
return { default: mod.default || mod[exportName] }
|
||||
}),
|
||||
categories: undefined,
|
||||
meta: undefined,
|
||||
},
|
||||
"spinner-demo": {
|
||||
name: "spinner-demo",
|
||||
description: "",
|
||||
|
||||
@@ -1,144 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import { IconMinus, IconPlus } from "@tabler/icons-react"
|
||||
import { CheckIcon } from "lucide-react"
|
||||
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import { ButtonGroup } from "@/registry/new-york-v4/ui/button-group"
|
||||
import {
|
||||
Field,
|
||||
FieldContent,
|
||||
FieldDescription,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldLegend,
|
||||
FieldSeparator,
|
||||
FieldSet,
|
||||
FieldTitle,
|
||||
} from "@/registry/new-york-v4/ui/field"
|
||||
import { Input } from "@/registry/new-york-v4/ui/input"
|
||||
import { 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() {
|
||||
return (
|
||||
<FieldSet>
|
||||
<FieldGroup>
|
||||
<FieldSet>
|
||||
<FieldLegend>Compute Environment</FieldLegend>
|
||||
<FieldDescription>
|
||||
Select the compute environment for your cluster.
|
||||
</FieldDescription>
|
||||
<RadioGroup defaultValue="kubernetes">
|
||||
<FieldLabel htmlFor="kubernetes-r2h">
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldTitle>Kubernetes</FieldTitle>
|
||||
<FieldDescription>
|
||||
Run GPU workloads on a K8s configured cluster. This is the
|
||||
default.
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<RadioGroupItem value="kubernetes" id="kubernetes-r2h" />
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
<FieldLabel htmlFor="vm-z4k">
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldTitle>Virtual Machine</FieldTitle>
|
||||
<FieldDescription>
|
||||
Access a VM configured cluster to run workloads. (Coming
|
||||
soon)
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<RadioGroupItem value="vm" id="vm-z4k" />
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
</RadioGroup>
|
||||
</FieldSet>
|
||||
<FieldSeparator />
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldTitle>Accent</FieldTitle>
|
||||
<FieldDescription>Select the accent color to use.</FieldDescription>
|
||||
</FieldContent>
|
||||
<FieldSet aria-label="Accent">
|
||||
<RadioGroup className="flex flex-wrap gap-2" defaultValue="blue">
|
||||
{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>
|
||||
<FieldDescription>You can add more later.</FieldDescription>
|
||||
</FieldContent>
|
||||
<ButtonGroup>
|
||||
<Input
|
||||
id="number-of-gpus-f6l"
|
||||
placeholder="8"
|
||||
size={3}
|
||||
className="h-8 !w-14 font-mono"
|
||||
maxLength={3}
|
||||
/>
|
||||
<Button variant="outline" size="icon-sm" type="button">
|
||||
<IconMinus />
|
||||
</Button>
|
||||
<Button variant="outline" size="icon-sm" type="button">
|
||||
<IconPlus />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</Field>
|
||||
<FieldSeparator />
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor="tinting">Wallpaper Tinting</FieldLabel>
|
||||
<FieldDescription>
|
||||
Allow the wallpaper to be tinted.
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<Switch id="tinting" defaultChecked />
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</FieldSet>
|
||||
)
|
||||
}
|
||||
@@ -1,113 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import {
|
||||
ArchiveIcon,
|
||||
ArrowLeftIcon,
|
||||
CalendarPlusIcon,
|
||||
ClockIcon,
|
||||
ListFilterIcon,
|
||||
MailCheckIcon,
|
||||
MoreHorizontalIcon,
|
||||
TagIcon,
|
||||
Trash2Icon,
|
||||
} from "lucide-react"
|
||||
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import { ButtonGroup } from "@/registry/new-york-v4/ui/button-group"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/registry/new-york-v4/ui/dropdown-menu"
|
||||
|
||||
export function ButtonGroupDemo() {
|
||||
const [label, setLabel] = React.useState("personal")
|
||||
|
||||
return (
|
||||
<ButtonGroup>
|
||||
<ButtonGroup className="hidden sm:flex">
|
||||
<Button variant="outline" size="icon" aria-label="Go Back">
|
||||
<ArrowLeftIcon />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup>
|
||||
<Button variant="outline">Archive</Button>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup>
|
||||
<Button variant="outline">Snooze</Button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" size="icon" aria-label="More Options">
|
||||
<MoreHorizontalIcon />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-52">
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>
|
||||
<MailCheckIcon />
|
||||
Mark as Read
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<ArchiveIcon />
|
||||
Archive
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>
|
||||
<ClockIcon />
|
||||
Snooze
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<CalendarPlusIcon />
|
||||
Add to Calendar
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<ListFilterIcon />
|
||||
Add to List
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger>
|
||||
<TagIcon />
|
||||
Label As...
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuSubContent>
|
||||
<DropdownMenuRadioGroup
|
||||
value={label}
|
||||
onValueChange={setLabel}
|
||||
>
|
||||
<DropdownMenuRadioItem value="personal">
|
||||
Personal
|
||||
</DropdownMenuRadioItem>
|
||||
<DropdownMenuRadioItem value="work">
|
||||
Work
|
||||
</DropdownMenuRadioItem>
|
||||
<DropdownMenuRadioItem value="other">
|
||||
Other
|
||||
</DropdownMenuRadioItem>
|
||||
</DropdownMenuRadioGroup>
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuSub>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem variant="destructive">
|
||||
<Trash2Icon />
|
||||
Trash
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</ButtonGroup>
|
||||
</ButtonGroup>
|
||||
)
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { AudioLinesIcon, PlusIcon } from "lucide-react"
|
||||
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import { ButtonGroup } from "@/registry/new-york-v4/ui/button-group"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupInput,
|
||||
} from "@/registry/new-york-v4/ui/input-group"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/registry/new-york-v4/ui/tooltip"
|
||||
|
||||
export function ButtonGroupInputGroup() {
|
||||
const [voiceEnabled, setVoiceEnabled] = React.useState(false)
|
||||
return (
|
||||
<ButtonGroup className="[--radius:9999rem]">
|
||||
<ButtonGroup>
|
||||
<Button variant="outline" size="icon">
|
||||
<PlusIcon />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup className="flex-1">
|
||||
<InputGroup>
|
||||
<InputGroupInput
|
||||
placeholder={
|
||||
voiceEnabled ? "Record and send audio..." : "Send a message..."
|
||||
}
|
||||
disabled={voiceEnabled}
|
||||
/>
|
||||
<InputGroupAddon align="inline-end">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<InputGroupButton
|
||||
onClick={() => setVoiceEnabled(!voiceEnabled)}
|
||||
data-active={voiceEnabled}
|
||||
className="data-[active=true]:bg-primary data-[active=true]:text-primary-foreground"
|
||||
aria-pressed={voiceEnabled}
|
||||
size="icon-xs"
|
||||
>
|
||||
<AudioLinesIcon />
|
||||
</InputGroupButton>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Voice Mode</TooltipContent>
|
||||
</Tooltip>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</ButtonGroup>
|
||||
</ButtonGroup>
|
||||
)
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import { ArrowLeftIcon, ArrowRightIcon } from "lucide-react"
|
||||
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import { ButtonGroup } from "@/registry/new-york-v4/ui/button-group"
|
||||
|
||||
export function ButtonGroupNested() {
|
||||
return (
|
||||
<ButtonGroup>
|
||||
<ButtonGroup>
|
||||
<Button variant="outline" size="sm">
|
||||
1
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
2
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
3
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup>
|
||||
<Button variant="outline" size="icon-sm" aria-label="Previous">
|
||||
<ArrowLeftIcon />
|
||||
</Button>
|
||||
<Button variant="outline" size="icon-sm" aria-label="Next">
|
||||
<ArrowRightIcon />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</ButtonGroup>
|
||||
)
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
import { BotIcon, ChevronDownIcon } from "lucide-react"
|
||||
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import { ButtonGroup } from "@/registry/new-york-v4/ui/button-group"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/registry/new-york-v4/ui/popover"
|
||||
import { Separator } from "@/registry/new-york-v4/ui/separator"
|
||||
import { Textarea } from "@/registry/new-york-v4/ui/textarea"
|
||||
|
||||
export function ButtonGroupPopover() {
|
||||
return (
|
||||
<ButtonGroup>
|
||||
<Button variant="outline" size="sm">
|
||||
<BotIcon /> Copilot
|
||||
</Button>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline" aria-label="Open Popover" size="icon-sm">
|
||||
<ChevronDownIcon />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent align="end" className="rounded-xl p-0 text-sm">
|
||||
<div className="px-4 py-3">
|
||||
<div className="text-sm font-medium">Agent Tasks</div>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="p-4 text-sm *:[p:not(:last-child)]:mb-2">
|
||||
<Textarea
|
||||
placeholder="Describe your task in natural language."
|
||||
className="mb-4 resize-none"
|
||||
/>
|
||||
<p className="font-medium">Start a new task with Copilot</p>
|
||||
<p className="text-muted-foreground">
|
||||
Describe your task in natural language. Copilot will work in the
|
||||
background and open a pull request for your review.
|
||||
</p>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</ButtonGroup>
|
||||
)
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
import { PlusIcon } from "lucide-react"
|
||||
|
||||
import {
|
||||
Avatar,
|
||||
AvatarFallback,
|
||||
AvatarImage,
|
||||
} from "@/registry/new-york-v4/ui/avatar"
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import {
|
||||
Empty,
|
||||
EmptyContent,
|
||||
EmptyDescription,
|
||||
EmptyHeader,
|
||||
EmptyMedia,
|
||||
EmptyTitle,
|
||||
} from "@/registry/new-york-v4/ui/empty"
|
||||
|
||||
export function EmptyAvatarGroup() {
|
||||
return (
|
||||
<Empty>
|
||||
<EmptyHeader>
|
||||
<EmptyMedia>
|
||||
<div className="*:data-[slot=avatar]:ring-background flex -space-x-2 *:data-[slot=avatar]:size-12 *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:grayscale">
|
||||
<Avatar>
|
||||
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
|
||||
<AvatarFallback>CN</AvatarFallback>
|
||||
</Avatar>
|
||||
<Avatar>
|
||||
<AvatarImage
|
||||
src="https://github.com/maxleiter.png"
|
||||
alt="@maxleiter"
|
||||
/>
|
||||
<AvatarFallback>LR</AvatarFallback>
|
||||
</Avatar>
|
||||
<Avatar>
|
||||
<AvatarImage
|
||||
src="https://github.com/evilrabbit.png"
|
||||
alt="@evilrabbit"
|
||||
/>
|
||||
<AvatarFallback>ER</AvatarFallback>
|
||||
</Avatar>
|
||||
</div>
|
||||
</EmptyMedia>
|
||||
<EmptyTitle>No Team Members</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
Invite your team to collaborate on this project.
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
<EmptyContent>
|
||||
<Button size="sm">
|
||||
<PlusIcon />
|
||||
Invite Members
|
||||
</Button>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
)
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
import { SearchIcon } from "lucide-react"
|
||||
|
||||
import {
|
||||
Empty,
|
||||
EmptyContent,
|
||||
EmptyDescription,
|
||||
EmptyHeader,
|
||||
EmptyTitle,
|
||||
} from "@/registry/new-york-v4/ui/empty"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupInput,
|
||||
} from "@/registry/new-york-v4/ui/input-group"
|
||||
import { Kbd } from "@/registry/new-york-v4/ui/kbd"
|
||||
|
||||
export function EmptyInputGroup() {
|
||||
return (
|
||||
<Empty>
|
||||
<EmptyHeader>
|
||||
<EmptyTitle>404 - Not Found</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
The page you're looking for doesn't exist. Try searching for
|
||||
what you need below.
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
<EmptyContent>
|
||||
<InputGroup className="w-3/4">
|
||||
<InputGroupInput placeholder="Try searching for pages..." />
|
||||
<InputGroupAddon>
|
||||
<SearchIcon />
|
||||
</InputGroupAddon>
|
||||
<InputGroupAddon align="inline-end">
|
||||
<Kbd>/</Kbd>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<EmptyDescription>
|
||||
Need help? <a href="#">Contact support</a>
|
||||
</EmptyDescription>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
)
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
import {
|
||||
Field,
|
||||
FieldContent,
|
||||
FieldDescription,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldSet,
|
||||
FieldTitle,
|
||||
} from "@/registry/new-york-v4/ui/field"
|
||||
import {
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
} from "@/registry/new-york-v4/ui/radio-group"
|
||||
|
||||
export function FieldChoiceCard() {
|
||||
return (
|
||||
<div className="w-full max-w-md">
|
||||
<FieldGroup>
|
||||
<FieldSet>
|
||||
<FieldLabel htmlFor="compute-environment-p8w">
|
||||
Compute Environment
|
||||
</FieldLabel>
|
||||
<FieldDescription>
|
||||
Select the compute environment for your cluster.
|
||||
</FieldDescription>
|
||||
<RadioGroup defaultValue="kubernetes">
|
||||
<FieldLabel htmlFor="kubernetes-r2h">
|
||||
<Field orientation="horizontal">
|
||||
<RadioGroupItem value="kubernetes" id="kubernetes-r2h" />
|
||||
<FieldContent>
|
||||
<FieldTitle>Kubernetes</FieldTitle>
|
||||
<FieldDescription>
|
||||
Run GPU workloads on a K8s configured cluster.
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
<FieldLabel htmlFor="vm-z4k">
|
||||
<Field orientation="horizontal">
|
||||
<RadioGroupItem value="vm" id="vm-z4k" />
|
||||
<FieldContent>
|
||||
<FieldTitle>Virtual Machine</FieldTitle>
|
||||
<FieldDescription>
|
||||
Access a VM configured cluster to run workloads.
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
</RadioGroup>
|
||||
</FieldSet>
|
||||
</FieldGroup>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,151 +0,0 @@
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import { Checkbox } from "@/registry/new-york-v4/ui/checkbox"
|
||||
import {
|
||||
Field,
|
||||
FieldDescription,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldLegend,
|
||||
FieldSeparator,
|
||||
FieldSet,
|
||||
} from "@/registry/new-york-v4/ui/field"
|
||||
import { Input } from "@/registry/new-york-v4/ui/input"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/registry/new-york-v4/ui/select"
|
||||
import { Textarea } from "@/registry/new-york-v4/ui/textarea"
|
||||
|
||||
export function FieldDemo() {
|
||||
return (
|
||||
<div className="w-full max-w-md">
|
||||
<form>
|
||||
<FieldGroup>
|
||||
<FieldSet>
|
||||
<FieldLegend>Payment Method</FieldLegend>
|
||||
<FieldDescription>
|
||||
All transactions are secure and encrypted
|
||||
</FieldDescription>
|
||||
<FieldGroup>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="checkout-7j9-card-name-43j">
|
||||
Name on Card
|
||||
</FieldLabel>
|
||||
<Input
|
||||
id="checkout-7j9-card-name-43j"
|
||||
placeholder="John Doe"
|
||||
required
|
||||
/>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="checkout-7j9-card-number-uw1">
|
||||
Card Number
|
||||
</FieldLabel>
|
||||
<Input
|
||||
id="checkout-7j9-card-number-uw1"
|
||||
placeholder="1234 5678 9012 3456"
|
||||
required
|
||||
/>
|
||||
<FieldDescription>
|
||||
Enter your 16-digit card number
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<Field>
|
||||
<FieldLabel htmlFor="checkout-7j9-exp-month-ts6">
|
||||
Month
|
||||
</FieldLabel>
|
||||
<Select defaultValue="">
|
||||
<SelectTrigger id="checkout-7j9-exp-month-ts6">
|
||||
<SelectValue placeholder="MM" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="01">01</SelectItem>
|
||||
<SelectItem value="02">02</SelectItem>
|
||||
<SelectItem value="03">03</SelectItem>
|
||||
<SelectItem value="04">04</SelectItem>
|
||||
<SelectItem value="05">05</SelectItem>
|
||||
<SelectItem value="06">06</SelectItem>
|
||||
<SelectItem value="07">07</SelectItem>
|
||||
<SelectItem value="08">08</SelectItem>
|
||||
<SelectItem value="09">09</SelectItem>
|
||||
<SelectItem value="10">10</SelectItem>
|
||||
<SelectItem value="11">11</SelectItem>
|
||||
<SelectItem value="12">12</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="checkout-7j9-exp-year-f59">
|
||||
Year
|
||||
</FieldLabel>
|
||||
<Select defaultValue="">
|
||||
<SelectTrigger id="checkout-7j9-exp-year-f59">
|
||||
<SelectValue placeholder="YYYY" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="2024">2024</SelectItem>
|
||||
<SelectItem value="2025">2025</SelectItem>
|
||||
<SelectItem value="2026">2026</SelectItem>
|
||||
<SelectItem value="2027">2027</SelectItem>
|
||||
<SelectItem value="2028">2028</SelectItem>
|
||||
<SelectItem value="2029">2029</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="checkout-7j9-cvv">CVV</FieldLabel>
|
||||
<Input id="checkout-7j9-cvv" placeholder="123" required />
|
||||
</Field>
|
||||
</div>
|
||||
</FieldGroup>
|
||||
</FieldSet>
|
||||
<FieldSeparator />
|
||||
<FieldSet>
|
||||
<FieldLegend>Billing Address</FieldLegend>
|
||||
<FieldDescription>
|
||||
The billing address associated with your payment method
|
||||
</FieldDescription>
|
||||
<FieldGroup>
|
||||
<Field orientation="horizontal">
|
||||
<Checkbox
|
||||
id="checkout-7j9-same-as-shipping-wgm"
|
||||
defaultChecked
|
||||
/>
|
||||
<FieldLabel
|
||||
htmlFor="checkout-7j9-same-as-shipping-wgm"
|
||||
className="font-normal"
|
||||
>
|
||||
Same as shipping address
|
||||
</FieldLabel>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</FieldSet>
|
||||
<FieldSeparator />
|
||||
<FieldSet>
|
||||
<FieldGroup>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="checkout-7j9-optional-comments">
|
||||
Comments
|
||||
</FieldLabel>
|
||||
<Textarea
|
||||
id="checkout-7j9-optional-comments"
|
||||
placeholder="Add any additional comments"
|
||||
/>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</FieldSet>
|
||||
<Field orientation="horizontal">
|
||||
<Button type="submit">Submit</Button>
|
||||
<Button variant="outline" type="button">
|
||||
Cancel
|
||||
</Button>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
|
||||
import {
|
||||
Field,
|
||||
FieldDescription,
|
||||
FieldTitle,
|
||||
} from "@/registry/new-york-v4/ui/field"
|
||||
import { Slider } from "@/registry/new-york-v4/ui/slider"
|
||||
|
||||
export function FieldSlider() {
|
||||
const [value, setValue] = useState([200, 800])
|
||||
return (
|
||||
<div className="w-full max-w-md">
|
||||
<Field>
|
||||
<FieldTitle>Price Range</FieldTitle>
|
||||
<FieldDescription>
|
||||
Set your budget range ($
|
||||
<span className="font-medium tabular-nums">{value[0]}</span> -{" "}
|
||||
<span className="font-medium tabular-nums">{value[1]}</span>).
|
||||
</FieldDescription>
|
||||
<Slider
|
||||
value={value}
|
||||
onValueChange={setValue}
|
||||
max={1000}
|
||||
min={0}
|
||||
step={10}
|
||||
className="mt-2 w-full"
|
||||
aria-label="Price Range"
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { IconInfoCircle, IconStar } from "@tabler/icons-react"
|
||||
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupInput,
|
||||
} from "@/registry/new-york-v4/ui/input-group"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/registry/new-york-v4/ui/popover"
|
||||
|
||||
export function InputGroupButtonExample() {
|
||||
const [isFavorite, setIsFavorite] = React.useState(false)
|
||||
|
||||
return (
|
||||
<div className="grid w-full max-w-sm gap-6">
|
||||
<InputGroup className="[--radius:9999px]">
|
||||
<InputGroupInput id="input-secure-19" />
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<InputGroupAddon>
|
||||
<InputGroupButton variant="secondary" size="icon-xs">
|
||||
<IconInfoCircle />
|
||||
</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
align="start"
|
||||
className="flex flex-col gap-1 rounded-xl text-sm"
|
||||
>
|
||||
<p className="font-medium">Your connection is not secure.</p>
|
||||
<p>You should not enter any sensitive information on this site.</p>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<InputGroupAddon className="text-muted-foreground !pl-1">
|
||||
https://
|
||||
</InputGroupAddon>
|
||||
<InputGroupAddon align="inline-end">
|
||||
<InputGroupButton
|
||||
onClick={() => setIsFavorite(!isFavorite)}
|
||||
size="icon-xs"
|
||||
>
|
||||
<IconStar
|
||||
data-favorite={isFavorite}
|
||||
className="data-[favorite=true]:fill-primary data-[favorite=true]:stroke-primary"
|
||||
/>
|
||||
</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
import { IconCheck, IconInfoCircle, IconPlus } from "@tabler/icons-react"
|
||||
import { ArrowUpIcon, Search } from "lucide-react"
|
||||
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/registry/new-york-v4/ui/dropdown-menu"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupInput,
|
||||
InputGroupText,
|
||||
InputGroupTextarea,
|
||||
} from "@/registry/new-york-v4/ui/input-group"
|
||||
import { Separator } from "@/registry/new-york-v4/ui/separator"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/registry/new-york-v4/ui/tooltip"
|
||||
|
||||
export function InputGroupDemo() {
|
||||
return (
|
||||
<div className="grid w-full max-w-sm gap-6">
|
||||
<InputGroup>
|
||||
<InputGroupInput placeholder="Search..." />
|
||||
<InputGroupAddon>
|
||||
<Search />
|
||||
</InputGroupAddon>
|
||||
<InputGroupAddon align="inline-end">12 results</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<InputGroup>
|
||||
<InputGroupInput placeholder="example.com" className="!pl-1" />
|
||||
<InputGroupAddon>
|
||||
<InputGroupText>https://</InputGroupText>
|
||||
</InputGroupAddon>
|
||||
<InputGroupAddon align="inline-end">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<InputGroupButton className="rounded-full" size="icon-xs">
|
||||
<IconInfoCircle />
|
||||
</InputGroupButton>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>This is content in a tooltip.</TooltipContent>
|
||||
</Tooltip>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<InputGroup>
|
||||
<InputGroupTextarea placeholder="Ask, Search or Chat..." />
|
||||
<InputGroupAddon align="block-end">
|
||||
<InputGroupButton
|
||||
variant="outline"
|
||||
className="rounded-full"
|
||||
size="icon-xs"
|
||||
>
|
||||
<IconPlus />
|
||||
</InputGroupButton>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<InputGroupButton variant="ghost">Auto</InputGroupButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
side="top"
|
||||
align="start"
|
||||
className="[--radius:0.95rem]"
|
||||
>
|
||||
<DropdownMenuItem>Auto</DropdownMenuItem>
|
||||
<DropdownMenuItem>Agent</DropdownMenuItem>
|
||||
<DropdownMenuItem>Manual</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<InputGroupText className="ml-auto">52% used</InputGroupText>
|
||||
<Separator orientation="vertical" className="!h-4" />
|
||||
<InputGroupButton
|
||||
variant="default"
|
||||
className="rounded-full"
|
||||
size="icon-xs"
|
||||
>
|
||||
<ArrowUpIcon />
|
||||
<span className="sr-only">Send</span>
|
||||
</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<InputGroup>
|
||||
<InputGroupInput placeholder="@shadcn" />
|
||||
<InputGroupAddon align="inline-end">
|
||||
<div className="bg-primary text-foreground flex size-4 items-center justify-center rounded-full">
|
||||
<IconCheck className="size-3 text-white" />
|
||||
</div>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
import {
|
||||
IconBrandJavascript,
|
||||
IconCopy,
|
||||
IconCornerDownLeft,
|
||||
IconRefresh,
|
||||
} from "@tabler/icons-react"
|
||||
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupText,
|
||||
InputGroupTextarea,
|
||||
} from "@/registry/new-york-v4/ui/input-group"
|
||||
|
||||
export function InputGroupTextareaExample() {
|
||||
return (
|
||||
<div className="grid w-full max-w-md gap-4">
|
||||
<InputGroup>
|
||||
<InputGroupTextarea
|
||||
id="textarea-code-32"
|
||||
placeholder="console.log('Hello, world!');"
|
||||
className="min-h-[180px]"
|
||||
/>
|
||||
<InputGroupAddon align="block-end" className="border-t">
|
||||
<InputGroupText>Line 1, Column 1</InputGroupText>
|
||||
<InputGroupButton size="sm" className="ml-auto" variant="default">
|
||||
Run <IconCornerDownLeft />
|
||||
</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
<InputGroupAddon align="block-start" className="border-b">
|
||||
<InputGroupText className="font-mono font-medium">
|
||||
<IconBrandJavascript />
|
||||
script.js
|
||||
</InputGroupText>
|
||||
<InputGroupButton className="ml-auto">
|
||||
<IconRefresh />
|
||||
</InputGroupButton>
|
||||
<InputGroupButton variant="ghost">
|
||||
<IconCopy />
|
||||
</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
import { Plus } from "lucide-react"
|
||||
|
||||
import {
|
||||
Avatar,
|
||||
AvatarFallback,
|
||||
AvatarImage,
|
||||
} from "@/registry/new-york-v4/ui/avatar"
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import {
|
||||
Item,
|
||||
ItemActions,
|
||||
ItemContent,
|
||||
ItemDescription,
|
||||
ItemMedia,
|
||||
ItemTitle,
|
||||
} from "@/registry/new-york-v4/ui/item"
|
||||
|
||||
export function ItemAvatar() {
|
||||
return (
|
||||
<div className="flex w-full max-w-lg flex-col gap-6">
|
||||
<Item variant="outline" className="hidden">
|
||||
<ItemMedia>
|
||||
<Avatar className="size-10">
|
||||
<AvatarImage src="https://github.com/maxleiter.png" />
|
||||
<AvatarFallback>LR</AvatarFallback>
|
||||
</Avatar>
|
||||
</ItemMedia>
|
||||
<ItemContent>
|
||||
<ItemTitle>Max Leiter</ItemTitle>
|
||||
<ItemDescription>Last seen 5 months ago</ItemDescription>
|
||||
</ItemContent>
|
||||
<ItemActions>
|
||||
<Button
|
||||
size="icon-sm"
|
||||
variant="outline"
|
||||
className="rounded-full"
|
||||
aria-label="Invite"
|
||||
>
|
||||
<Plus />
|
||||
</Button>
|
||||
</ItemActions>
|
||||
</Item>
|
||||
<Item variant="outline">
|
||||
<ItemMedia>
|
||||
<div className="*:data-[slot=avatar]:ring-background flex -space-x-2 *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:grayscale">
|
||||
<Avatar className="hidden sm:flex">
|
||||
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
|
||||
<AvatarFallback>CN</AvatarFallback>
|
||||
</Avatar>
|
||||
<Avatar className="hidden sm:flex">
|
||||
<AvatarImage
|
||||
src="https://github.com/maxleiter.png"
|
||||
alt="@maxleiter"
|
||||
/>
|
||||
<AvatarFallback>LR</AvatarFallback>
|
||||
</Avatar>
|
||||
<Avatar>
|
||||
<AvatarImage
|
||||
src="https://github.com/evilrabbit.png"
|
||||
alt="@evilrabbit"
|
||||
/>
|
||||
<AvatarFallback>ER</AvatarFallback>
|
||||
</Avatar>
|
||||
</div>
|
||||
</ItemMedia>
|
||||
<ItemContent>
|
||||
<ItemTitle>No Team Members</ItemTitle>
|
||||
<ItemDescription>Invite your team to collaborate.</ItemDescription>
|
||||
</ItemContent>
|
||||
<ItemActions>
|
||||
<Button size="sm" variant="outline">
|
||||
Invite
|
||||
</Button>
|
||||
</ItemActions>
|
||||
</Item>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
import { BadgeCheckIcon, ChevronRightIcon } from "lucide-react"
|
||||
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import {
|
||||
Item,
|
||||
ItemActions,
|
||||
ItemContent,
|
||||
ItemDescription,
|
||||
ItemMedia,
|
||||
ItemTitle,
|
||||
} from "@/registry/new-york-v4/ui/item"
|
||||
|
||||
export function ItemDemo() {
|
||||
return (
|
||||
<div className="flex w-full max-w-md flex-col gap-6">
|
||||
<Item variant="outline">
|
||||
<ItemContent>
|
||||
<ItemTitle>Two-factor authentication</ItemTitle>
|
||||
<ItemDescription className="text-pretty">
|
||||
Verify via email or phone number.
|
||||
</ItemDescription>
|
||||
</ItemContent>
|
||||
<ItemActions>
|
||||
<Button size="sm">Enable</Button>
|
||||
</ItemActions>
|
||||
</Item>
|
||||
<Item variant="outline" size="sm" asChild>
|
||||
<a href="#">
|
||||
<ItemMedia>
|
||||
<BadgeCheckIcon className="size-5" />
|
||||
</ItemMedia>
|
||||
<ItemContent>
|
||||
<ItemTitle>Your profile has been verified.</ItemTitle>
|
||||
</ItemContent>
|
||||
<ItemActions>
|
||||
<ChevronRightIcon className="size-4" />
|
||||
</ItemActions>
|
||||
</a>
|
||||
</Item>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,467 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import { useMemo, useState } from "react"
|
||||
import {
|
||||
IconApps,
|
||||
IconArrowUp,
|
||||
IconAt,
|
||||
IconBook,
|
||||
IconBrandAbstract,
|
||||
IconBrandOpenai,
|
||||
IconBrandZeit,
|
||||
IconCircleDashedPlus,
|
||||
IconPaperclip,
|
||||
IconPlus,
|
||||
IconWorld,
|
||||
IconX,
|
||||
} from "@tabler/icons-react"
|
||||
|
||||
import {
|
||||
Avatar,
|
||||
AvatarFallback,
|
||||
AvatarImage,
|
||||
} from "@/registry/new-york-v4/ui/avatar"
|
||||
import { Badge } from "@/registry/new-york-v4/ui/badge"
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@/registry/new-york-v4/ui/command"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/registry/new-york-v4/ui/dropdown-menu"
|
||||
import { Field, FieldLabel } from "@/registry/new-york-v4/ui/field"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupTextarea,
|
||||
} from "@/registry/new-york-v4/ui/input-group"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/registry/new-york-v4/ui/popover"
|
||||
import { Switch } from "@/registry/new-york-v4/ui/switch"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/registry/new-york-v4/ui/tooltip"
|
||||
|
||||
const SAMPLE_DATA = {
|
||||
mentionable: [
|
||||
{
|
||||
type: "page",
|
||||
title: "Meeting Notes",
|
||||
image: "📝",
|
||||
},
|
||||
{
|
||||
type: "page",
|
||||
title: "Project Dashboard",
|
||||
image: "📊",
|
||||
},
|
||||
{
|
||||
type: "page",
|
||||
title: "Ideas & Brainstorming",
|
||||
image: "💡",
|
||||
},
|
||||
{
|
||||
type: "page",
|
||||
title: "Calendar & Events",
|
||||
image: "📅",
|
||||
},
|
||||
{
|
||||
type: "page",
|
||||
title: "Documentation",
|
||||
image: "📚",
|
||||
},
|
||||
{
|
||||
type: "page",
|
||||
title: "Goals & Objectives",
|
||||
image: "🎯",
|
||||
},
|
||||
{
|
||||
type: "page",
|
||||
title: "Budget Planning",
|
||||
image: "💰",
|
||||
},
|
||||
{
|
||||
type: "page",
|
||||
title: "Team Directory",
|
||||
image: "👥",
|
||||
},
|
||||
{
|
||||
type: "page",
|
||||
title: "Technical Specs",
|
||||
image: "🔧",
|
||||
},
|
||||
{
|
||||
type: "page",
|
||||
title: "Analytics Report",
|
||||
image: "📈",
|
||||
},
|
||||
{
|
||||
type: "user",
|
||||
title: "shadcn",
|
||||
image: "https://github.com/shadcn.png",
|
||||
workspace: "Workspace",
|
||||
},
|
||||
{
|
||||
type: "user",
|
||||
title: "maxleiter",
|
||||
image: "https://github.com/maxleiter.png",
|
||||
workspace: "Cursor",
|
||||
},
|
||||
{
|
||||
type: "user",
|
||||
title: "evilrabbit",
|
||||
image: "https://github.com/evilrabbit.png",
|
||||
workspace: "Vercel",
|
||||
},
|
||||
],
|
||||
models: [
|
||||
{
|
||||
name: "Auto",
|
||||
icon: IconBrandZeit,
|
||||
},
|
||||
{
|
||||
name: "Claude Sonnet 4",
|
||||
icon: IconBrandAbstract,
|
||||
badge: "Beta",
|
||||
},
|
||||
{
|
||||
name: "GPT-5",
|
||||
icon: IconBrandOpenai,
|
||||
badge: "Beta",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
function MentionableIcon({
|
||||
item,
|
||||
}: {
|
||||
item: (typeof SAMPLE_DATA.mentionable)[0]
|
||||
}) {
|
||||
return item.type === "page" ? (
|
||||
<span className="flex size-4 items-center justify-center">
|
||||
{item.image}
|
||||
</span>
|
||||
) : (
|
||||
<Avatar className="size-4">
|
||||
<AvatarImage src={item.image} />
|
||||
<AvatarFallback>{item.title[0]}</AvatarFallback>
|
||||
</Avatar>
|
||||
)
|
||||
}
|
||||
|
||||
export function NotionPromptForm() {
|
||||
const [mentions, setMentions] = useState<string[]>([])
|
||||
const [mentionPopoverOpen, setMentionPopoverOpen] = useState(false)
|
||||
const [modelPopoverOpen, setModelPopoverOpen] = useState(false)
|
||||
const [selectedModel, setSelectedModel] = useState<
|
||||
(typeof SAMPLE_DATA.models)[0]
|
||||
>(SAMPLE_DATA.models[0])
|
||||
const [scopeMenuOpen, setScopeMenuOpen] = useState(false)
|
||||
|
||||
const grouped = useMemo(() => {
|
||||
return SAMPLE_DATA.mentionable.reduce(
|
||||
(acc, item) => {
|
||||
const isAvailable = !mentions.includes(item.title)
|
||||
|
||||
if (isAvailable) {
|
||||
if (!acc[item.type]) {
|
||||
acc[item.type] = []
|
||||
}
|
||||
acc[item.type].push(item)
|
||||
}
|
||||
return acc
|
||||
},
|
||||
{} as Record<string, typeof SAMPLE_DATA.mentionable>
|
||||
)
|
||||
}, [mentions])
|
||||
|
||||
const hasMentions = mentions.length > 0
|
||||
|
||||
return (
|
||||
<form className="[--radius:1.2rem]">
|
||||
<Field>
|
||||
<FieldLabel htmlFor="notion-prompt" className="sr-only">
|
||||
Prompt
|
||||
</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupTextarea
|
||||
id="notion-prompt"
|
||||
placeholder="Ask, search, or make anything..."
|
||||
/>
|
||||
<InputGroupAddon align="block-start">
|
||||
<Popover
|
||||
open={mentionPopoverOpen}
|
||||
onOpenChange={setMentionPopoverOpen}
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
asChild
|
||||
onFocusCapture={(e) => e.stopPropagation()}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<InputGroupButton
|
||||
variant="outline"
|
||||
size={!hasMentions ? "sm" : "icon-sm"}
|
||||
className="rounded-full transition-transform"
|
||||
>
|
||||
<IconAt /> {!hasMentions && "Add context"}
|
||||
</InputGroupButton>
|
||||
</PopoverTrigger>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Mention a person, page, or date</TooltipContent>
|
||||
</Tooltip>
|
||||
<PopoverContent className="p-0 [--radius:1.2rem]" align="start">
|
||||
<Command>
|
||||
<CommandInput placeholder="Search pages..." />
|
||||
<CommandList>
|
||||
<CommandEmpty>No pages found</CommandEmpty>
|
||||
{Object.entries(grouped).map(([type, items]) => (
|
||||
<CommandGroup
|
||||
key={type}
|
||||
heading={type === "page" ? "Pages" : "Users"}
|
||||
>
|
||||
{items.map((item) => (
|
||||
<CommandItem
|
||||
key={item.title}
|
||||
value={item.title}
|
||||
onSelect={(currentValue) => {
|
||||
setMentions((prev) => [...prev, currentValue])
|
||||
setMentionPopoverOpen(false)
|
||||
}}
|
||||
>
|
||||
<MentionableIcon item={item} />
|
||||
{item.title}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
))}
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<div className="no-scrollbar -m-1.5 flex gap-1 overflow-y-auto p-1.5">
|
||||
{mentions.map((mention) => {
|
||||
const item = SAMPLE_DATA.mentionable.find(
|
||||
(item) => item.title === mention
|
||||
)
|
||||
|
||||
if (!item) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<InputGroupButton
|
||||
key={mention}
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
className="rounded-full !pl-2"
|
||||
onClick={() => {
|
||||
setMentions((prev) => prev.filter((m) => m !== mention))
|
||||
}}
|
||||
>
|
||||
<MentionableIcon item={item} />
|
||||
{item.title}
|
||||
<IconX />
|
||||
</InputGroupButton>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</InputGroupAddon>
|
||||
<InputGroupAddon align="block-end" className="gap-1">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<InputGroupButton
|
||||
size="icon-sm"
|
||||
className="rounded-full"
|
||||
aria-label="Attach file"
|
||||
>
|
||||
<IconPaperclip />
|
||||
</InputGroupButton>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Attach file</TooltipContent>
|
||||
</Tooltip>
|
||||
<DropdownMenu
|
||||
open={modelPopoverOpen}
|
||||
onOpenChange={setModelPopoverOpen}
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<InputGroupButton size="sm" className="rounded-full">
|
||||
{selectedModel.icon && selectedModel.name !== "Auto" && (
|
||||
<selectedModel.icon />
|
||||
)}
|
||||
{selectedModel.name}
|
||||
</InputGroupButton>
|
||||
</DropdownMenuTrigger>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Select AI model</TooltipContent>
|
||||
</Tooltip>
|
||||
<DropdownMenuContent
|
||||
side="top"
|
||||
align="start"
|
||||
className="[--radius:1.2rem]"
|
||||
>
|
||||
<DropdownMenuGroup className="w-72">
|
||||
<DropdownMenuLabel className="text-muted-foreground text-xs">
|
||||
Get answers about your workspace
|
||||
</DropdownMenuLabel>
|
||||
{SAMPLE_DATA.models.map((model) => (
|
||||
<DropdownMenuCheckboxItem
|
||||
key={model.name}
|
||||
checked={model.name === selectedModel.name}
|
||||
onCheckedChange={(checked) => {
|
||||
if (checked) {
|
||||
setSelectedModel(model)
|
||||
}
|
||||
}}
|
||||
className="pl-2 *:[span:first-child]:right-2 *:[span:first-child]:left-auto"
|
||||
>
|
||||
{model.icon && <model.icon />}
|
||||
{model.name}
|
||||
{model.badge && (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="h-5 rounded-sm bg-blue-100 px-1 text-xs text-blue-800 dark:bg-blue-900 dark:text-blue-100"
|
||||
>
|
||||
{model.badge}
|
||||
</Badge>
|
||||
)}
|
||||
</DropdownMenuCheckboxItem>
|
||||
))}
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<DropdownMenu open={scopeMenuOpen} onOpenChange={setScopeMenuOpen}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<InputGroupButton size="sm" className="rounded-full">
|
||||
<IconWorld /> All Sources
|
||||
</InputGroupButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
side="top"
|
||||
align="start"
|
||||
className="[--radius:1.2rem]"
|
||||
>
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem
|
||||
asChild
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
<label htmlFor="web-search">
|
||||
<IconWorld /> Web Search{" "}
|
||||
<Switch
|
||||
id="web-search"
|
||||
className="ml-auto"
|
||||
defaultChecked
|
||||
/>
|
||||
</label>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem
|
||||
asChild
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
<label htmlFor="apps">
|
||||
<IconApps /> Apps and Integrations
|
||||
<Switch id="apps" className="ml-auto" defaultChecked />
|
||||
</label>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<IconCircleDashedPlus /> All Sources I can access
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger>
|
||||
<Avatar className="size-4">
|
||||
<AvatarImage src="https://github.com/shadcn.png" />
|
||||
<AvatarFallback>CN</AvatarFallback>
|
||||
</Avatar>
|
||||
shadcn
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuSubContent className="w-72 p-0 [--radius:1.2rem]">
|
||||
<Command>
|
||||
<CommandInput
|
||||
placeholder="Find or use knowledge in..."
|
||||
autoFocus
|
||||
/>
|
||||
<CommandList>
|
||||
<CommandEmpty>No knowledge found</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{SAMPLE_DATA.mentionable
|
||||
.filter((item) => item.type === "user")
|
||||
.map((user) => (
|
||||
<CommandItem
|
||||
key={user.title}
|
||||
value={user.title}
|
||||
onSelect={() => {
|
||||
// Handle user selection here
|
||||
console.log("Selected user:", user.title)
|
||||
}}
|
||||
>
|
||||
<Avatar className="size-4">
|
||||
<AvatarImage src={user.image} />
|
||||
<AvatarFallback>
|
||||
{user.title[0]}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
{user.title}{" "}
|
||||
<span className="text-muted-foreground">
|
||||
- {user.workspace}
|
||||
</span>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuSub>
|
||||
<DropdownMenuItem>
|
||||
<IconBook /> Help Center
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>
|
||||
<IconPlus /> Connect Apps
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuLabel className="text-muted-foreground text-xs">
|
||||
We'll only search in the sources selected here.
|
||||
</DropdownMenuLabel>
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<InputGroupButton
|
||||
aria-label="Send"
|
||||
className="ml-auto rounded-full"
|
||||
variant="default"
|
||||
size="icon-sm"
|
||||
>
|
||||
<IconArrowUp />
|
||||
</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</Field>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import { Badge } from "@/registry/new-york-v4/ui/badge"
|
||||
import { Spinner } from "@/registry/new-york-v4/ui/spinner"
|
||||
|
||||
export function SpinnerBadge() {
|
||||
return (
|
||||
<div className="flex items-center gap-2 [--radius:1.2rem]">
|
||||
<Badge>
|
||||
<Spinner />
|
||||
Syncing
|
||||
</Badge>
|
||||
<Badge variant="secondary">
|
||||
<Spinner />
|
||||
Updating
|
||||
</Badge>
|
||||
<Badge variant="outline">
|
||||
<Spinner />
|
||||
Processing
|
||||
</Badge>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import {
|
||||
Empty,
|
||||
EmptyContent,
|
||||
EmptyDescription,
|
||||
EmptyHeader,
|
||||
EmptyMedia,
|
||||
EmptyTitle,
|
||||
} from "@/registry/new-york-v4/ui/empty"
|
||||
import { Spinner } from "@/registry/new-york-v4/ui/spinner"
|
||||
|
||||
export function SpinnerEmpty() {
|
||||
return (
|
||||
<Empty className="w-full">
|
||||
<EmptyHeader>
|
||||
<EmptyMedia variant="icon">
|
||||
<Spinner />
|
||||
</EmptyMedia>
|
||||
<EmptyTitle>Processing your request</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
Please wait while we process your request. Do not refresh the page.
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
<EmptyContent>
|
||||
<Button variant="outline" size="sm">
|
||||
Cancel
|
||||
</Button>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
)
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Separator } from "@/registry/new-york-v4/ui/separator"
|
||||
|
||||
const buttonGroupVariants = cva(
|
||||
"flex w-fit items-stretch [&>*]:focus-visible:z-10 [&>*]:focus-visible:relative [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md has-[>[data-slot=button-group]]:gap-2",
|
||||
{
|
||||
variants: {
|
||||
orientation: {
|
||||
horizontal:
|
||||
"[&>*:not(:first-child)]:rounded-l-none [&>*:not(:first-child)]:border-l-0 [&>*:not(:last-child)]:rounded-r-none",
|
||||
vertical:
|
||||
"flex-col [&>*:not(:first-child)]:rounded-t-none [&>*:not(:first-child)]:border-t-0 [&>*:not(:last-child)]:rounded-b-none",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
orientation: "horizontal",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function ButtonGroup({
|
||||
className,
|
||||
orientation,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & VariantProps<typeof buttonGroupVariants>) {
|
||||
return (
|
||||
<div
|
||||
role="group"
|
||||
data-slot="button-group"
|
||||
data-orientation={orientation}
|
||||
className={cn(buttonGroupVariants({ orientation }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function ButtonGroupText({
|
||||
className,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & {
|
||||
asChild?: boolean
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "div"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
className={cn(
|
||||
"bg-muted flex items-center gap-2 rounded-md border px-4 text-sm font-medium shadow-xs [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function ButtonGroupSeparator({
|
||||
className,
|
||||
orientation = "vertical",
|
||||
...props
|
||||
}: React.ComponentProps<typeof Separator>) {
|
||||
return (
|
||||
<Separator
|
||||
data-slot="button-group-separator"
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"bg-input relative !m-0 self-stretch data-[orientation=vertical]:h-auto",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
ButtonGroup,
|
||||
ButtonGroupSeparator,
|
||||
ButtonGroupText,
|
||||
buttonGroupVariants,
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||
outline:
|
||||
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost:
|
||||
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
||||
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||
icon: "size-9",
|
||||
"icon-sm": "size-8",
|
||||
"icon-lg": "size-10",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Button({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"button"> &
|
||||
VariantProps<typeof buttonVariants> & {
|
||||
asChild?: boolean
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="button"
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Button, buttonVariants }
|
||||
@@ -1,257 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
||||
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function DropdownMenu({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
|
||||
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
|
||||
}
|
||||
|
||||
function DropdownMenuPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Trigger
|
||||
data-slot="dropdown-menu-trigger"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuContent({
|
||||
className,
|
||||
sideOffset = 4,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
data-slot="dropdown-menu-content"
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuItem({
|
||||
className,
|
||||
inset,
|
||||
variant = "default",
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
|
||||
inset?: boolean
|
||||
variant?: "default" | "destructive"
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Item
|
||||
data-slot="dropdown-menu-item"
|
||||
data-inset={inset}
|
||||
data-variant={variant}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuCheckboxItem({
|
||||
className,
|
||||
children,
|
||||
checked,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
data-slot="dropdown-menu-checkbox-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<CheckIcon className="size-4" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuRadioGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.RadioGroup
|
||||
data-slot="dropdown-menu-radio-group"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuRadioItem({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
data-slot="dropdown-menu-radio-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<CircleIcon className="size-2 fill-current" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuLabel({
|
||||
className,
|
||||
inset,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
|
||||
inset?: boolean
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Label
|
||||
data-slot="dropdown-menu-label"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
data-slot="dropdown-menu-separator"
|
||||
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuShortcut({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="dropdown-menu-shortcut"
|
||||
className={cn(
|
||||
"text-muted-foreground ml-auto text-xs tracking-widest",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuSub({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
|
||||
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
|
||||
}
|
||||
|
||||
function DropdownMenuSubTrigger({
|
||||
className,
|
||||
inset,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
data-slot="dropdown-menu-sub-trigger"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRightIcon className="ml-auto size-4" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuSubContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
data-slot="dropdown-menu-sub-content"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
DropdownMenu,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuSubContent,
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Empty({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="empty"
|
||||
className={cn(
|
||||
"flex min-w-0 flex-1 flex-col items-center justify-center gap-6 rounded-lg border-dashed p-6 text-center text-balance md:p-12",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function EmptyHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="empty-header"
|
||||
className={cn(
|
||||
"flex max-w-sm flex-col items-center gap-2 text-center",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const emptyMediaVariants = cva(
|
||||
"flex shrink-0 items-center justify-center mb-2 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-transparent",
|
||||
icon: "bg-muted text-foreground flex size-10 shrink-0 items-center justify-center rounded-lg [&_svg:not([class*='size-'])]:size-6",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function EmptyMedia({
|
||||
className,
|
||||
variant = "default",
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & VariantProps<typeof emptyMediaVariants>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="empty-icon"
|
||||
data-variant={variant}
|
||||
className={cn(emptyMediaVariants({ variant, className }))}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function EmptyTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="empty-title"
|
||||
className={cn("text-lg font-medium tracking-tight", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function EmptyDescription({ className, ...props }: React.ComponentProps<"p">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="empty-description"
|
||||
className={cn(
|
||||
"text-muted-foreground [&>a:hover]:text-primary text-sm/relaxed [&>a]:underline [&>a]:underline-offset-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function EmptyContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="empty-content"
|
||||
className={cn(
|
||||
"flex w-full max-w-sm min-w-0 flex-col items-center gap-4 text-sm text-balance",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Empty,
|
||||
EmptyHeader,
|
||||
EmptyTitle,
|
||||
EmptyDescription,
|
||||
EmptyContent,
|
||||
EmptyMedia,
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user