mirror of
https://github.com/shadcn-ui/ui.git
synced 2026-06-14 03:11:33 +00:00
Compare commits
110 Commits
shadcn@3.0
...
shadcn/tes
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3a5fa409eb | ||
|
|
6567897393 | ||
|
|
2675fa3941 | ||
|
|
fbda67c88c | ||
|
|
e8674ee848 | ||
|
|
adb66f4d43 | ||
|
|
3afb46eaf6 | ||
|
|
7cd019ad36 | ||
|
|
bea7d30536 | ||
|
|
40c3ff513a | ||
|
|
89ebfdce47 | ||
|
|
b83023034a | ||
|
|
6a534d7954 | ||
|
|
ef1987ded9 | ||
|
|
77bf7d28b4 | ||
|
|
41f4f7357d | ||
|
|
bc99818e04 | ||
|
|
162ba7b13c | ||
|
|
f12db1e3a2 | ||
|
|
ce3e2b1df8 | ||
|
|
dcfe911b33 | ||
|
|
7210a4919a | ||
|
|
d198908510 | ||
|
|
b0b1cd1f0d | ||
|
|
f3d70724b6 | ||
|
|
407e9c6802 | ||
|
|
c67e630521 | ||
|
|
f494411953 | ||
|
|
a43c1d1342 | ||
|
|
607a6fd127 | ||
|
|
fbcc665b49 | ||
|
|
7ddcf31e43 | ||
|
|
3e39163b08 | ||
|
|
e311fdae04 | ||
|
|
26640d9d88 | ||
|
|
3e20c228da | ||
|
|
0810c0e1a2 | ||
|
|
1205ea5445 | ||
|
|
4430ab8bab | ||
|
|
d6716db9cc | ||
|
|
da8fa6aacd | ||
|
|
e96f9edf02 | ||
|
|
b19e9cadb2 | ||
|
|
3bb47bf914 | ||
|
|
a72fac6fde | ||
|
|
4b3186c46b | ||
|
|
e67e955f2a | ||
|
|
bf047b9824 | ||
|
|
04432835f9 | ||
|
|
77e6f28e81 | ||
|
|
f1e51ec8a1 | ||
|
|
3c525b8305 | ||
|
|
e7e844ff63 | ||
|
|
e14c55ac65 | ||
|
|
043be944ab | ||
|
|
4eb257bc14 | ||
|
|
1289192d4f | ||
|
|
75dde2e646 | ||
|
|
b9f3ce1988 | ||
|
|
cdf58be7e1 | ||
|
|
fae1a81add | ||
|
|
fc6d909ba2 | ||
|
|
590b9be610 | ||
|
|
41eb9d5c46 | ||
|
|
b7c28199be | ||
|
|
7869defd42 | ||
|
|
6daa5215cc | ||
|
|
722fb81b95 | ||
|
|
543be31722 | ||
|
|
09b90cd5c2 | ||
|
|
c95959a9b3 | ||
|
|
08820ce5ee | ||
|
|
cb96e58992 | ||
|
|
fce5926265 | ||
|
|
f7c0f81258 | ||
|
|
960b22b301 | ||
|
|
6f057c9cc3 | ||
|
|
615a32d97a | ||
|
|
bfe6e1946c | ||
|
|
baaa82e4e7 | ||
|
|
caeed7bd65 | ||
|
|
61254f0c3f | ||
|
|
3dcd797f2c | ||
|
|
b76f5cdbf7 | ||
|
|
fcb1e2ca50 | ||
|
|
df94537e0f | ||
|
|
275e3a2d59 | ||
|
|
e5402f9a20 | ||
|
|
04668da018 | ||
|
|
0805751703 | ||
|
|
9ecb19cf2e | ||
|
|
9c5eb0d20f | ||
|
|
2752ce11d8 | ||
|
|
d972caa853 | ||
|
|
00b2f0796e | ||
|
|
3ed9af5757 | ||
|
|
a4237e38f7 | ||
|
|
1178d40352 | ||
|
|
cc612359ee | ||
|
|
4d0272a659 | ||
|
|
a15534bdb7 | ||
|
|
62c41c3271 | ||
|
|
851c0fa0d1 | ||
|
|
e84c819977 | ||
|
|
64f8baf9aa | ||
|
|
4b44c6489a | ||
|
|
f9021e9388 | ||
|
|
b1e3d4b740 | ||
|
|
084fb927a1 | ||
|
|
7304ef2105 |
5
.changeset/fix-universal-registry-item-empty-files.md
Normal file
5
.changeset/fix-universal-registry-item-empty-files.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"shadcn": patch
|
||||
---
|
||||
|
||||
Fix support for universal registry items that only have dependencies without files
|
||||
5
.changeset/hot-feet-beam.md
Normal file
5
.changeset/hot-feet-beam.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"shadcn": patch
|
||||
---
|
||||
|
||||
add support for color as var
|
||||
5
.changeset/lazy-mails-wear.md
Normal file
5
.changeset/lazy-mails-wear.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"shadcn": patch
|
||||
---
|
||||
|
||||
fix adding registry item with CSS at-property
|
||||
75
.github/workflows/deprecated.yml
vendored
Normal file
75
.github/workflows/deprecated.yml
vendored
Normal file
@@ -0,0 +1,75 @@
|
||||
name: Deprecated
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
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:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Get changed files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@v44
|
||||
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/')
|
||||
);
|
||||
|
||||
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');
|
||||
}
|
||||
}
|
||||
54
.github/workflows/validate-registries.yml
vendored
Normal file
54
.github/workflows/validate-registries.yml
vendored
Normal file
@@ -0,0 +1,54 @@
|
||||
name: Validate Registries
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "apps/v4/public/r/registries.json"
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "apps/v4/public/r/registries.json"
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
runs-on: ubuntu-latest
|
||||
name: pnpm validate:registries
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
name: Install pnpm
|
||||
id: pnpm-install
|
||||
with:
|
||||
version: 9.0.6
|
||||
run_install: false
|
||||
|
||||
- name: Get pnpm store directory
|
||||
id: pnpm-cache
|
||||
run: |
|
||||
echo "pnpm_cache_dir=$(pnpm store path)" >> $GITHUB_OUTPUT
|
||||
- uses: actions/cache@v3
|
||||
name: Setup pnpm cache
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Build packages
|
||||
run: pnpm build --filter=shadcn
|
||||
|
||||
- name: Validate registries
|
||||
run: pnpm --filter=v4 validate:registries
|
||||
12
.vscode/settings.json
vendored
12
.vscode/settings.json
vendored
@@ -3,15 +3,13 @@
|
||||
{ "pattern": "apps/*/" },
|
||||
{ "pattern": "packages/*/" }
|
||||
],
|
||||
"tailwindCSS.experimental.classRegex": [
|
||||
["cva\\(((?:[^()]|\\([^()]*\\))*)\\)", "[\"'`]?([^\"'`]+)[\"'`]?"],
|
||||
["cn\\(((?:[^()]|\\([^()]*\\))*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"]
|
||||
// "cva\\(([^)]*)\\)",
|
||||
// "[\"'`]([^\"'`]*).*?[\"'`]"
|
||||
],
|
||||
"tailwindCSS.classFunctions": ["cva", "cn"],
|
||||
"vitest.debugExclude": [
|
||||
"<node_internals>/**",
|
||||
"**/node_modules/**",
|
||||
"**/fixtures/**"
|
||||
]
|
||||
],
|
||||
"files.exclude": {
|
||||
"apps/www": true
|
||||
}
|
||||
}
|
||||
|
||||
168
apps/v4/app/(app)/(root)/components/appearance-settings.tsx
Normal file
168
apps/v4/app/(app)/(root)/components/appearance-settings.tsx
Normal file
@@ -0,0 +1,168 @@
|
||||
"use client"
|
||||
|
||||
import { IconMinus, IconPlus } from "@tabler/icons-react"
|
||||
import { CheckIcon } from "lucide-react"
|
||||
|
||||
import { useThemeConfig } from "@/components/active-theme"
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import { ButtonGroup } from "@/registry/new-york-v4/ui/button-group"
|
||||
import {
|
||||
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() {
|
||||
const { activeTheme, setActiveTheme } = useThemeConfig()
|
||||
return (
|
||||
<FieldSet>
|
||||
<FieldGroup>
|
||||
<FieldSet>
|
||||
<FieldLegend>Compute Environment</FieldLegend>
|
||||
<FieldDescription>
|
||||
Select the compute environment for your cluster.
|
||||
</FieldDescription>
|
||||
<RadioGroup defaultValue="kubernetes">
|
||||
<FieldLabel htmlFor="kubernetes-r2h">
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldTitle>Kubernetes</FieldTitle>
|
||||
<FieldDescription>
|
||||
Run GPU workloads on a K8s configured cluster. This is the
|
||||
default.
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<RadioGroupItem
|
||||
value="kubernetes"
|
||||
id="kubernetes-r2h"
|
||||
aria-label="Kubernetes"
|
||||
/>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
<FieldLabel htmlFor="vm-z4k">
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldTitle>Virtual Machine</FieldTitle>
|
||||
<FieldDescription>
|
||||
Access a VM configured cluster to run workloads. (Coming
|
||||
soon)
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<RadioGroupItem
|
||||
value="vm"
|
||||
id="vm-z4k"
|
||||
aria-label="Virtual Machine"
|
||||
/>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
</RadioGroup>
|
||||
</FieldSet>
|
||||
<FieldSeparator />
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldTitle>Accent</FieldTitle>
|
||||
<FieldDescription>Select the accent color.</FieldDescription>
|
||||
</FieldContent>
|
||||
<FieldSet aria-label="Accent">
|
||||
<RadioGroup
|
||||
className="flex flex-wrap gap-2"
|
||||
value={activeTheme}
|
||||
onValueChange={setActiveTheme}
|
||||
>
|
||||
{accents.map((accent) => (
|
||||
<Label
|
||||
htmlFor={accent.value}
|
||||
key={accent.value}
|
||||
data-theme={accent.value}
|
||||
className="flex size-6 items-center justify-center rounded-full data-[theme=amber]:bg-amber-600 data-[theme=blue]:bg-blue-700 data-[theme=green]:bg-green-600 data-[theme=rose]:bg-rose-600"
|
||||
>
|
||||
<RadioGroupItem
|
||||
id={accent.value}
|
||||
value={accent.value}
|
||||
aria-label={accent.name}
|
||||
className="peer sr-only"
|
||||
/>
|
||||
<CheckIcon className="hidden size-4 stroke-white peer-data-[state=checked]:block" />
|
||||
</Label>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</FieldSet>
|
||||
</Field>
|
||||
<FieldSeparator />
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor="number-of-gpus-f6l">Number of GPUs</FieldLabel>
|
||||
<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"
|
||||
aria-label="Decrement"
|
||||
>
|
||||
<IconMinus />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon-sm"
|
||||
type="button"
|
||||
aria-label="Increment"
|
||||
>
|
||||
<IconPlus />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</Field>
|
||||
<FieldSeparator />
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor="tinting">Wallpaper Tinting</FieldLabel>
|
||||
<FieldDescription>
|
||||
Allow the wallpaper to be tinted.
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<Switch id="tinting" defaultChecked />
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</FieldSet>
|
||||
)
|
||||
}
|
||||
120
apps/v4/app/(app)/(root)/components/button-group-demo.tsx
Normal file
120
apps/v4/app/(app)/(root)/components/button-group-demo.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import {
|
||||
ArchiveIcon,
|
||||
ArrowLeftIcon,
|
||||
CalendarPlusIcon,
|
||||
ClockIcon,
|
||||
ListFilterPlusIcon,
|
||||
MailCheckIcon,
|
||||
MoreHorizontalIcon,
|
||||
TagIcon,
|
||||
Trash2Icon,
|
||||
} from "lucide-react"
|
||||
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import { ButtonGroup } from "@/registry/new-york-v4/ui/button-group"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/registry/new-york-v4/ui/dropdown-menu"
|
||||
|
||||
export function ButtonGroupDemo() {
|
||||
const [label, setLabel] = React.useState("personal")
|
||||
|
||||
return (
|
||||
<ButtonGroup>
|
||||
<ButtonGroup className="hidden sm:flex">
|
||||
<Button variant="outline" size="icon-sm" aria-label="Go Back">
|
||||
<ArrowLeftIcon />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup>
|
||||
<Button variant="outline" size="sm">
|
||||
Archive
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
Report
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup>
|
||||
<Button variant="outline" size="sm">
|
||||
Snooze
|
||||
</Button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" size="icon-sm" aria-label="More Options">
|
||||
<MoreHorizontalIcon />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-48 [--radius:1rem]">
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>
|
||||
<MailCheckIcon />
|
||||
Mark as Read
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<ArchiveIcon />
|
||||
Archive
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>
|
||||
<ClockIcon />
|
||||
Snooze
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<CalendarPlusIcon />
|
||||
Add to Calendar
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<ListFilterPlusIcon />
|
||||
Add to List
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger>
|
||||
<TagIcon />
|
||||
Label As...
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuSubContent>
|
||||
<DropdownMenuRadioGroup
|
||||
value={label}
|
||||
onValueChange={setLabel}
|
||||
>
|
||||
<DropdownMenuRadioItem value="personal">
|
||||
Personal
|
||||
</DropdownMenuRadioItem>
|
||||
<DropdownMenuRadioItem value="work">
|
||||
Work
|
||||
</DropdownMenuRadioItem>
|
||||
<DropdownMenuRadioItem value="other">
|
||||
Other
|
||||
</DropdownMenuRadioItem>
|
||||
</DropdownMenuRadioGroup>
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuSub>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem variant="destructive">
|
||||
<Trash2Icon />
|
||||
Trash
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</ButtonGroup>
|
||||
</ButtonGroup>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { AudioLinesIcon, PlusIcon } from "lucide-react"
|
||||
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import { ButtonGroup } from "@/registry/new-york-v4/ui/button-group"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupInput,
|
||||
} from "@/registry/new-york-v4/ui/input-group"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/registry/new-york-v4/ui/tooltip"
|
||||
|
||||
export function ButtonGroupInputGroup() {
|
||||
const [voiceEnabled, setVoiceEnabled] = React.useState(false)
|
||||
return (
|
||||
<ButtonGroup className="[--radius:9999rem]">
|
||||
<ButtonGroup>
|
||||
<Button variant="outline" size="icon" aria-label="Add">
|
||||
<PlusIcon />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup className="flex-1">
|
||||
<InputGroup>
|
||||
<InputGroupInput
|
||||
placeholder={
|
||||
voiceEnabled ? "Record and send audio..." : "Send a message..."
|
||||
}
|
||||
disabled={voiceEnabled}
|
||||
/>
|
||||
<InputGroupAddon align="inline-end">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<InputGroupButton
|
||||
onClick={() => setVoiceEnabled(!voiceEnabled)}
|
||||
data-active={voiceEnabled}
|
||||
className="data-[active=true]:bg-primary data-[active=true]:text-primary-foreground"
|
||||
aria-pressed={voiceEnabled}
|
||||
size="icon-xs"
|
||||
aria-label="Voice Mode"
|
||||
>
|
||||
<AudioLinesIcon />
|
||||
</InputGroupButton>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Voice Mode</TooltipContent>
|
||||
</Tooltip>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</ButtonGroup>
|
||||
</ButtonGroup>
|
||||
)
|
||||
}
|
||||
32
apps/v4/app/(app)/(root)/components/button-group-nested.tsx
Normal file
32
apps/v4/app/(app)/(root)/components/button-group-nested.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
"use client"
|
||||
|
||||
import { ArrowLeftIcon, ArrowRightIcon } from "lucide-react"
|
||||
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import { ButtonGroup } from "@/registry/new-york-v4/ui/button-group"
|
||||
|
||||
export function ButtonGroupNested() {
|
||||
return (
|
||||
<ButtonGroup>
|
||||
<ButtonGroup>
|
||||
<Button variant="outline" size="sm">
|
||||
1
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
2
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
3
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup>
|
||||
<Button variant="outline" size="icon-sm" aria-label="Previous">
|
||||
<ArrowLeftIcon />
|
||||
</Button>
|
||||
<Button variant="outline" size="icon-sm" aria-label="Next">
|
||||
<ArrowRightIcon />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</ButtonGroup>
|
||||
)
|
||||
}
|
||||
45
apps/v4/app/(app)/(root)/components/button-group-popover.tsx
Normal file
45
apps/v4/app/(app)/(root)/components/button-group-popover.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import { BotIcon, ChevronDownIcon } from "lucide-react"
|
||||
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import { ButtonGroup } from "@/registry/new-york-v4/ui/button-group"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/registry/new-york-v4/ui/popover"
|
||||
import { Separator } from "@/registry/new-york-v4/ui/separator"
|
||||
import { Textarea } from "@/registry/new-york-v4/ui/textarea"
|
||||
|
||||
export function ButtonGroupPopover() {
|
||||
return (
|
||||
<ButtonGroup>
|
||||
<Button variant="outline" size="sm">
|
||||
<BotIcon /> Copilot
|
||||
</Button>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline" size="icon-sm" aria-label="Open Popover">
|
||||
<ChevronDownIcon />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent align="end" className="rounded-xl p-0 text-sm">
|
||||
<div className="px-4 py-3">
|
||||
<div className="text-sm font-medium">Agent Tasks</div>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="p-4 text-sm *:[p:not(:last-child)]:mb-2">
|
||||
<Textarea
|
||||
placeholder="Describe your task in natural language."
|
||||
className="mb-4 resize-none"
|
||||
/>
|
||||
<p className="font-medium">Start a new task with Copilot</p>
|
||||
<p className="text-muted-foreground">
|
||||
Describe your task in natural language. Copilot will work in the
|
||||
background and open a pull request for your review.
|
||||
</p>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</ButtonGroup>
|
||||
)
|
||||
}
|
||||
57
apps/v4/app/(app)/(root)/components/empty-avatar-group.tsx
Normal file
57
apps/v4/app/(app)/(root)/components/empty-avatar-group.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import { PlusIcon } from "lucide-react"
|
||||
|
||||
import {
|
||||
Avatar,
|
||||
AvatarFallback,
|
||||
AvatarImage,
|
||||
} from "@/registry/new-york-v4/ui/avatar"
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import {
|
||||
Empty,
|
||||
EmptyContent,
|
||||
EmptyDescription,
|
||||
EmptyHeader,
|
||||
EmptyMedia,
|
||||
EmptyTitle,
|
||||
} from "@/registry/new-york-v4/ui/empty"
|
||||
|
||||
export function EmptyAvatarGroup() {
|
||||
return (
|
||||
<Empty className="flex-none border">
|
||||
<EmptyHeader>
|
||||
<EmptyMedia>
|
||||
<div className="*:data-[slot=avatar]:ring-background flex -space-x-2 *:data-[slot=avatar]:size-12 *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:grayscale">
|
||||
<Avatar>
|
||||
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
|
||||
<AvatarFallback>CN</AvatarFallback>
|
||||
</Avatar>
|
||||
<Avatar>
|
||||
<AvatarImage
|
||||
src="https://github.com/maxleiter.png"
|
||||
alt="@maxleiter"
|
||||
/>
|
||||
<AvatarFallback>LR</AvatarFallback>
|
||||
</Avatar>
|
||||
<Avatar>
|
||||
<AvatarImage
|
||||
src="https://github.com/evilrabbit.png"
|
||||
alt="@evilrabbit"
|
||||
/>
|
||||
<AvatarFallback>ER</AvatarFallback>
|
||||
</Avatar>
|
||||
</div>
|
||||
</EmptyMedia>
|
||||
<EmptyTitle>No Team Members</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
Invite your team to collaborate on this project.
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
<EmptyContent>
|
||||
<Button size="sm">
|
||||
<PlusIcon />
|
||||
Invite Members
|
||||
</Button>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
)
|
||||
}
|
||||
43
apps/v4/app/(app)/(root)/components/empty-input-group.tsx
Normal file
43
apps/v4/app/(app)/(root)/components/empty-input-group.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import { SearchIcon } from "lucide-react"
|
||||
|
||||
import {
|
||||
Empty,
|
||||
EmptyContent,
|
||||
EmptyDescription,
|
||||
EmptyHeader,
|
||||
EmptyTitle,
|
||||
} from "@/registry/new-york-v4/ui/empty"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupInput,
|
||||
} from "@/registry/new-york-v4/ui/input-group"
|
||||
import { Kbd } from "@/registry/new-york-v4/ui/kbd"
|
||||
|
||||
export function EmptyInputGroup() {
|
||||
return (
|
||||
<Empty>
|
||||
<EmptyHeader>
|
||||
<EmptyTitle>404 - Not Found</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
The page you're looking for doesn't exist. Try searching for
|
||||
what you need below.
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
<EmptyContent>
|
||||
<InputGroup className="w-3/4">
|
||||
<InputGroupInput placeholder="Try searching for pages..." />
|
||||
<InputGroupAddon>
|
||||
<SearchIcon />
|
||||
</InputGroupAddon>
|
||||
<InputGroupAddon align="inline-end">
|
||||
<Kbd>/</Kbd>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<EmptyDescription>
|
||||
Need help? <a href="#">Contact support</a>
|
||||
</EmptyDescription>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
)
|
||||
}
|
||||
15
apps/v4/app/(app)/(root)/components/field-checkbox.tsx
Normal file
15
apps/v4/app/(app)/(root)/components/field-checkbox.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Checkbox } from "@/registry/new-york-v4/ui/checkbox"
|
||||
import { Field, FieldLabel } from "@/registry/new-york-v4/ui/field"
|
||||
|
||||
export function FieldCheckbox() {
|
||||
return (
|
||||
<FieldLabel htmlFor="checkbox-demo">
|
||||
<Field orientation="horizontal">
|
||||
<Checkbox id="checkbox-demo" defaultChecked />
|
||||
<FieldLabel htmlFor="checkbox-demo" className="line-clamp-1">
|
||||
I agree to the terms and conditions
|
||||
</FieldLabel>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
)
|
||||
}
|
||||
62
apps/v4/app/(app)/(root)/components/field-choice-card.tsx
Normal file
62
apps/v4/app/(app)/(root)/components/field-choice-card.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import {
|
||||
Field,
|
||||
FieldContent,
|
||||
FieldDescription,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldSet,
|
||||
FieldTitle,
|
||||
} from "@/registry/new-york-v4/ui/field"
|
||||
import {
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
} from "@/registry/new-york-v4/ui/radio-group"
|
||||
|
||||
export function FieldChoiceCard() {
|
||||
return (
|
||||
<div className="w-full max-w-md">
|
||||
<FieldGroup>
|
||||
<FieldSet>
|
||||
<FieldLabel htmlFor="compute-environment-p8w">
|
||||
Compute Environment
|
||||
</FieldLabel>
|
||||
<FieldDescription>
|
||||
Select the compute environment for your cluster.
|
||||
</FieldDescription>
|
||||
<RadioGroup defaultValue="kubernetes">
|
||||
<FieldLabel htmlFor="kubernetes-r2h">
|
||||
<Field orientation="horizontal">
|
||||
<RadioGroupItem
|
||||
value="kubernetes"
|
||||
id="kubernetes-r2h"
|
||||
aria-label="Kubernetes"
|
||||
/>
|
||||
<FieldContent>
|
||||
<FieldTitle>Kubernetes</FieldTitle>
|
||||
<FieldDescription>
|
||||
Run GPU workloads on a K8s configured cluster.
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
<FieldLabel htmlFor="vm-z4k">
|
||||
<Field orientation="horizontal">
|
||||
<RadioGroupItem
|
||||
value="vm"
|
||||
id="vm-z4k"
|
||||
aria-label="Virtual Machine"
|
||||
/>
|
||||
<FieldContent>
|
||||
<FieldTitle>Virtual Machine</FieldTitle>
|
||||
<FieldDescription>
|
||||
Access a VM configured cluster to run workloads.
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
</RadioGroup>
|
||||
</FieldSet>
|
||||
</FieldGroup>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
153
apps/v4/app/(app)/(root)/components/field-demo.tsx
Normal file
153
apps/v4/app/(app)/(root)/components/field-demo.tsx
Normal file
@@ -0,0 +1,153 @@
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import { Checkbox } from "@/registry/new-york-v4/ui/checkbox"
|
||||
import {
|
||||
Field,
|
||||
FieldDescription,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldLegend,
|
||||
FieldSeparator,
|
||||
FieldSet,
|
||||
} from "@/registry/new-york-v4/ui/field"
|
||||
import { Input } from "@/registry/new-york-v4/ui/input"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/registry/new-york-v4/ui/select"
|
||||
import { Textarea } from "@/registry/new-york-v4/ui/textarea"
|
||||
|
||||
export function FieldDemo() {
|
||||
return (
|
||||
<div className="w-full max-w-md rounded-lg border p-6">
|
||||
<form>
|
||||
<FieldGroup>
|
||||
<FieldSet>
|
||||
<FieldLegend>Payment Method</FieldLegend>
|
||||
<FieldDescription>
|
||||
All transactions are secure and encrypted
|
||||
</FieldDescription>
|
||||
<FieldGroup>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="checkout-7j9-card-name-43j">
|
||||
Name on Card
|
||||
</FieldLabel>
|
||||
<Input
|
||||
id="checkout-7j9-card-name-43j"
|
||||
placeholder="John Doe"
|
||||
required
|
||||
/>
|
||||
</Field>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<Field className="col-span-2">
|
||||
<FieldLabel htmlFor="checkout-7j9-card-number-uw1">
|
||||
Card Number
|
||||
</FieldLabel>
|
||||
<Input
|
||||
id="checkout-7j9-card-number-uw1"
|
||||
placeholder="1234 5678 9012 3456"
|
||||
required
|
||||
/>
|
||||
<FieldDescription>
|
||||
Enter your 16-digit number.
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
<Field className="col-span-1">
|
||||
<FieldLabel htmlFor="checkout-7j9-cvv">CVV</FieldLabel>
|
||||
<Input id="checkout-7j9-cvv" placeholder="123" required />
|
||||
</Field>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Field>
|
||||
<FieldLabel htmlFor="checkout-7j9-exp-month-ts6">
|
||||
Month
|
||||
</FieldLabel>
|
||||
<Select defaultValue="">
|
||||
<SelectTrigger id="checkout-7j9-exp-month-ts6">
|
||||
<SelectValue placeholder="MM" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="01">01</SelectItem>
|
||||
<SelectItem value="02">02</SelectItem>
|
||||
<SelectItem value="03">03</SelectItem>
|
||||
<SelectItem value="04">04</SelectItem>
|
||||
<SelectItem value="05">05</SelectItem>
|
||||
<SelectItem value="06">06</SelectItem>
|
||||
<SelectItem value="07">07</SelectItem>
|
||||
<SelectItem value="08">08</SelectItem>
|
||||
<SelectItem value="09">09</SelectItem>
|
||||
<SelectItem value="10">10</SelectItem>
|
||||
<SelectItem value="11">11</SelectItem>
|
||||
<SelectItem value="12">12</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="checkout-7j9-exp-year-f59">
|
||||
Year
|
||||
</FieldLabel>
|
||||
<Select defaultValue="">
|
||||
<SelectTrigger id="checkout-7j9-exp-year-f59">
|
||||
<SelectValue placeholder="YYYY" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="2024">2024</SelectItem>
|
||||
<SelectItem value="2025">2025</SelectItem>
|
||||
<SelectItem value="2026">2026</SelectItem>
|
||||
<SelectItem value="2027">2027</SelectItem>
|
||||
<SelectItem value="2028">2028</SelectItem>
|
||||
<SelectItem value="2029">2029</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
</div>
|
||||
</FieldGroup>
|
||||
</FieldSet>
|
||||
<FieldSeparator />
|
||||
<FieldSet>
|
||||
<FieldLegend>Billing Address</FieldLegend>
|
||||
<FieldDescription>
|
||||
The billing address associated with your payment method
|
||||
</FieldDescription>
|
||||
<FieldGroup>
|
||||
<Field orientation="horizontal">
|
||||
<Checkbox
|
||||
id="checkout-7j9-same-as-shipping-wgm"
|
||||
defaultChecked
|
||||
/>
|
||||
<FieldLabel
|
||||
htmlFor="checkout-7j9-same-as-shipping-wgm"
|
||||
className="font-normal"
|
||||
>
|
||||
Same as shipping address
|
||||
</FieldLabel>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</FieldSet>
|
||||
<FieldSeparator />
|
||||
<FieldSet>
|
||||
<FieldGroup>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="checkout-7j9-optional-comments">
|
||||
Comments
|
||||
</FieldLabel>
|
||||
<Textarea
|
||||
id="checkout-7j9-optional-comments"
|
||||
placeholder="Add any additional comments"
|
||||
/>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</FieldSet>
|
||||
<Field orientation="horizontal">
|
||||
<Button type="submit">Submit</Button>
|
||||
<Button variant="outline" type="button">
|
||||
Cancel
|
||||
</Button>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
72
apps/v4/app/(app)/(root)/components/field-hear.tsx
Normal file
72
apps/v4/app/(app)/(root)/components/field-hear.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
import { Card, CardContent } from "@/registry/new-york-v4/ui/card"
|
||||
import { Checkbox } from "@/registry/new-york-v4/ui/checkbox"
|
||||
import {
|
||||
Field,
|
||||
FieldDescription,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldLegend,
|
||||
FieldSet,
|
||||
FieldTitle,
|
||||
} from "@/registry/new-york-v4/ui/field"
|
||||
|
||||
const options = [
|
||||
{
|
||||
label: "Social Media",
|
||||
value: "social-media",
|
||||
},
|
||||
|
||||
{
|
||||
label: "Search Engine",
|
||||
value: "search-engine",
|
||||
},
|
||||
{
|
||||
label: "Referral",
|
||||
value: "referral",
|
||||
},
|
||||
{
|
||||
label: "Other",
|
||||
value: "other",
|
||||
},
|
||||
]
|
||||
|
||||
export function FieldHear() {
|
||||
return (
|
||||
<Card className="py-4 shadow-none">
|
||||
<CardContent className="px-4">
|
||||
<form>
|
||||
<FieldGroup>
|
||||
<FieldSet className="gap-4">
|
||||
<FieldLegend>How did you hear about us?</FieldLegend>
|
||||
<FieldDescription className="line-clamp-1">
|
||||
Select the option that best describes how you heard about us.
|
||||
</FieldDescription>
|
||||
<FieldGroup className="flex flex-row flex-wrap gap-2 [--radius:9999rem]">
|
||||
{options.map((option) => (
|
||||
<FieldLabel
|
||||
htmlFor={option.value}
|
||||
key={option.value}
|
||||
className="!w-fit"
|
||||
>
|
||||
<Field
|
||||
orientation="horizontal"
|
||||
className="gap-1.5 overflow-hidden !px-3 !py-1.5 transition-all duration-100 ease-linear group-has-data-[state=checked]/field-label:!px-2"
|
||||
>
|
||||
<Checkbox
|
||||
value={option.value}
|
||||
id={option.value}
|
||||
defaultChecked={option.value === "social-media"}
|
||||
className="-ml-6 -translate-x-1 rounded-full transition-all duration-100 ease-linear data-[state=checked]:ml-0 data-[state=checked]:translate-x-0"
|
||||
/>
|
||||
<FieldTitle>{option.label}</FieldTitle>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
))}
|
||||
</FieldGroup>
|
||||
</FieldSet>
|
||||
</FieldGroup>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
35
apps/v4/app/(app)/(root)/components/field-slider.tsx
Normal file
35
apps/v4/app/(app)/(root)/components/field-slider.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
|
||||
import {
|
||||
Field,
|
||||
FieldDescription,
|
||||
FieldTitle,
|
||||
} from "@/registry/new-york-v4/ui/field"
|
||||
import { Slider } from "@/registry/new-york-v4/ui/slider"
|
||||
|
||||
export function FieldSlider() {
|
||||
const [value, setValue] = useState([200, 800])
|
||||
return (
|
||||
<div className="w-full max-w-md">
|
||||
<Field>
|
||||
<FieldTitle>Price Range</FieldTitle>
|
||||
<FieldDescription>
|
||||
Set your budget range ($
|
||||
<span className="font-medium tabular-nums">{value[0]}</span> -{" "}
|
||||
<span className="font-medium tabular-nums">{value[1]}</span>).
|
||||
</FieldDescription>
|
||||
<Slider
|
||||
value={value}
|
||||
onValueChange={setValue}
|
||||
max={1000}
|
||||
min={0}
|
||||
step={10}
|
||||
className="mt-2 w-full"
|
||||
aria-label="Price Range"
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
52
apps/v4/app/(app)/(root)/components/index.tsx
Normal file
52
apps/v4/app/(app)/(root)/components/index.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import { FieldSeparator } from "@/registry/new-york-v4/ui/field"
|
||||
|
||||
import { AppearanceSettings } from "./appearance-settings"
|
||||
import { ButtonGroupDemo } from "./button-group-demo"
|
||||
import { ButtonGroupInputGroup } from "./button-group-input-group"
|
||||
import { ButtonGroupNested } from "./button-group-nested"
|
||||
import { ButtonGroupPopover } from "./button-group-popover"
|
||||
import { EmptyAvatarGroup } from "./empty-avatar-group"
|
||||
import { FieldCheckbox } from "./field-checkbox"
|
||||
import { FieldDemo } from "./field-demo"
|
||||
import { FieldHear } from "./field-hear"
|
||||
import { FieldSlider } from "./field-slider"
|
||||
import { InputGroupButtonExample } from "./input-group-button"
|
||||
import { InputGroupDemo } from "./input-group-demo"
|
||||
import { ItemDemo } from "./item-demo"
|
||||
import { NotionPromptForm } from "./notion-prompt-form"
|
||||
import { SpinnerBadge } from "./spinner-badge"
|
||||
import { SpinnerEmpty } from "./spinner-empty"
|
||||
|
||||
export function RootComponents() {
|
||||
return (
|
||||
<div className="theme-container mx-auto grid gap-8 py-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 xl:gap-6 2xl:gap-8">
|
||||
<div className="flex flex-col gap-6 *:[div]:w-full *:[div]:max-w-full">
|
||||
<FieldDemo />
|
||||
</div>
|
||||
<div className="flex flex-col gap-6 *:[div]:w-full *:[div]:max-w-full">
|
||||
<EmptyAvatarGroup />
|
||||
<SpinnerBadge />
|
||||
<ButtonGroupInputGroup />
|
||||
<FieldSlider />
|
||||
<InputGroupDemo />
|
||||
</div>
|
||||
<div className="flex flex-col gap-6 *:[div]:w-full *:[div]:max-w-full">
|
||||
<InputGroupButtonExample />
|
||||
<ItemDemo />
|
||||
<FieldSeparator>Appearance Settings</FieldSeparator>
|
||||
<AppearanceSettings />
|
||||
</div>
|
||||
<div className="order-first flex flex-col gap-6 lg:hidden xl:order-last xl:flex *:[div]:w-full *:[div]:max-w-full">
|
||||
<NotionPromptForm />
|
||||
<ButtonGroupDemo />
|
||||
<FieldCheckbox />
|
||||
<div className="flex justify-between gap-4">
|
||||
<ButtonGroupNested />
|
||||
<ButtonGroupPopover />
|
||||
</div>
|
||||
<FieldHear />
|
||||
<SpinnerEmpty />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
68
apps/v4/app/(app)/(root)/components/input-group-button.tsx
Normal file
68
apps/v4/app/(app)/(root)/components/input-group-button.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { IconInfoCircle, IconStar } from "@tabler/icons-react"
|
||||
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupInput,
|
||||
} from "@/registry/new-york-v4/ui/input-group"
|
||||
import { Label } from "@/registry/new-york-v4/ui/label"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/registry/new-york-v4/ui/popover"
|
||||
|
||||
export function InputGroupButtonExample() {
|
||||
const [isFavorite, setIsFavorite] = React.useState(false)
|
||||
|
||||
return (
|
||||
<div className="grid w-full max-w-sm gap-6">
|
||||
<Label htmlFor="input-secure-19" className="sr-only">
|
||||
Input Secure
|
||||
</Label>
|
||||
<InputGroup className="[--radius:9999px]">
|
||||
<InputGroupInput id="input-secure-19" className="!pl-0.5" />
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<InputGroupAddon>
|
||||
<InputGroupButton
|
||||
variant="secondary"
|
||||
size="icon-xs"
|
||||
aria-label="Info"
|
||||
>
|
||||
<IconInfoCircle />
|
||||
</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
align="start"
|
||||
alignOffset={10}
|
||||
className="flex flex-col gap-1 rounded-xl text-sm"
|
||||
>
|
||||
<p className="font-medium">Your connection is not secure.</p>
|
||||
<p>You should not enter any sensitive information on this site.</p>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<InputGroupAddon className="text-muted-foreground !pl-1">
|
||||
https://
|
||||
</InputGroupAddon>
|
||||
<InputGroupAddon align="inline-end">
|
||||
<InputGroupButton
|
||||
onClick={() => setIsFavorite(!isFavorite)}
|
||||
size="icon-xs"
|
||||
aria-label="Favorite"
|
||||
>
|
||||
<IconStar
|
||||
data-favorite={isFavorite}
|
||||
className="data-[favorite=true]:fill-primary data-[favorite=true]:stroke-primary"
|
||||
/>
|
||||
</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
102
apps/v4/app/(app)/(root)/components/input-group-demo.tsx
Normal file
102
apps/v4/app/(app)/(root)/components/input-group-demo.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
import { IconCheck, IconInfoCircle, IconPlus } from "@tabler/icons-react"
|
||||
import { ArrowUpIcon, Search } from "lucide-react"
|
||||
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/registry/new-york-v4/ui/dropdown-menu"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupInput,
|
||||
InputGroupText,
|
||||
InputGroupTextarea,
|
||||
} from "@/registry/new-york-v4/ui/input-group"
|
||||
import { Separator } from "@/registry/new-york-v4/ui/separator"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/registry/new-york-v4/ui/tooltip"
|
||||
|
||||
export function InputGroupDemo() {
|
||||
return (
|
||||
<div className="grid w-full max-w-sm gap-6">
|
||||
<InputGroup>
|
||||
<InputGroupInput placeholder="Search..." />
|
||||
<InputGroupAddon>
|
||||
<Search />
|
||||
</InputGroupAddon>
|
||||
<InputGroupAddon align="inline-end">12 results</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<InputGroup>
|
||||
<InputGroupInput placeholder="example.com" className="!pl-1" />
|
||||
<InputGroupAddon>
|
||||
<InputGroupText>https://</InputGroupText>
|
||||
</InputGroupAddon>
|
||||
<InputGroupAddon align="inline-end">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<InputGroupButton
|
||||
className="rounded-full"
|
||||
size="icon-xs"
|
||||
aria-label="Info"
|
||||
>
|
||||
<IconInfoCircle />
|
||||
</InputGroupButton>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>This is content in a tooltip.</TooltipContent>
|
||||
</Tooltip>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<InputGroup>
|
||||
<InputGroupTextarea placeholder="Ask, Search or Chat..." />
|
||||
<InputGroupAddon align="block-end">
|
||||
<InputGroupButton
|
||||
variant="outline"
|
||||
className="rounded-full"
|
||||
size="icon-xs"
|
||||
aria-label="Add"
|
||||
>
|
||||
<IconPlus />
|
||||
</InputGroupButton>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<InputGroupButton variant="ghost">Auto</InputGroupButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
side="top"
|
||||
align="start"
|
||||
className="[--radius:0.95rem]"
|
||||
>
|
||||
<DropdownMenuItem>Auto</DropdownMenuItem>
|
||||
<DropdownMenuItem>Agent</DropdownMenuItem>
|
||||
<DropdownMenuItem>Manual</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<InputGroupText className="ml-auto">52% used</InputGroupText>
|
||||
<Separator orientation="vertical" className="!h-4" />
|
||||
<InputGroupButton
|
||||
variant="default"
|
||||
className="rounded-full"
|
||||
size="icon-xs"
|
||||
>
|
||||
<ArrowUpIcon />
|
||||
<span className="sr-only">Send</span>
|
||||
</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<InputGroup>
|
||||
<InputGroupInput placeholder="@shadcn" />
|
||||
<InputGroupAddon align="inline-end">
|
||||
<div className="bg-primary text-foreground flex size-4 items-center justify-center rounded-full">
|
||||
<IconCheck className="size-3 text-white" />
|
||||
</div>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
46
apps/v4/app/(app)/(root)/components/input-group-textarea.tsx
Normal file
46
apps/v4/app/(app)/(root)/components/input-group-textarea.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import {
|
||||
IconBrandJavascript,
|
||||
IconCopy,
|
||||
IconCornerDownLeft,
|
||||
IconRefresh,
|
||||
} from "@tabler/icons-react"
|
||||
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupText,
|
||||
InputGroupTextarea,
|
||||
} from "@/registry/new-york-v4/ui/input-group"
|
||||
|
||||
export function InputGroupTextareaExample() {
|
||||
return (
|
||||
<div className="grid w-full max-w-md gap-4">
|
||||
<InputGroup>
|
||||
<InputGroupTextarea
|
||||
id="textarea-code-32"
|
||||
placeholder="console.log('Hello, world!');"
|
||||
className="min-h-[180px]"
|
||||
/>
|
||||
<InputGroupAddon align="block-end" className="border-t">
|
||||
<InputGroupText>Line 1, Column 1</InputGroupText>
|
||||
<InputGroupButton size="sm" className="ml-auto" variant="default">
|
||||
Run <IconCornerDownLeft />
|
||||
</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
<InputGroupAddon align="block-start" className="border-b">
|
||||
<InputGroupText className="font-mono font-medium">
|
||||
<IconBrandJavascript />
|
||||
script.js
|
||||
</InputGroupText>
|
||||
<InputGroupButton className="ml-auto">
|
||||
<IconRefresh />
|
||||
</InputGroupButton>
|
||||
<InputGroupButton variant="ghost">
|
||||
<IconCopy />
|
||||
</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
78
apps/v4/app/(app)/(root)/components/item-avatar.tsx
Normal file
78
apps/v4/app/(app)/(root)/components/item-avatar.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
import { Plus } from "lucide-react"
|
||||
|
||||
import {
|
||||
Avatar,
|
||||
AvatarFallback,
|
||||
AvatarImage,
|
||||
} from "@/registry/new-york-v4/ui/avatar"
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import {
|
||||
Item,
|
||||
ItemActions,
|
||||
ItemContent,
|
||||
ItemDescription,
|
||||
ItemMedia,
|
||||
ItemTitle,
|
||||
} from "@/registry/new-york-v4/ui/item"
|
||||
|
||||
export function ItemAvatar() {
|
||||
return (
|
||||
<div className="flex w-full max-w-lg flex-col gap-6">
|
||||
<Item variant="outline" className="hidden">
|
||||
<ItemMedia>
|
||||
<Avatar className="size-10">
|
||||
<AvatarImage src="https://github.com/maxleiter.png" />
|
||||
<AvatarFallback>LR</AvatarFallback>
|
||||
</Avatar>
|
||||
</ItemMedia>
|
||||
<ItemContent>
|
||||
<ItemTitle>Max Leiter</ItemTitle>
|
||||
<ItemDescription>Last seen 5 months ago</ItemDescription>
|
||||
</ItemContent>
|
||||
<ItemActions>
|
||||
<Button
|
||||
size="icon-sm"
|
||||
variant="outline"
|
||||
className="rounded-full"
|
||||
aria-label="Invite"
|
||||
>
|
||||
<Plus />
|
||||
</Button>
|
||||
</ItemActions>
|
||||
</Item>
|
||||
<Item variant="outline">
|
||||
<ItemMedia>
|
||||
<div className="*:data-[slot=avatar]:ring-background flex -space-x-2 *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:grayscale">
|
||||
<Avatar className="hidden sm:flex">
|
||||
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
|
||||
<AvatarFallback>CN</AvatarFallback>
|
||||
</Avatar>
|
||||
<Avatar className="hidden sm:flex">
|
||||
<AvatarImage
|
||||
src="https://github.com/maxleiter.png"
|
||||
alt="@maxleiter"
|
||||
/>
|
||||
<AvatarFallback>LR</AvatarFallback>
|
||||
</Avatar>
|
||||
<Avatar>
|
||||
<AvatarImage
|
||||
src="https://github.com/evilrabbit.png"
|
||||
alt="@evilrabbit"
|
||||
/>
|
||||
<AvatarFallback>ER</AvatarFallback>
|
||||
</Avatar>
|
||||
</div>
|
||||
</ItemMedia>
|
||||
<ItemContent>
|
||||
<ItemTitle>No Team Members</ItemTitle>
|
||||
<ItemDescription>Invite your team to collaborate.</ItemDescription>
|
||||
</ItemContent>
|
||||
<ItemActions>
|
||||
<Button size="sm" variant="outline">
|
||||
Invite
|
||||
</Button>
|
||||
</ItemActions>
|
||||
</Item>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
42
apps/v4/app/(app)/(root)/components/item-demo.tsx
Normal file
42
apps/v4/app/(app)/(root)/components/item-demo.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import { BadgeCheckIcon, ChevronRightIcon } from "lucide-react"
|
||||
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import {
|
||||
Item,
|
||||
ItemActions,
|
||||
ItemContent,
|
||||
ItemDescription,
|
||||
ItemMedia,
|
||||
ItemTitle,
|
||||
} from "@/registry/new-york-v4/ui/item"
|
||||
|
||||
export function ItemDemo() {
|
||||
return (
|
||||
<div className="flex w-full max-w-md flex-col gap-6">
|
||||
<Item variant="outline">
|
||||
<ItemContent>
|
||||
<ItemTitle>Two-factor authentication</ItemTitle>
|
||||
<ItemDescription className="text-pretty xl:hidden 2xl:block">
|
||||
Verify via email or phone number.
|
||||
</ItemDescription>
|
||||
</ItemContent>
|
||||
<ItemActions>
|
||||
<Button size="sm">Enable</Button>
|
||||
</ItemActions>
|
||||
</Item>
|
||||
<Item variant="outline" size="sm" asChild>
|
||||
<a href="#">
|
||||
<ItemMedia>
|
||||
<BadgeCheckIcon className="size-5" />
|
||||
</ItemMedia>
|
||||
<ItemContent>
|
||||
<ItemTitle>Your profile has been verified.</ItemTitle>
|
||||
</ItemContent>
|
||||
<ItemActions>
|
||||
<ChevronRightIcon className="size-4" />
|
||||
</ItemActions>
|
||||
</a>
|
||||
</Item>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
456
apps/v4/app/(app)/(root)/components/notion-prompt-form.tsx
Normal file
456
apps/v4/app/(app)/(root)/components/notion-prompt-form.tsx
Normal file
@@ -0,0 +1,456 @@
|
||||
"use client"
|
||||
|
||||
import { useMemo, useState } from "react"
|
||||
import {
|
||||
IconApps,
|
||||
IconArrowUp,
|
||||
IconAt,
|
||||
IconBook,
|
||||
IconCircleDashedPlus,
|
||||
IconPaperclip,
|
||||
IconPlus,
|
||||
IconWorld,
|
||||
IconX,
|
||||
} from "@tabler/icons-react"
|
||||
|
||||
import {
|
||||
Avatar,
|
||||
AvatarFallback,
|
||||
AvatarImage,
|
||||
} from "@/registry/new-york-v4/ui/avatar"
|
||||
import { Badge } from "@/registry/new-york-v4/ui/badge"
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@/registry/new-york-v4/ui/command"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/registry/new-york-v4/ui/dropdown-menu"
|
||||
import { Field, FieldLabel } from "@/registry/new-york-v4/ui/field"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupTextarea,
|
||||
} from "@/registry/new-york-v4/ui/input-group"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/registry/new-york-v4/ui/popover"
|
||||
import { Switch } from "@/registry/new-york-v4/ui/switch"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/registry/new-york-v4/ui/tooltip"
|
||||
|
||||
const SAMPLE_DATA = {
|
||||
mentionable: [
|
||||
{
|
||||
type: "page",
|
||||
title: "Meeting Notes",
|
||||
image: "📝",
|
||||
},
|
||||
{
|
||||
type: "page",
|
||||
title: "Project Dashboard",
|
||||
image: "📊",
|
||||
},
|
||||
{
|
||||
type: "page",
|
||||
title: "Ideas & Brainstorming",
|
||||
image: "💡",
|
||||
},
|
||||
{
|
||||
type: "page",
|
||||
title: "Calendar & Events",
|
||||
image: "📅",
|
||||
},
|
||||
{
|
||||
type: "page",
|
||||
title: "Documentation",
|
||||
image: "📚",
|
||||
},
|
||||
{
|
||||
type: "page",
|
||||
title: "Goals & Objectives",
|
||||
image: "🎯",
|
||||
},
|
||||
{
|
||||
type: "page",
|
||||
title: "Budget Planning",
|
||||
image: "💰",
|
||||
},
|
||||
{
|
||||
type: "page",
|
||||
title: "Team Directory",
|
||||
image: "👥",
|
||||
},
|
||||
{
|
||||
type: "page",
|
||||
title: "Technical Specs",
|
||||
image: "🔧",
|
||||
},
|
||||
{
|
||||
type: "page",
|
||||
title: "Analytics Report",
|
||||
image: "📈",
|
||||
},
|
||||
{
|
||||
type: "user",
|
||||
title: "shadcn",
|
||||
image: "https://github.com/shadcn.png",
|
||||
workspace: "Workspace",
|
||||
},
|
||||
{
|
||||
type: "user",
|
||||
title: "maxleiter",
|
||||
image: "https://github.com/maxleiter.png",
|
||||
workspace: "Workspace",
|
||||
},
|
||||
{
|
||||
type: "user",
|
||||
title: "evilrabbit",
|
||||
image: "https://github.com/evilrabbit.png",
|
||||
workspace: "Workspace",
|
||||
},
|
||||
],
|
||||
models: [
|
||||
{
|
||||
name: "Auto",
|
||||
},
|
||||
{
|
||||
name: "Agent Mode",
|
||||
badge: "Beta",
|
||||
},
|
||||
{
|
||||
name: "Plan Mode",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
function MentionableIcon({
|
||||
item,
|
||||
}: {
|
||||
item: (typeof SAMPLE_DATA.mentionable)[0]
|
||||
}) {
|
||||
return item.type === "page" ? (
|
||||
<span className="flex size-4 items-center justify-center">
|
||||
{item.image}
|
||||
</span>
|
||||
) : (
|
||||
<Avatar className="size-4">
|
||||
<AvatarImage src={item.image} />
|
||||
<AvatarFallback>{item.title[0]}</AvatarFallback>
|
||||
</Avatar>
|
||||
)
|
||||
}
|
||||
|
||||
export function NotionPromptForm() {
|
||||
const [mentions, setMentions] = useState<string[]>([])
|
||||
const [mentionPopoverOpen, setMentionPopoverOpen] = useState(false)
|
||||
const [modelPopoverOpen, setModelPopoverOpen] = useState(false)
|
||||
const [selectedModel, setSelectedModel] = useState<
|
||||
(typeof SAMPLE_DATA.models)[0]
|
||||
>(SAMPLE_DATA.models[0])
|
||||
const [scopeMenuOpen, setScopeMenuOpen] = useState(false)
|
||||
|
||||
const grouped = useMemo(() => {
|
||||
return SAMPLE_DATA.mentionable.reduce(
|
||||
(acc, item) => {
|
||||
const isAvailable = !mentions.includes(item.title)
|
||||
|
||||
if (isAvailable) {
|
||||
if (!acc[item.type]) {
|
||||
acc[item.type] = []
|
||||
}
|
||||
acc[item.type].push(item)
|
||||
}
|
||||
return acc
|
||||
},
|
||||
{} as Record<string, typeof SAMPLE_DATA.mentionable>
|
||||
)
|
||||
}, [mentions])
|
||||
|
||||
const hasMentions = mentions.length > 0
|
||||
|
||||
return (
|
||||
<form className="[--radius:1.2rem]">
|
||||
<Field>
|
||||
<FieldLabel htmlFor="notion-prompt" className="sr-only">
|
||||
Prompt
|
||||
</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupTextarea
|
||||
id="notion-prompt"
|
||||
placeholder="Ask, search, or make anything..."
|
||||
/>
|
||||
<InputGroupAddon align="block-start">
|
||||
<Popover
|
||||
open={mentionPopoverOpen}
|
||||
onOpenChange={setMentionPopoverOpen}
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
asChild
|
||||
onFocusCapture={(e) => e.stopPropagation()}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<InputGroupButton
|
||||
variant="outline"
|
||||
size={!hasMentions ? "sm" : "icon-sm"}
|
||||
className="rounded-full transition-transform"
|
||||
>
|
||||
<IconAt /> {!hasMentions && "Add context"}
|
||||
</InputGroupButton>
|
||||
</PopoverTrigger>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Mention a person, page, or date</TooltipContent>
|
||||
</Tooltip>
|
||||
<PopoverContent className="p-0 [--radius:1.2rem]" align="start">
|
||||
<Command>
|
||||
<CommandInput placeholder="Search pages..." />
|
||||
<CommandList>
|
||||
<CommandEmpty>No pages found</CommandEmpty>
|
||||
{Object.entries(grouped).map(([type, items]) => (
|
||||
<CommandGroup
|
||||
key={type}
|
||||
heading={type === "page" ? "Pages" : "Users"}
|
||||
>
|
||||
{items.map((item) => (
|
||||
<CommandItem
|
||||
key={item.title}
|
||||
value={item.title}
|
||||
onSelect={(currentValue) => {
|
||||
setMentions((prev) => [...prev, currentValue])
|
||||
setMentionPopoverOpen(false)
|
||||
}}
|
||||
>
|
||||
<MentionableIcon item={item} />
|
||||
{item.title}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
))}
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<div className="no-scrollbar -m-1.5 flex gap-1 overflow-y-auto p-1.5">
|
||||
{mentions.map((mention) => {
|
||||
const item = SAMPLE_DATA.mentionable.find(
|
||||
(item) => item.title === mention
|
||||
)
|
||||
|
||||
if (!item) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<InputGroupButton
|
||||
key={mention}
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
className="rounded-full !pl-2"
|
||||
onClick={() => {
|
||||
setMentions((prev) => prev.filter((m) => m !== mention))
|
||||
}}
|
||||
>
|
||||
<MentionableIcon item={item} />
|
||||
{item.title}
|
||||
<IconX />
|
||||
</InputGroupButton>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</InputGroupAddon>
|
||||
<InputGroupAddon align="block-end" className="gap-1">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<InputGroupButton
|
||||
size="icon-sm"
|
||||
className="rounded-full"
|
||||
aria-label="Attach file"
|
||||
>
|
||||
<IconPaperclip />
|
||||
</InputGroupButton>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Attach file</TooltipContent>
|
||||
</Tooltip>
|
||||
<DropdownMenu
|
||||
open={modelPopoverOpen}
|
||||
onOpenChange={setModelPopoverOpen}
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<InputGroupButton size="sm" className="rounded-full">
|
||||
{selectedModel.name}
|
||||
</InputGroupButton>
|
||||
</DropdownMenuTrigger>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Select AI model</TooltipContent>
|
||||
</Tooltip>
|
||||
<DropdownMenuContent
|
||||
side="top"
|
||||
align="start"
|
||||
className="[--radius:1rem]"
|
||||
>
|
||||
<DropdownMenuGroup className="w-42">
|
||||
<DropdownMenuLabel className="text-muted-foreground text-xs">
|
||||
Select Agent Mode
|
||||
</DropdownMenuLabel>
|
||||
{SAMPLE_DATA.models.map((model) => (
|
||||
<DropdownMenuCheckboxItem
|
||||
key={model.name}
|
||||
checked={model.name === selectedModel.name}
|
||||
onCheckedChange={(checked) => {
|
||||
if (checked) {
|
||||
setSelectedModel(model)
|
||||
}
|
||||
}}
|
||||
className="pl-2 *:[span:first-child]:right-2 *:[span:first-child]:left-auto"
|
||||
>
|
||||
{model.name}
|
||||
{model.badge && (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="h-5 rounded-sm bg-blue-100 px-1 text-xs text-blue-800 dark:bg-blue-900 dark:text-blue-100"
|
||||
>
|
||||
{model.badge}
|
||||
</Badge>
|
||||
)}
|
||||
</DropdownMenuCheckboxItem>
|
||||
))}
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<DropdownMenu open={scopeMenuOpen} onOpenChange={setScopeMenuOpen}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<InputGroupButton size="sm" className="rounded-full">
|
||||
<IconWorld /> All Sources
|
||||
</InputGroupButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
side="top"
|
||||
align="end"
|
||||
className="[--radius:1rem]"
|
||||
>
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem
|
||||
asChild
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
<label htmlFor="web-search">
|
||||
<IconWorld /> Web Search{" "}
|
||||
<Switch
|
||||
id="web-search"
|
||||
className="ml-auto"
|
||||
defaultChecked
|
||||
/>
|
||||
</label>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem
|
||||
asChild
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
<label htmlFor="apps">
|
||||
<IconApps /> Apps and Integrations
|
||||
<Switch id="apps" className="ml-auto" defaultChecked />
|
||||
</label>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<IconCircleDashedPlus /> All Sources I can access
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger>
|
||||
<Avatar className="size-4">
|
||||
<AvatarImage src="https://github.com/shadcn.png" />
|
||||
<AvatarFallback>CN</AvatarFallback>
|
||||
</Avatar>
|
||||
shadcn
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuSubContent className="w-72 p-0 [--radius:1rem]">
|
||||
<Command>
|
||||
<CommandInput
|
||||
placeholder="Find or use knowledge in..."
|
||||
autoFocus
|
||||
/>
|
||||
<CommandList>
|
||||
<CommandEmpty>No knowledge found</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{SAMPLE_DATA.mentionable
|
||||
.filter((item) => item.type === "user")
|
||||
.map((user) => (
|
||||
<CommandItem
|
||||
key={user.title}
|
||||
value={user.title}
|
||||
onSelect={() => {
|
||||
// Handle user selection here
|
||||
console.log("Selected user:", user.title)
|
||||
}}
|
||||
>
|
||||
<Avatar className="size-4">
|
||||
<AvatarImage src={user.image} />
|
||||
<AvatarFallback>
|
||||
{user.title[0]}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
{user.title}{" "}
|
||||
<span className="text-muted-foreground">
|
||||
- {user.workspace}
|
||||
</span>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuSub>
|
||||
<DropdownMenuItem>
|
||||
<IconBook /> Help Center
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>
|
||||
<IconPlus /> Connect Apps
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuLabel className="text-muted-foreground text-xs">
|
||||
We'll only search in the sources selected here.
|
||||
</DropdownMenuLabel>
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<InputGroupButton
|
||||
aria-label="Send"
|
||||
className="ml-auto rounded-full"
|
||||
variant="default"
|
||||
size="icon-sm"
|
||||
>
|
||||
<IconArrowUp />
|
||||
</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</Field>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
21
apps/v4/app/(app)/(root)/components/spinner-badge.tsx
Normal file
21
apps/v4/app/(app)/(root)/components/spinner-badge.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import { Badge } from "@/registry/new-york-v4/ui/badge"
|
||||
import { Spinner } from "@/registry/new-york-v4/ui/spinner"
|
||||
|
||||
export function SpinnerBadge() {
|
||||
return (
|
||||
<div className="flex items-center gap-2 [--radius:1.2rem]">
|
||||
<Badge>
|
||||
<Spinner />
|
||||
Syncing
|
||||
</Badge>
|
||||
<Badge variant="secondary">
|
||||
<Spinner />
|
||||
Updating
|
||||
</Badge>
|
||||
<Badge variant="outline">
|
||||
<Spinner />
|
||||
Loading
|
||||
</Badge>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
31
apps/v4/app/(app)/(root)/components/spinner-empty.tsx
Normal file
31
apps/v4/app/(app)/(root)/components/spinner-empty.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import {
|
||||
Empty,
|
||||
EmptyContent,
|
||||
EmptyDescription,
|
||||
EmptyHeader,
|
||||
EmptyMedia,
|
||||
EmptyTitle,
|
||||
} from "@/registry/new-york-v4/ui/empty"
|
||||
import { Spinner } from "@/registry/new-york-v4/ui/spinner"
|
||||
|
||||
export function SpinnerEmpty() {
|
||||
return (
|
||||
<Empty className="w-full border md:p-6">
|
||||
<EmptyHeader>
|
||||
<EmptyMedia variant="icon">
|
||||
<Spinner />
|
||||
</EmptyMedia>
|
||||
<EmptyTitle>Processing your request</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
Please wait while we process your request. Do not refresh the page.
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
<EmptyContent>
|
||||
<Button variant="outline" size="sm">
|
||||
Cancel
|
||||
</Button>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
)
|
||||
}
|
||||
@@ -3,7 +3,6 @@ import Image from "next/image"
|
||||
import Link from "next/link"
|
||||
|
||||
import { Announcement } from "@/components/announcement"
|
||||
import { CardsDemo } from "@/components/cards"
|
||||
import { ExamplesNav } from "@/components/examples-nav"
|
||||
import {
|
||||
PageActions,
|
||||
@@ -15,6 +14,8 @@ import { PageNav } from "@/components/page-nav"
|
||||
import { ThemeSelector } from "@/components/theme-selector"
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
|
||||
import { RootComponents } from "./components"
|
||||
|
||||
const title = "The Foundation for your Design System"
|
||||
const description =
|
||||
"A set of beautifully designed components that you can customize, extend, and build on. Start here then make it your own. Open Source. Open Code."
|
||||
@@ -87,7 +88,7 @@ export default function IndexPage() {
|
||||
/>
|
||||
</section>
|
||||
<section className="theme-container hidden md:block">
|
||||
<CardsDemo />
|
||||
<RootComponents />
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -144,19 +144,19 @@ export default async function Page(props: {
|
||||
)}
|
||||
</div>
|
||||
{links ? (
|
||||
<div className="flex items-center space-x-2 pt-4">
|
||||
<div className="flex items-center gap-2 pt-4">
|
||||
{links?.doc && (
|
||||
<Badge asChild variant="secondary">
|
||||
<Link href={links.doc} target="_blank" rel="noreferrer">
|
||||
<Badge asChild variant="secondary" className="rounded-full">
|
||||
<a href={links.doc} target="_blank" rel="noreferrer">
|
||||
Docs <IconArrowUpRight />
|
||||
</Link>
|
||||
</a>
|
||||
</Badge>
|
||||
)}
|
||||
{links?.api && (
|
||||
<Badge asChild variant="secondary">
|
||||
<Link href={links.api} target="_blank" rel="noreferrer">
|
||||
<Badge asChild variant="secondary" className="rounded-full">
|
||||
<a href={links.api} target="_blank" rel="noreferrer">
|
||||
API Reference <IconArrowUpRight />
|
||||
</Link>
|
||||
</a>
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
@@ -193,7 +193,7 @@ export default async function Page(props: {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="sticky top-[calc(var(--header-height)+1px)] z-30 ml-auto hidden h-[calc(100svh-var(--header-height)-var(--footer-height))] w-72 flex-col gap-4 overflow-hidden overscroll-none pb-8 xl:flex">
|
||||
<div className="sticky top-[calc(var(--header-height)+1px)] z-30 ml-auto hidden h-[calc(100svh-var(--footer-height)+2rem)] w-72 flex-col gap-4 overflow-hidden overscroll-none pb-8 xl:flex">
|
||||
<div className="h-(--top-spacing) shrink-0" />
|
||||
{/* @ts-expect-error - revisit fumadocs types. */}
|
||||
{doc.toc?.length ? (
|
||||
|
||||
@@ -5,8 +5,14 @@ import * as React from "react"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Icons } from "@/components/icons"
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import {
|
||||
Field,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldSeparator,
|
||||
} from "@/registry/new-york-v4/ui/field"
|
||||
import { Input } from "@/registry/new-york-v4/ui/input"
|
||||
import { Label } from "@/registry/new-york-v4/ui/label"
|
||||
import { Spinner } from "@/registry/new-york-v4/ui/spinner"
|
||||
|
||||
export function UserAuthForm({
|
||||
className,
|
||||
@@ -26,11 +32,11 @@ export function UserAuthForm({
|
||||
return (
|
||||
<div className={cn("grid gap-6", className)} {...props}>
|
||||
<form onSubmit={onSubmit}>
|
||||
<div className="grid gap-2">
|
||||
<div className="grid gap-1">
|
||||
<Label className="sr-only" htmlFor="email">
|
||||
<FieldGroup>
|
||||
<Field>
|
||||
<FieldLabel className="sr-only" htmlFor="email">
|
||||
Email
|
||||
</Label>
|
||||
</FieldLabel>
|
||||
<Input
|
||||
id="email"
|
||||
placeholder="name@example.com"
|
||||
@@ -40,31 +46,18 @@ export function UserAuthForm({
|
||||
autoCorrect="off"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</div>
|
||||
<Button disabled={isLoading}>
|
||||
{isLoading && (
|
||||
<Icons.spinner className="mr-2 h-4 w-4 animate-spin" />
|
||||
)}
|
||||
Sign In with Email
|
||||
</Button>
|
||||
</div>
|
||||
</Field>
|
||||
<Field>
|
||||
<Button disabled={isLoading}>
|
||||
{isLoading && <Spinner />}
|
||||
Sign In with Email
|
||||
</Button>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</form>
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
<span className="w-full border-t" />
|
||||
</div>
|
||||
<div className="relative flex justify-center text-xs uppercase">
|
||||
<span className="bg-background text-muted-foreground px-2">
|
||||
Or continue with
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<FieldSeparator>Or continue with</FieldSeparator>
|
||||
<Button variant="outline" type="button" disabled={isLoading}>
|
||||
{isLoading ? (
|
||||
<Icons.spinner className="mr-2 h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Icons.gitHub className="mr-2 h-4 w-4" />
|
||||
)}{" "}
|
||||
{isLoading ? <Spinner /> : <Icons.gitHub className="mr-2 h-4 w-4" />}{" "}
|
||||
GitHub
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,7 @@ import Link from "next/link"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { buttonVariants } from "@/registry/new-york-v4/ui/button"
|
||||
import { FieldDescription } from "@/registry/new-york-v4/ui/field"
|
||||
import { UserAuthForm } from "@/app/(app)/examples/authentication/components/user-auth-form"
|
||||
|
||||
export const metadata: Metadata = {
|
||||
@@ -78,23 +79,11 @@ export default function AuthenticationPage() {
|
||||
</p>
|
||||
</div>
|
||||
<UserAuthForm />
|
||||
<p className="text-muted-foreground px-8 text-center text-sm">
|
||||
<FieldDescription className="px-6 text-center">
|
||||
By clicking continue, you agree to our{" "}
|
||||
<Link
|
||||
href="/terms"
|
||||
className="hover:text-primary underline underline-offset-4"
|
||||
>
|
||||
Terms of Service
|
||||
</Link>{" "}
|
||||
and{" "}
|
||||
<Link
|
||||
href="/privacy"
|
||||
className="hover:text-primary underline underline-offset-4"
|
||||
>
|
||||
Privacy Policy
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
<Link href="/terms">Terms of Service</Link> and{" "}
|
||||
<Link href="/privacy">Privacy Policy</Link>.
|
||||
</FieldDescription>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -142,13 +142,7 @@ const chartConfig = {
|
||||
|
||||
export function ChartAreaInteractive() {
|
||||
const isMobile = useIsMobile()
|
||||
const [timeRange, setTimeRange] = React.useState("90d")
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isMobile) {
|
||||
setTimeRange("7d")
|
||||
}
|
||||
}, [isMobile])
|
||||
const [timeRange, setTimeRange] = React.useState("7d")
|
||||
|
||||
const filteredData = chartData.filter((item) => {
|
||||
const date = new Date(item.date)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { notFound } from "next/navigation"
|
||||
import { NextResponse, type NextRequest } from "next/server"
|
||||
|
||||
import { processMdxForLLMs } from "@/lib/llm"
|
||||
import { source } from "@/lib/source"
|
||||
|
||||
export const revalidate = false
|
||||
@@ -17,7 +18,9 @@ export async function GET(
|
||||
}
|
||||
|
||||
// @ts-expect-error - revisit fumadocs types.
|
||||
return new NextResponse(page.data.content, {
|
||||
const processedContent = processMdxForLLMs(page.data.content)
|
||||
|
||||
return new NextResponse(processedContent, {
|
||||
headers: {
|
||||
"Content-Type": "text/markdown; charset=utf-8",
|
||||
},
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
import Image from "next/image"
|
||||
import { CheckIcon } from "lucide-react"
|
||||
|
||||
import {
|
||||
Field,
|
||||
FieldContent,
|
||||
FieldDescription,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldLegend,
|
||||
FieldSeparator,
|
||||
FieldSet,
|
||||
FieldTitle,
|
||||
} from "@/registry/new-york-v4/ui/field"
|
||||
import { Label } from "@/registry/new-york-v4/ui/label"
|
||||
import {
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
} from "@/registry/new-york-v4/ui/radio-group"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/registry/new-york-v4/ui/select"
|
||||
import { Switch } from "@/registry/new-york-v4/ui/switch"
|
||||
|
||||
const modes = [
|
||||
{
|
||||
name: "Light",
|
||||
value: "light",
|
||||
image: "/placeholder.svg",
|
||||
},
|
||||
{
|
||||
name: "Dark",
|
||||
value: "dark",
|
||||
image: "/placeholder.svg",
|
||||
},
|
||||
{
|
||||
name: "System",
|
||||
value: "system",
|
||||
image: "/placeholder.svg",
|
||||
},
|
||||
]
|
||||
|
||||
const accents = [
|
||||
{
|
||||
name: "Blue",
|
||||
value: "#007AFF",
|
||||
},
|
||||
{
|
||||
name: "Purple",
|
||||
value: "#6A4695",
|
||||
},
|
||||
{
|
||||
name: "Red",
|
||||
value: "#FF3B30",
|
||||
},
|
||||
{
|
||||
name: "Orange",
|
||||
value: "#FF9500",
|
||||
},
|
||||
]
|
||||
|
||||
export function AppearanceSettings() {
|
||||
return (
|
||||
<FieldSet>
|
||||
<FieldLegend>Appearance</FieldLegend>
|
||||
<FieldDescription>
|
||||
Configure appearance. accent, scroll bar, and more.
|
||||
</FieldDescription>
|
||||
<FieldGroup>
|
||||
<FieldSet>
|
||||
<FieldLegend variant="label">Mode</FieldLegend>
|
||||
<FieldDescription>
|
||||
Select the mode to use for the appearance.
|
||||
</FieldDescription>
|
||||
<RadioGroup
|
||||
className="flex flex-col gap-4 @min-[28rem]/field-group:grid @min-[28rem]/field-group:grid-cols-3"
|
||||
defaultValue="light"
|
||||
>
|
||||
{modes.map((mode) => (
|
||||
<FieldLabel
|
||||
htmlFor={mode.value}
|
||||
className="gap-0 overflow-hidden"
|
||||
key={mode.value}
|
||||
>
|
||||
<Image
|
||||
src={mode.image}
|
||||
alt={mode.name}
|
||||
width={160}
|
||||
height={90}
|
||||
className="hidden aspect-video w-full object-cover @min-[28rem]/field-group:block dark:brightness-[0.2] dark:grayscale"
|
||||
/>
|
||||
<Field
|
||||
orientation="horizontal"
|
||||
className="@min-[28rem]/field-group:border-t-input @min-[28rem]/field-group:border-t"
|
||||
>
|
||||
<FieldTitle>{mode.name}</FieldTitle>
|
||||
<RadioGroupItem id={mode.value} value={mode.value} />
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</FieldSet>
|
||||
<FieldSeparator />
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldTitle>Accent</FieldTitle>
|
||||
<FieldDescription>
|
||||
Select the accent color to use for the appearance.
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<FieldSet aria-label="Accent">
|
||||
<RadioGroup className="flex flex-wrap gap-2" defaultValue="#007AFF">
|
||||
{accents.map((accent) => (
|
||||
<Label
|
||||
htmlFor={accent.value}
|
||||
key={accent.value}
|
||||
className="flex size-6 items-center justify-center rounded-full"
|
||||
style={{ backgroundColor: accent.value }}
|
||||
>
|
||||
<RadioGroupItem
|
||||
id={accent.value}
|
||||
value={accent.value}
|
||||
aria-label={accent.name}
|
||||
className="peer sr-only"
|
||||
/>
|
||||
<CheckIcon className="hidden size-4 stroke-white peer-data-[state=checked]:block" />
|
||||
</Label>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</FieldSet>
|
||||
</Field>
|
||||
<FieldSeparator />
|
||||
<Field orientation="responsive">
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor="icon-size">Sidebar Icon Size</FieldLabel>
|
||||
<FieldDescription>
|
||||
Select the size of the sidebar icons.
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<Select>
|
||||
<SelectTrigger id="icon-size" className="ml-auto">
|
||||
<SelectValue placeholder="Select" />
|
||||
</SelectTrigger>
|
||||
<SelectContent align="end">
|
||||
<SelectItem value="small">Small</SelectItem>
|
||||
<SelectItem value="medium">Medium</SelectItem>
|
||||
<SelectItem value="large">Large</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
<FieldSeparator />
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor="tinting">Wallpaper Tinting</FieldLabel>
|
||||
<FieldDescription>
|
||||
Allow the wallpaper to be tinted with the accent color.
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<Switch id="tinting" defaultChecked />
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</FieldSet>
|
||||
)
|
||||
}
|
||||
463
apps/v4/app/(internal)/sink/(pages)/forms/chat-settings.tsx
Normal file
463
apps/v4/app/(internal)/sink/(pages)/forms/chat-settings.tsx
Normal file
@@ -0,0 +1,463 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { CircleIcon, InfoIcon } from "lucide-react"
|
||||
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import { Checkbox } from "@/registry/new-york-v4/ui/checkbox"
|
||||
import {
|
||||
Field,
|
||||
FieldContent,
|
||||
FieldDescription,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldSeparator,
|
||||
FieldSet,
|
||||
FieldTitle,
|
||||
} from "@/registry/new-york-v4/ui/field"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupInput,
|
||||
} from "@/registry/new-york-v4/ui/input-group"
|
||||
import { Kbd } from "@/registry/new-york-v4/ui/kbd"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectSeparator,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/registry/new-york-v4/ui/select"
|
||||
import { Switch } from "@/registry/new-york-v4/ui/switch"
|
||||
import {
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from "@/registry/new-york-v4/ui/tabs"
|
||||
import { Textarea } from "@/registry/new-york-v4/ui/textarea"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/registry/new-york-v4/ui/tooltip"
|
||||
|
||||
const spokenLanguages = [
|
||||
{ label: "English", value: "en" },
|
||||
{ label: "Spanish", value: "es" },
|
||||
{ label: "French", value: "fr" },
|
||||
{ label: "German", value: "de" },
|
||||
{ label: "Italian", value: "it" },
|
||||
{ label: "Portuguese", value: "pt" },
|
||||
{ label: "Russian", value: "ru" },
|
||||
{ label: "Chinese", value: "zh" },
|
||||
{ label: "Japanese", value: "ja" },
|
||||
{ label: "Korean", value: "ko" },
|
||||
{ label: "Arabic", value: "ar" },
|
||||
{ label: "Hindi", value: "hi" },
|
||||
{ label: "Bengali", value: "bn" },
|
||||
{ label: "Telugu", value: "te" },
|
||||
{ label: "Marathi", value: "mr" },
|
||||
{ label: "Kannada", value: "kn" },
|
||||
{ label: "Malayalam", value: "ml" },
|
||||
]
|
||||
|
||||
const voices = [
|
||||
{ label: "Samantha", value: "samantha" },
|
||||
{ label: "Alex", value: "alex" },
|
||||
{ label: "Fred", value: "fred" },
|
||||
{ label: "Victoria", value: "victoria" },
|
||||
{ label: "Tom", value: "tom" },
|
||||
{ label: "Karen", value: "karen" },
|
||||
{ label: "Sam", value: "sam" },
|
||||
{ label: "Daniel", value: "daniel" },
|
||||
]
|
||||
|
||||
const personalities = [
|
||||
{
|
||||
label: "Friendly",
|
||||
value: "friendly",
|
||||
description: "Friendly and approachable.",
|
||||
},
|
||||
{
|
||||
label: "Professional",
|
||||
value: "professional",
|
||||
description: "Professional and authoritative.",
|
||||
},
|
||||
{ label: "Funny", value: "funny", description: "Funny and light-hearted." },
|
||||
{
|
||||
label: "Sarcastic",
|
||||
value: "sarcastic",
|
||||
description: "Sarcastic and witty.",
|
||||
},
|
||||
{ label: "Cynical", value: "cynical", description: "Cynical and skeptical." },
|
||||
]
|
||||
|
||||
const instructions = [
|
||||
{
|
||||
label: "Witty",
|
||||
value: "witty",
|
||||
description: "Use quick and clever responses when appropriate.",
|
||||
},
|
||||
{
|
||||
label: "Professional",
|
||||
value: "professional",
|
||||
description: "Have a professional and authoritative tone.",
|
||||
},
|
||||
{
|
||||
label: "Funny",
|
||||
value: "funny",
|
||||
description: "Use humor and wit to engage the user.",
|
||||
},
|
||||
{
|
||||
label: "Sarcastic",
|
||||
value: "sarcastic",
|
||||
description: "Use sarcasm and wit to engage the user.",
|
||||
},
|
||||
{
|
||||
label: "Cynical",
|
||||
value: "cynical",
|
||||
description: "Use cynicism and skepticism to engage the user.",
|
||||
},
|
||||
]
|
||||
|
||||
export function ChatSettings() {
|
||||
const [tab, setTab] = useState("general")
|
||||
const [theme, setTheme] = useState("system")
|
||||
const [accentColor, setAccentColor] = useState("default")
|
||||
const [spokenLanguage, setSpokenLanguage] = useState("en")
|
||||
const [voice, setVoice] = useState("samantha")
|
||||
const [personality, setPersonality] = useState("friendly")
|
||||
const [customInstructions, setCustomInstructions] = useState("")
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<Button variant="outline" asChild className="w-full md:hidden">
|
||||
<select
|
||||
value={tab}
|
||||
onChange={(e) => setTab(e.target.value)}
|
||||
className="appearance-none"
|
||||
>
|
||||
<option value="general">General</option>
|
||||
<option value="notifications">Notifications</option>
|
||||
<option value="personalization">Personalization</option>
|
||||
<option value="security">Security</option>
|
||||
</select>
|
||||
</Button>
|
||||
<Tabs value={tab} onValueChange={setTab}>
|
||||
<TabsList className="hidden md:flex">
|
||||
<TabsTrigger value="general">General</TabsTrigger>
|
||||
<TabsTrigger value="notifications">Notifications</TabsTrigger>
|
||||
<TabsTrigger value="personalization">Personalization</TabsTrigger>
|
||||
<TabsTrigger value="security">Security</TabsTrigger>
|
||||
</TabsList>
|
||||
<div className="rounded-lg border p-6 [&_[data-slot=select-trigger]]:min-w-[125px]">
|
||||
<TabsContent value="general">
|
||||
<FieldSet>
|
||||
<FieldGroup>
|
||||
<Field orientation="horizontal">
|
||||
<FieldLabel htmlFor="theme">Theme</FieldLabel>
|
||||
<Select value={theme} onValueChange={setTheme}>
|
||||
<SelectTrigger id="theme">
|
||||
<SelectValue placeholder="Select" />
|
||||
</SelectTrigger>
|
||||
<SelectContent align="end">
|
||||
<SelectItem value="light">Light</SelectItem>
|
||||
<SelectItem value="dark">Dark</SelectItem>
|
||||
<SelectItem value="system">System</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
<FieldSeparator />
|
||||
<Field orientation="horizontal">
|
||||
<FieldLabel htmlFor="accent-color">Accent Color</FieldLabel>
|
||||
<Select value={accentColor} onValueChange={setAccentColor}>
|
||||
<SelectTrigger id="accent-color">
|
||||
<SelectValue placeholder="Select" />
|
||||
</SelectTrigger>
|
||||
<SelectContent align="end">
|
||||
<SelectItem value="default">
|
||||
<CircleIcon className="fill-neutral-500 stroke-neutral-500 dark:fill-neutral-400 dark:stroke-neutral-400" />
|
||||
Default
|
||||
</SelectItem>
|
||||
<SelectItem value="red">
|
||||
<CircleIcon className="fill-red-500 stroke-red-500 dark:fill-red-400 dark:stroke-red-400" />
|
||||
Red
|
||||
</SelectItem>
|
||||
<SelectItem value="blue">
|
||||
<CircleIcon className="fill-blue-500 stroke-blue-500 dark:fill-blue-400 dark:stroke-blue-400" />
|
||||
Blue
|
||||
</SelectItem>
|
||||
<SelectItem value="green">
|
||||
<CircleIcon className="fill-green-500 stroke-green-500 dark:fill-green-400 dark:stroke-green-400" />
|
||||
Green
|
||||
</SelectItem>
|
||||
<SelectItem value="purple">
|
||||
<CircleIcon className="fill-purple-500 stroke-purple-500 dark:fill-purple-400 dark:stroke-purple-400" />
|
||||
Purple
|
||||
</SelectItem>
|
||||
<SelectItem value="pink">
|
||||
<CircleIcon className="fill-pink-500 stroke-pink-500 dark:fill-pink-400 dark:stroke-pink-400" />
|
||||
Pink
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
<FieldSeparator />
|
||||
<Field orientation="responsive">
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor="spoken-language">
|
||||
Spoken Language
|
||||
</FieldLabel>
|
||||
<FieldDescription>
|
||||
For best results, select the language you mainly speak. If
|
||||
it's not listed, it may still be supported via
|
||||
auto-detection.
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<Select
|
||||
value={spokenLanguage}
|
||||
onValueChange={setSpokenLanguage}
|
||||
>
|
||||
<SelectTrigger id="spoken-language">
|
||||
<SelectValue placeholder="Select" />
|
||||
</SelectTrigger>
|
||||
<SelectContent align="end" position="item-aligned">
|
||||
<SelectItem value="auto">Auto</SelectItem>
|
||||
<SelectSeparator />
|
||||
{spokenLanguages.map((language) => (
|
||||
<SelectItem key={language.value} value={language.value}>
|
||||
{language.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
<FieldSeparator />
|
||||
<Field orientation="horizontal">
|
||||
<FieldLabel htmlFor="voice">Voice</FieldLabel>
|
||||
<Select value={voice} onValueChange={setVoice}>
|
||||
<SelectTrigger id="voice">
|
||||
<SelectValue placeholder="Select" />
|
||||
</SelectTrigger>
|
||||
<SelectContent align="end" position="item-aligned">
|
||||
{voices.map((voice) => (
|
||||
<SelectItem key={voice.value} value={voice.value}>
|
||||
{voice.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</FieldSet>
|
||||
</TabsContent>
|
||||
<TabsContent value="notifications">
|
||||
<FieldGroup>
|
||||
<FieldSet>
|
||||
<FieldLabel>Responses</FieldLabel>
|
||||
<FieldDescription>
|
||||
Get notified when ChatGPT responds to requests that take time,
|
||||
like research or image generation.
|
||||
</FieldDescription>
|
||||
<FieldGroup data-slot="checkbox-group">
|
||||
<Field orientation="horizontal">
|
||||
<Checkbox id="push" defaultChecked disabled />
|
||||
<FieldLabel htmlFor="push" className="font-normal">
|
||||
Push notifications
|
||||
</FieldLabel>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</FieldSet>
|
||||
<FieldSeparator />
|
||||
<FieldSet>
|
||||
<FieldLabel>Tasks</FieldLabel>
|
||||
<FieldDescription>
|
||||
Get notified when tasks you've created have updates.{" "}
|
||||
<a href="#">Manage tasks</a>
|
||||
</FieldDescription>
|
||||
<FieldGroup data-slot="checkbox-group">
|
||||
<Field orientation="horizontal">
|
||||
<Checkbox id="push-tasks" />
|
||||
<FieldLabel htmlFor="push-tasks" className="font-normal">
|
||||
Push notifications
|
||||
</FieldLabel>
|
||||
</Field>
|
||||
<Field orientation="horizontal">
|
||||
<Checkbox id="email-tasks" />
|
||||
<FieldLabel htmlFor="email-tasks" className="font-normal">
|
||||
Email notifications
|
||||
</FieldLabel>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</FieldSet>
|
||||
</FieldGroup>
|
||||
</TabsContent>
|
||||
<TabsContent value="personalization">
|
||||
<FieldGroup>
|
||||
<Field orientation="responsive">
|
||||
<FieldLabel htmlFor="nickname">Nickname</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupInput
|
||||
id="nickname"
|
||||
placeholder="Broski"
|
||||
className="@md/field-group:max-w-[200px]"
|
||||
/>
|
||||
<InputGroupAddon align="inline-end">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<InputGroupButton size="icon-xs">
|
||||
<InfoIcon />
|
||||
</InputGroupButton>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="flex items-center gap-2">
|
||||
Used to identify you in the chat. <Kbd>N</Kbd>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</Field>
|
||||
<FieldSeparator />
|
||||
<Field
|
||||
orientation="responsive"
|
||||
className="@md/field-group:flex-col @2xl/field-group:flex-row"
|
||||
>
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor="about">More about you</FieldLabel>
|
||||
<FieldDescription>
|
||||
Tell us more about yourself. This will be used to help us
|
||||
personalize your experience.
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<Textarea
|
||||
id="about"
|
||||
placeholder="I'm a software engineer..."
|
||||
className="min-h-[120px] @md/field-group:min-w-full @2xl/field-group:min-w-[300px]"
|
||||
/>
|
||||
</Field>
|
||||
<FieldSeparator />
|
||||
<FieldLabel>
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor="customization">
|
||||
Enable customizations
|
||||
</FieldLabel>
|
||||
<FieldDescription>
|
||||
Enable customizations to make ChatGPT more personalized.
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<Switch id="customization" defaultChecked />
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
<FieldSeparator />
|
||||
<Field orientation="responsive">
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor="personality">
|
||||
ChatGPT Personality
|
||||
</FieldLabel>
|
||||
<FieldDescription>
|
||||
Set the style and tone ChatGPT should use when responding.
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<Select value={personality} onValueChange={setPersonality}>
|
||||
<SelectTrigger id="personality">
|
||||
{personalities.find((p) => p.value === personality)?.label}
|
||||
</SelectTrigger>
|
||||
<SelectContent align="end">
|
||||
{personalities.map((personality) => (
|
||||
<SelectItem
|
||||
key={personality.value}
|
||||
value={personality.value}
|
||||
>
|
||||
<FieldContent className="gap-0.5">
|
||||
<FieldLabel>{personality.label}</FieldLabel>
|
||||
<FieldDescription className="text-xs">
|
||||
{personality.description}
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
<FieldSeparator />
|
||||
<Field>
|
||||
<FieldLabel htmlFor="instructions">
|
||||
Custom Instructions
|
||||
</FieldLabel>
|
||||
<Textarea
|
||||
id="instructions"
|
||||
value={customInstructions}
|
||||
onChange={(e) => setCustomInstructions(e.target.value)}
|
||||
/>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{instructions.map((instruction) => (
|
||||
<Button
|
||||
variant="outline"
|
||||
key={instruction.value}
|
||||
value={instruction.value}
|
||||
className="rounded-full"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
setCustomInstructions(
|
||||
`${customInstructions} ${instruction.description}`
|
||||
)
|
||||
}
|
||||
>
|
||||
{instruction.label}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</TabsContent>
|
||||
<TabsContent value="security">
|
||||
<FieldGroup>
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor="2fa">
|
||||
Multi-factor authentication
|
||||
</FieldLabel>
|
||||
<FieldDescription>
|
||||
Enable multi-factor authentication to secure your account.
|
||||
If you do not have a two-factor authentication device, you
|
||||
can use a one-time code sent to your email.
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<Switch id="2fa" />
|
||||
</Field>
|
||||
<FieldSeparator />
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldTitle>Log out</FieldTitle>
|
||||
<FieldDescription>
|
||||
Log out of your account on this device.
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<Button variant="outline" size="sm">
|
||||
Log Out
|
||||
</Button>
|
||||
</Field>
|
||||
<FieldSeparator />
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldTitle>Log out of all devices</FieldTitle>
|
||||
<FieldDescription>
|
||||
This will log you out of all devices, including the current
|
||||
session. It may take up to 30 minutes for the changes to
|
||||
take effect.
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<Button variant="outline" size="sm">
|
||||
Log Out All
|
||||
</Button>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</TabsContent>
|
||||
</div>
|
||||
</Tabs>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
137
apps/v4/app/(internal)/sink/(pages)/forms/display-settings.tsx
Normal file
137
apps/v4/app/(internal)/sink/(pages)/forms/display-settings.tsx
Normal file
@@ -0,0 +1,137 @@
|
||||
import { SunDimIcon, SunIcon } from "lucide-react"
|
||||
|
||||
import { Checkbox } from "@/registry/new-york-v4/ui/checkbox"
|
||||
import {
|
||||
Field,
|
||||
FieldContent,
|
||||
FieldDescription,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldLegend,
|
||||
FieldSeparator,
|
||||
FieldSet,
|
||||
FieldTitle,
|
||||
} from "@/registry/new-york-v4/ui/field"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/registry/new-york-v4/ui/select"
|
||||
import { Slider } from "@/registry/new-york-v4/ui/slider"
|
||||
import { Switch } from "@/registry/new-york-v4/ui/switch"
|
||||
|
||||
export function DisplaySettings() {
|
||||
return (
|
||||
<FieldSet>
|
||||
<FieldLegend>Display</FieldLegend>
|
||||
<FieldDescription>
|
||||
Configure display settings, brightness, refresh rate, and more.
|
||||
</FieldDescription>
|
||||
<FieldGroup>
|
||||
<Field orientation="responsive">
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor="resolution">Resolution</FieldLabel>
|
||||
<FieldDescription>Select the display resolution.</FieldDescription>
|
||||
</FieldContent>
|
||||
<Select>
|
||||
<SelectTrigger id="resolution" className="ml-auto">
|
||||
<SelectValue placeholder="Select" />
|
||||
</SelectTrigger>
|
||||
<SelectContent align="end">
|
||||
<SelectItem value="1920x1080">1920 x 1080</SelectItem>
|
||||
<SelectItem value="2560x1440">2560 x 1440</SelectItem>
|
||||
<SelectItem value="3840x2160">3840 x 2160</SelectItem>
|
||||
<SelectItem value="auto">Auto</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
<FieldSeparator />
|
||||
<Field orientation="responsive">
|
||||
<FieldContent>
|
||||
<FieldTitle>Brightness</FieldTitle>
|
||||
<FieldDescription>
|
||||
Adjust the display brightness level.
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<div className="flex min-w-[150px] items-center gap-2">
|
||||
<SunDimIcon className="size-4 shrink-0" />
|
||||
<Slider
|
||||
id="brightness"
|
||||
defaultValue={[75]}
|
||||
max={100}
|
||||
step={1}
|
||||
aria-label="Brightness"
|
||||
/>
|
||||
<SunIcon className="size-4 shrink-0" />
|
||||
</div>
|
||||
</Field>
|
||||
<FieldSeparator />
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor="auto-brightness">
|
||||
Automatically Adjust Brightness
|
||||
</FieldLabel>
|
||||
<FieldDescription>
|
||||
Automatically adjust brightness based on ambient light.
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<Checkbox id="auto-brightness" defaultChecked />
|
||||
</Field>
|
||||
<FieldSeparator />
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor="true-tone">True Tone</FieldLabel>
|
||||
<FieldDescription>
|
||||
Automatically adjust colors to match ambient lighting.
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<Switch id="true-tone" />
|
||||
</Field>
|
||||
<FieldSeparator />
|
||||
<Field orientation="responsive">
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor="refresh-rate">Refresh Rate</FieldLabel>
|
||||
<FieldDescription>
|
||||
Select the display refresh rate.
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<Select>
|
||||
<SelectTrigger id="refresh-rate" className="ml-auto min-w-[200px]">
|
||||
<SelectValue placeholder="Select" />
|
||||
</SelectTrigger>
|
||||
<SelectContent align="end">
|
||||
<SelectItem value="60hz">60 Hz</SelectItem>
|
||||
<SelectItem value="120hz">120 Hz</SelectItem>
|
||||
<SelectItem value="144hz">144 Hz</SelectItem>
|
||||
<SelectItem value="240hz">240 Hz</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
<FieldSeparator />
|
||||
<Field orientation="responsive">
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor="tv-connection">
|
||||
When connected to TV
|
||||
</FieldLabel>
|
||||
<FieldDescription>
|
||||
Choose display behavior when connected to a TV.
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<Select>
|
||||
<SelectTrigger id="tv-connection" className="ml-auto">
|
||||
<SelectValue placeholder="Select" />
|
||||
</SelectTrigger>
|
||||
<SelectContent align="end">
|
||||
<SelectItem value="mirror">Mirror Display</SelectItem>
|
||||
<SelectItem value="extend">Extend Display</SelectItem>
|
||||
<SelectItem value="tv-only">TV Only</SelectItem>
|
||||
<SelectItem value="auto">Auto</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</FieldSet>
|
||||
)
|
||||
}
|
||||
464
apps/v4/app/(internal)/sink/(pages)/forms/notion-prompt-form.tsx
Normal file
464
apps/v4/app/(internal)/sink/(pages)/forms/notion-prompt-form.tsx
Normal file
@@ -0,0 +1,464 @@
|
||||
"use client"
|
||||
|
||||
import { useMemo, useState } from "react"
|
||||
import {
|
||||
IconApps,
|
||||
IconArrowUp,
|
||||
IconAt,
|
||||
IconBook,
|
||||
IconBrandAbstract,
|
||||
IconBrandOpenai,
|
||||
IconBrandZeit,
|
||||
IconCircleDashedPlus,
|
||||
IconPaperclip,
|
||||
IconPlus,
|
||||
IconWorld,
|
||||
IconX,
|
||||
} from "@tabler/icons-react"
|
||||
|
||||
import {
|
||||
Avatar,
|
||||
AvatarFallback,
|
||||
AvatarImage,
|
||||
} from "@/registry/new-york-v4/ui/avatar"
|
||||
import { Badge } from "@/registry/new-york-v4/ui/badge"
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@/registry/new-york-v4/ui/command"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/registry/new-york-v4/ui/dropdown-menu"
|
||||
import { Field, FieldLabel } from "@/registry/new-york-v4/ui/field"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupTextarea,
|
||||
} from "@/registry/new-york-v4/ui/input-group"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/registry/new-york-v4/ui/popover"
|
||||
import { Switch } from "@/registry/new-york-v4/ui/switch"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/registry/new-york-v4/ui/tooltip"
|
||||
|
||||
const SAMPLE_DATA = {
|
||||
mentionable: [
|
||||
{
|
||||
type: "page",
|
||||
title: "Meeting Notes",
|
||||
image: "📝",
|
||||
},
|
||||
{
|
||||
type: "page",
|
||||
title: "Project Dashboard",
|
||||
image: "📊",
|
||||
},
|
||||
{
|
||||
type: "page",
|
||||
title: "Ideas & Brainstorming",
|
||||
image: "💡",
|
||||
},
|
||||
{
|
||||
type: "page",
|
||||
title: "Calendar & Events",
|
||||
image: "📅",
|
||||
},
|
||||
{
|
||||
type: "page",
|
||||
title: "Documentation",
|
||||
image: "📚",
|
||||
},
|
||||
{
|
||||
type: "page",
|
||||
title: "Goals & Objectives",
|
||||
image: "🎯",
|
||||
},
|
||||
{
|
||||
type: "page",
|
||||
title: "Budget Planning",
|
||||
image: "💰",
|
||||
},
|
||||
{
|
||||
type: "page",
|
||||
title: "Team Directory",
|
||||
image: "👥",
|
||||
},
|
||||
{
|
||||
type: "page",
|
||||
title: "Technical Specs",
|
||||
image: "🔧",
|
||||
},
|
||||
{
|
||||
type: "page",
|
||||
title: "Analytics Report",
|
||||
image: "📈",
|
||||
},
|
||||
{
|
||||
type: "user",
|
||||
title: "shadcn",
|
||||
image: "https://github.com/shadcn.png",
|
||||
workspace: "Workspace",
|
||||
},
|
||||
{
|
||||
type: "user",
|
||||
title: "maxleiter",
|
||||
image: "https://github.com/maxleiter.png",
|
||||
workspace: "Cursor",
|
||||
},
|
||||
{
|
||||
type: "user",
|
||||
title: "evilrabbit",
|
||||
image: "https://github.com/evilrabbit.png",
|
||||
workspace: "Vercel",
|
||||
},
|
||||
],
|
||||
models: [
|
||||
{
|
||||
name: "Auto",
|
||||
icon: IconBrandZeit,
|
||||
},
|
||||
{
|
||||
name: "Claude Sonnet 4",
|
||||
icon: IconBrandAbstract,
|
||||
badge: "Beta",
|
||||
},
|
||||
{
|
||||
name: "GPT-5",
|
||||
icon: IconBrandOpenai,
|
||||
badge: "Beta",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
function MentionableIcon({
|
||||
item,
|
||||
}: {
|
||||
item: (typeof SAMPLE_DATA.mentionable)[0]
|
||||
}) {
|
||||
return item.type === "page" ? (
|
||||
<span className="flex size-4 items-center justify-center">
|
||||
{item.image}
|
||||
</span>
|
||||
) : (
|
||||
<Avatar className="size-4">
|
||||
<AvatarImage src={item.image} />
|
||||
<AvatarFallback>{item.title[0]}</AvatarFallback>
|
||||
</Avatar>
|
||||
)
|
||||
}
|
||||
|
||||
export function NotionPromptForm() {
|
||||
const [mentions, setMentions] = useState<string[]>([])
|
||||
const [mentionPopoverOpen, setMentionPopoverOpen] = useState(false)
|
||||
const [modelPopoverOpen, setModelPopoverOpen] = useState(false)
|
||||
const [selectedModel, setSelectedModel] = useState<
|
||||
(typeof SAMPLE_DATA.models)[0]
|
||||
>(SAMPLE_DATA.models[0])
|
||||
const [scopeMenuOpen, setScopeMenuOpen] = useState(false)
|
||||
|
||||
const grouped = useMemo(() => {
|
||||
return SAMPLE_DATA.mentionable.reduce(
|
||||
(acc, item) => {
|
||||
const isAvailable = !mentions.includes(item.title)
|
||||
|
||||
if (isAvailable) {
|
||||
if (!acc[item.type]) {
|
||||
acc[item.type] = []
|
||||
}
|
||||
acc[item.type].push(item)
|
||||
}
|
||||
return acc
|
||||
},
|
||||
{} as Record<string, typeof SAMPLE_DATA.mentionable>
|
||||
)
|
||||
}, [mentions])
|
||||
|
||||
const hasMentions = mentions.length > 0
|
||||
|
||||
return (
|
||||
<form className="[--radius:1.2rem]">
|
||||
<Field>
|
||||
<FieldLabel htmlFor="notion-prompt" className="sr-only">
|
||||
Prompt
|
||||
</FieldLabel>
|
||||
<InputGroup className="bg-background dark:bg-background shadow-none">
|
||||
<InputGroupTextarea
|
||||
id="notion-prompt"
|
||||
placeholder="Ask, search, or make anything..."
|
||||
/>
|
||||
<InputGroupAddon align="block-start">
|
||||
<Popover
|
||||
open={mentionPopoverOpen}
|
||||
onOpenChange={setMentionPopoverOpen}
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<PopoverTrigger asChild>
|
||||
<InputGroupButton
|
||||
variant="outline"
|
||||
size={!hasMentions ? "sm" : "icon-sm"}
|
||||
className="rounded-full transition-transform"
|
||||
>
|
||||
<IconAt /> {!hasMentions && "Add context"}
|
||||
</InputGroupButton>
|
||||
</PopoverTrigger>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Mention a person, page, or date</TooltipContent>
|
||||
</Tooltip>
|
||||
<PopoverContent className="p-0 [--radius:1.2rem]" align="start">
|
||||
<Command>
|
||||
<CommandInput placeholder="Search pages..." />
|
||||
<CommandList>
|
||||
<CommandEmpty>No pages found</CommandEmpty>
|
||||
{Object.entries(grouped).map(([type, items]) => (
|
||||
<CommandGroup
|
||||
key={type}
|
||||
heading={type === "page" ? "Pages" : "Users"}
|
||||
>
|
||||
{items.map((item) => (
|
||||
<CommandItem
|
||||
key={item.title}
|
||||
value={item.title}
|
||||
onSelect={(currentValue) => {
|
||||
setMentions((prev) => [...prev, currentValue])
|
||||
setMentionPopoverOpen(false)
|
||||
}}
|
||||
>
|
||||
<MentionableIcon item={item} />
|
||||
{item.title}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
))}
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<div className="no-scrollbar -m-1.5 flex gap-1 overflow-y-auto p-1.5">
|
||||
{mentions.map((mention) => {
|
||||
const item = SAMPLE_DATA.mentionable.find(
|
||||
(item) => item.title === mention
|
||||
)
|
||||
|
||||
if (!item) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<InputGroupButton
|
||||
key={mention}
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
className="rounded-full !pl-2"
|
||||
onClick={() => {
|
||||
setMentions((prev) => prev.filter((m) => m !== mention))
|
||||
}}
|
||||
>
|
||||
<MentionableIcon item={item} />
|
||||
{item.title}
|
||||
<IconX />
|
||||
</InputGroupButton>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</InputGroupAddon>
|
||||
<InputGroupAddon align="block-end" className="gap-1">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<InputGroupButton
|
||||
size="icon-sm"
|
||||
className="rounded-full"
|
||||
aria-label="Attach file"
|
||||
>
|
||||
<IconPaperclip />
|
||||
</InputGroupButton>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Attach file</TooltipContent>
|
||||
</Tooltip>
|
||||
<DropdownMenu
|
||||
open={modelPopoverOpen}
|
||||
onOpenChange={setModelPopoverOpen}
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<InputGroupButton size="sm" className="rounded-full">
|
||||
{selectedModel.icon && selectedModel.name !== "Auto" && (
|
||||
<selectedModel.icon />
|
||||
)}
|
||||
{selectedModel.name}
|
||||
</InputGroupButton>
|
||||
</DropdownMenuTrigger>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Select AI model</TooltipContent>
|
||||
</Tooltip>
|
||||
<DropdownMenuContent
|
||||
side="top"
|
||||
align="start"
|
||||
className="[--radius:1.2rem]"
|
||||
>
|
||||
<DropdownMenuGroup className="w-72">
|
||||
<DropdownMenuLabel className="text-muted-foreground text-xs">
|
||||
Get answers about your workspace
|
||||
</DropdownMenuLabel>
|
||||
{SAMPLE_DATA.models.map((model) => (
|
||||
<DropdownMenuCheckboxItem
|
||||
key={model.name}
|
||||
checked={model.name === selectedModel.name}
|
||||
onCheckedChange={(checked) => {
|
||||
if (checked) {
|
||||
setSelectedModel(model)
|
||||
}
|
||||
}}
|
||||
className="pl-2 *:[span:first-child]:right-2 *:[span:first-child]:left-auto"
|
||||
>
|
||||
{model.icon && <model.icon />}
|
||||
{model.name}
|
||||
{model.badge && (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="h-5 rounded-sm bg-blue-100 px-1 text-xs text-blue-800 dark:bg-blue-900 dark:text-blue-100"
|
||||
>
|
||||
{model.badge}
|
||||
</Badge>
|
||||
)}
|
||||
</DropdownMenuCheckboxItem>
|
||||
))}
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<DropdownMenu open={scopeMenuOpen} onOpenChange={setScopeMenuOpen}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<InputGroupButton size="sm" className="rounded-full">
|
||||
<IconWorld /> All Sources
|
||||
</InputGroupButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
side="top"
|
||||
align="start"
|
||||
className="[--radius:1.2rem]"
|
||||
>
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem
|
||||
asChild
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
<label htmlFor="web-search">
|
||||
<IconWorld /> Web Search{" "}
|
||||
<Switch
|
||||
id="web-search"
|
||||
className="ml-auto"
|
||||
defaultChecked
|
||||
/>
|
||||
</label>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem
|
||||
asChild
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
<label htmlFor="apps">
|
||||
<IconApps /> Apps and Integrations
|
||||
<Switch id="apps" className="ml-auto" defaultChecked />
|
||||
</label>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<IconCircleDashedPlus /> All Sources I can access
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger>
|
||||
<Avatar className="size-4">
|
||||
<AvatarImage src="https://github.com/shadcn.png" />
|
||||
<AvatarFallback>CN</AvatarFallback>
|
||||
</Avatar>
|
||||
shadcn
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuSubContent className="w-72 p-0 [--radius:1.2rem]">
|
||||
<Command>
|
||||
<CommandInput
|
||||
placeholder="Find or use knowledge in..."
|
||||
autoFocus
|
||||
/>
|
||||
<CommandList>
|
||||
<CommandEmpty>No knowledge found</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{SAMPLE_DATA.mentionable
|
||||
.filter((item) => item.type === "user")
|
||||
.map((user) => (
|
||||
<CommandItem
|
||||
key={user.title}
|
||||
value={user.title}
|
||||
onSelect={() => {
|
||||
// Handle user selection here
|
||||
console.log("Selected user:", user.title)
|
||||
}}
|
||||
>
|
||||
<Avatar className="size-4">
|
||||
<AvatarImage src={user.image} />
|
||||
<AvatarFallback>
|
||||
{user.title[0]}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
{user.title}{" "}
|
||||
<span className="text-muted-foreground">
|
||||
- {user.workspace}
|
||||
</span>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuSub>
|
||||
<DropdownMenuItem>
|
||||
<IconBook /> Help Center
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>
|
||||
<IconPlus /> Connect Apps
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuLabel className="text-muted-foreground text-xs">
|
||||
We'll only search in the sources selected here.
|
||||
</DropdownMenuLabel>
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<InputGroupButton
|
||||
aria-label="Send"
|
||||
className="ml-auto rounded-full"
|
||||
variant="default"
|
||||
size="icon-sm"
|
||||
>
|
||||
<IconArrowUp />
|
||||
</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</Field>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
31
apps/v4/app/(internal)/sink/(pages)/forms/page.tsx
Normal file
31
apps/v4/app/(internal)/sink/(pages)/forms/page.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import { AppearanceSettings } from "@/app/(internal)/sink/(pages)/forms/appearance-settings"
|
||||
import { ChatSettings } from "@/app/(internal)/sink/(pages)/forms/chat-settings"
|
||||
import { DisplaySettings } from "@/app/(internal)/sink/(pages)/forms/display-settings"
|
||||
import { NotionPromptForm } from "@/app/(internal)/sink/(pages)/forms/notion-prompt-form"
|
||||
import { ShipRegistrationForm } from "@/app/(internal)/sink/(pages)/forms/ship-registration-form"
|
||||
import { ShippingForm } from "@/app/(internal)/sink/(pages)/forms/shipping-form"
|
||||
|
||||
export default function FormsPage() {
|
||||
return (
|
||||
<div className="@container flex flex-1 flex-col gap-12 p-4">
|
||||
<div className="grid flex-1 gap-12 @3xl:grid-cols-2 @5xl:grid-cols-3 @[120rem]:grid-cols-4 [&>div]:max-w-lg">
|
||||
<div className="flex flex-col gap-12">
|
||||
<NotionPromptForm />
|
||||
<ChatSettings />
|
||||
</div>
|
||||
<div className="flex flex-col gap-12">
|
||||
<AppearanceSettings />
|
||||
</div>
|
||||
<div className="flex flex-col gap-12">
|
||||
<DisplaySettings />
|
||||
</div>
|
||||
<div className="flex flex-col gap-12">
|
||||
<ShippingForm />
|
||||
</div>
|
||||
<div className="col-span-2 flex flex-col gap-12">
|
||||
<ShipRegistrationForm />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import { Checkbox } from "@/registry/new-york-v4/ui/checkbox"
|
||||
import {
|
||||
Field,
|
||||
FieldContent,
|
||||
FieldDescription,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldLegend,
|
||||
FieldSet,
|
||||
FieldTitle,
|
||||
} from "@/registry/new-york-v4/ui/field"
|
||||
import { Input } from "@/registry/new-york-v4/ui/input"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupInput,
|
||||
} from "@/registry/new-york-v4/ui/input-group"
|
||||
import {
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
} from "@/registry/new-york-v4/ui/radio-group"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/registry/new-york-v4/ui/select"
|
||||
import { Textarea } from "@/registry/new-york-v4/ui/textarea"
|
||||
|
||||
export function ShipRegistrationForm() {
|
||||
return (
|
||||
<div className="flex max-w-md flex-col gap-6">
|
||||
<div className="flex flex-col gap-2">
|
||||
<h1 className="text-2xl font-bold tracking-tight">
|
||||
Join us in SF or online on October 23
|
||||
</h1>
|
||||
<FieldDescription>
|
||||
Already signed up? <a href="#">Log in</a>
|
||||
</FieldDescription>
|
||||
</div>
|
||||
<form>
|
||||
<FieldGroup>
|
||||
<FieldSet>
|
||||
<FieldLegend>1. Select your ticket type</FieldLegend>
|
||||
<FieldDescription>
|
||||
Select your ticket type to join us in San Francisco or online on
|
||||
October 23.
|
||||
</FieldDescription>
|
||||
<Field>
|
||||
<RadioGroup>
|
||||
<FieldLabel htmlFor="in-person">
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldTitle>In Person</FieldTitle>
|
||||
<FieldDescription>
|
||||
Join us in San Francisco on October 23.
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<RadioGroupItem value="in-person" id="in-person" />
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
<FieldLabel htmlFor="online">
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldTitle>Online</FieldTitle>
|
||||
<FieldDescription>
|
||||
Join us online on October 23.
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<RadioGroupItem value="online" id="online" />
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
</RadioGroup>
|
||||
</Field>
|
||||
</FieldSet>
|
||||
<Field orientation="horizontal">
|
||||
<Checkbox id="next-conf" />
|
||||
<FieldLabel htmlFor="next-conf">
|
||||
Also sign up for Next.js Conf 2025
|
||||
</FieldLabel>
|
||||
</Field>
|
||||
<FieldSet>
|
||||
<FieldLegend>2. Complete your attendee information</FieldLegend>
|
||||
<FieldDescription>
|
||||
By entering your information, you acknowledge that you have read
|
||||
and agree to the <a href="#">Terms of Service</a> and{" "}
|
||||
<a href="#">Privacy Policy</a>.
|
||||
</FieldDescription>
|
||||
<FieldGroup className="grid grid-cols-2 gap-x-4">
|
||||
<Field>
|
||||
<FieldLabel htmlFor="first-name">First Name</FieldLabel>
|
||||
<Input id="first-name" placeholder="Jane" required />
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="last-name">Last Name</FieldLabel>
|
||||
<Input id="last-name" placeholder="Doe" required />
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="email">Email</FieldLabel>
|
||||
<Input id="email" placeholder="jane.doe@example.com" required />
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="company">Company</FieldLabel>
|
||||
<Input id="company" placeholder="Example Inc." required />
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="job-title">Job Title</FieldLabel>
|
||||
<Input
|
||||
id="job-title"
|
||||
placeholder="Software Engineer"
|
||||
required
|
||||
/>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="country">Country</FieldLabel>
|
||||
<Select>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a country" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="us">United States</SelectItem>
|
||||
<SelectItem value="uk">United Kingdom</SelectItem>
|
||||
<SelectItem value="ca">Canada</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
<Field className="col-span-2">
|
||||
<FieldLabel htmlFor="topics">
|
||||
What AI-related topics are you most curious about?
|
||||
</FieldLabel>
|
||||
<Textarea
|
||||
id="topics"
|
||||
placeholder="Agents, Security, Improving UX/Personalization, etc."
|
||||
className="min-h-[100px]"
|
||||
/>
|
||||
</Field>
|
||||
<Field className="col-span-2">
|
||||
<FieldLabel htmlFor="workloads">
|
||||
What types of AI workloads are you tackling right now?
|
||||
</FieldLabel>
|
||||
<Textarea id="workloads" className="min-h-[100px]" />
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</FieldSet>
|
||||
<FieldSet>
|
||||
<FieldLegend>3. Buy your ticket</FieldLegend>
|
||||
<FieldDescription>
|
||||
Enter your card details to purchase your ticket.
|
||||
</FieldDescription>
|
||||
<FieldGroup className="grid grid-cols-2 gap-x-4">
|
||||
<Field className="col-span-2">
|
||||
<FieldLabel htmlFor="card-number">Card Number</FieldLabel>
|
||||
<Input
|
||||
id="card-number"
|
||||
placeholder="1234 5678 9012 3456"
|
||||
required
|
||||
/>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="expiry-date">Expiry Date</FieldLabel>
|
||||
<Input id="expiry-date" placeholder="MM/YY" required />
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="cvv">CVV</FieldLabel>
|
||||
<Input id="cvv" placeholder="123" required />
|
||||
</Field>
|
||||
<Field className="col-span-2">
|
||||
<FieldLabel htmlFor="promo-code">Promo Code</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupInput id="promo-code" placeholder="PROMO10" />
|
||||
<InputGroupAddon align="inline-end">
|
||||
<InputGroupButton>Apply</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</FieldSet>
|
||||
<Field>
|
||||
<Button type="submit">Purchase Ticket</Button>
|
||||
<FieldDescription>
|
||||
By clicking Purchase Ticket, you agree to the{" "}
|
||||
<a href="#">Terms of Service</a> and{" "}
|
||||
<a href="#">Privacy Policy</a>.
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
121
apps/v4/app/(internal)/sink/(pages)/forms/shipping-form.tsx
Normal file
121
apps/v4/app/(internal)/sink/(pages)/forms/shipping-form.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
import { Badge } from "@/registry/new-york-v4/ui/badge"
|
||||
import { Checkbox } from "@/registry/new-york-v4/ui/checkbox"
|
||||
import {
|
||||
Field,
|
||||
FieldContent,
|
||||
FieldDescription,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldLegend,
|
||||
FieldSet,
|
||||
FieldTitle,
|
||||
} from "@/registry/new-york-v4/ui/field"
|
||||
import { Input } from "@/registry/new-york-v4/ui/input"
|
||||
import {
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
} from "@/registry/new-york-v4/ui/radio-group"
|
||||
import { Textarea } from "@/registry/new-york-v4/ui/textarea"
|
||||
|
||||
export function ShippingForm() {
|
||||
return (
|
||||
<FieldSet>
|
||||
<FieldLegend>Shipping Details</FieldLegend>
|
||||
<FieldDescription>
|
||||
Please provide your shipping details so we can deliver your order.
|
||||
</FieldDescription>
|
||||
<FieldGroup>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="street-address">Street Address</FieldLabel>
|
||||
<Input id="street-address" autoComplete="off" />
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="city">City</FieldLabel>
|
||||
<Input id="city" />
|
||||
</Field>
|
||||
<FieldSet>
|
||||
<FieldLegend variant="label">Shipping Method</FieldLegend>
|
||||
<FieldDescription>
|
||||
Please select the shipping method for your order.
|
||||
</FieldDescription>
|
||||
<RadioGroup>
|
||||
<Field orientation="horizontal">
|
||||
<RadioGroupItem value="standard" id="shipping-method-1" />
|
||||
<FieldLabel htmlFor="shipping-method-1" className="font-normal">
|
||||
Standard{" "}
|
||||
<Badge className="rounded-full py-px" variant="outline">
|
||||
Free
|
||||
</Badge>
|
||||
</FieldLabel>
|
||||
</Field>
|
||||
<Field orientation="horizontal">
|
||||
<RadioGroupItem value="express" id="shipping-method-2" />
|
||||
<FieldLabel htmlFor="shipping-method-2" className="font-normal">
|
||||
Express
|
||||
</FieldLabel>
|
||||
</Field>
|
||||
</RadioGroup>
|
||||
</FieldSet>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="message">Message</FieldLabel>
|
||||
<Textarea id="message" />
|
||||
<FieldDescription>Anything else you want to add?</FieldDescription>
|
||||
</Field>
|
||||
<FieldSet>
|
||||
<FieldLegend>Additional Items</FieldLegend>
|
||||
<FieldDescription>
|
||||
Please select the additional items for your order.
|
||||
</FieldDescription>
|
||||
<FieldGroup data-slot="checkbox-group">
|
||||
<FieldLabel htmlFor="gift-wrapping">
|
||||
<Field orientation="horizontal">
|
||||
<Checkbox
|
||||
value="gift-wrapping"
|
||||
id="gift-wrapping"
|
||||
aria-label="Gift Wrapping"
|
||||
/>
|
||||
<FieldContent>
|
||||
<FieldTitle>Gift Wrapping</FieldTitle>
|
||||
<FieldDescription>
|
||||
Add elegant gift wrapping with a personalized message.
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
<FieldLabel htmlFor="insurance">
|
||||
<Field orientation="horizontal">
|
||||
<Checkbox
|
||||
value="insurance"
|
||||
id="insurance"
|
||||
aria-label="Package Insurance"
|
||||
/>
|
||||
<FieldContent>
|
||||
<FieldTitle>Package Insurance</FieldTitle>
|
||||
<FieldDescription>
|
||||
Protect your shipment with comprehensive insurance coverage.
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
<FieldLabel htmlFor="signature-confirmation">
|
||||
<Field orientation="horizontal">
|
||||
<Checkbox
|
||||
value="signature-confirmation"
|
||||
id="signature-confirmation"
|
||||
aria-label="Signature Confirmation"
|
||||
/>
|
||||
<FieldContent>
|
||||
<FieldTitle>Signature Confirmation</FieldTitle>
|
||||
<FieldDescription>
|
||||
Require recipient signature upon delivery for added
|
||||
security.
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
</FieldGroup>
|
||||
</FieldSet>
|
||||
</FieldGroup>
|
||||
</FieldSet>
|
||||
)
|
||||
}
|
||||
55
apps/v4/app/(internal)/sink/(pages)/next-form/actions.ts
Normal file
55
apps/v4/app/(internal)/sink/(pages)/next-form/actions.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
"use server"
|
||||
|
||||
import { FormState } from "@/app/(internal)/sink/(pages)/next-form/example-form"
|
||||
import { exampleFormSchema } from "@/app/(internal)/sink/(pages)/schema"
|
||||
|
||||
export async function subscriptionAction(
|
||||
_prevState: FormState,
|
||||
formData: FormData
|
||||
): Promise<FormState> {
|
||||
// Simulate server processing
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
|
||||
const values = {
|
||||
name: formData.get("name") as string,
|
||||
email: formData.get("email") as string,
|
||||
plan: formData.get("plan") as "basic" | "pro",
|
||||
billingPeriod: formData.get("billingPeriod") as string,
|
||||
addons: formData.getAll("addons") as string[],
|
||||
teamSize: parseInt(formData.get("teamSize") as string) || 1,
|
||||
emailNotifications: formData.get("emailNotifications") === "on",
|
||||
startDate: formData.get("startDate")
|
||||
? new Date(formData.get("startDate") as string)
|
||||
: new Date(),
|
||||
theme: formData.get("theme") as string,
|
||||
password: formData.get("password") as string,
|
||||
comments: formData.get("comments") as string,
|
||||
}
|
||||
|
||||
const result = exampleFormSchema.safeParse(values)
|
||||
|
||||
if (!result.success) {
|
||||
return {
|
||||
values,
|
||||
success: false,
|
||||
errors: result.error.flatten().fieldErrors,
|
||||
}
|
||||
}
|
||||
|
||||
// Simulate some business logic validation
|
||||
if (result.data.email.includes("invalid")) {
|
||||
return {
|
||||
values,
|
||||
success: false,
|
||||
errors: {
|
||||
email: ["This email domain is not supported"],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
values,
|
||||
errors: null,
|
||||
success: true,
|
||||
}
|
||||
}
|
||||
391
apps/v4/app/(internal)/sink/(pages)/next-form/example-form.tsx
Normal file
391
apps/v4/app/(internal)/sink/(pages)/next-form/example-form.tsx
Normal file
@@ -0,0 +1,391 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import Form from "next/form"
|
||||
import { z } from "zod"
|
||||
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/new-york-v4/ui/card"
|
||||
import { Checkbox } from "@/registry/new-york-v4/ui/checkbox"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/registry/new-york-v4/ui/dialog"
|
||||
import {
|
||||
Field,
|
||||
FieldContent,
|
||||
FieldDescription,
|
||||
FieldError,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldLegend,
|
||||
FieldSeparator,
|
||||
FieldSet,
|
||||
FieldTitle,
|
||||
} from "@/registry/new-york-v4/ui/field"
|
||||
import { Input } from "@/registry/new-york-v4/ui/input"
|
||||
import {
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
} from "@/registry/new-york-v4/ui/radio-group"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/registry/new-york-v4/ui/select"
|
||||
import { Spinner } from "@/registry/new-york-v4/ui/spinner"
|
||||
import { Switch } from "@/registry/new-york-v4/ui/switch"
|
||||
import { Textarea } from "@/registry/new-york-v4/ui/textarea"
|
||||
import { addons, exampleFormSchema } from "@/app/(internal)/sink/(pages)/schema"
|
||||
|
||||
import { subscriptionAction } from "./actions"
|
||||
|
||||
export type FormState = {
|
||||
values: z.infer<typeof exampleFormSchema>
|
||||
errors: null | Partial<
|
||||
Record<keyof z.infer<typeof exampleFormSchema>, string[]>
|
||||
>
|
||||
success: boolean
|
||||
}
|
||||
|
||||
export function ExampleForm() {
|
||||
const formId = React.useId()
|
||||
const [formKey, setFormKey] = React.useState(formId)
|
||||
const [showResults, setShowResults] = React.useState(false)
|
||||
const [formState, formAction, pending] = React.useActionState<
|
||||
FormState,
|
||||
FormData
|
||||
>(subscriptionAction, {
|
||||
values: {
|
||||
name: "",
|
||||
email: "",
|
||||
plan: "basic",
|
||||
billingPeriod: "",
|
||||
addons: ["analytics"],
|
||||
teamSize: 1,
|
||||
emailNotifications: false,
|
||||
comments: "",
|
||||
startDate: new Date(),
|
||||
theme: "system",
|
||||
password: "",
|
||||
},
|
||||
errors: null,
|
||||
success: false,
|
||||
})
|
||||
|
||||
React.useEffect(() => {
|
||||
if (formState.success) {
|
||||
setShowResults(true)
|
||||
}
|
||||
}, [formState.success])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card className="w-full max-w-sm">
|
||||
<CardHeader className="border-b">
|
||||
<CardTitle>Subscription Form</CardTitle>
|
||||
<CardDescription>
|
||||
Create your subscription using server actions and useActionState.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Form action={formAction} id="subscription-form" key={formKey}>
|
||||
<FieldGroup>
|
||||
<Field data-invalid={!!formState.errors?.name?.length}>
|
||||
<FieldLabel htmlFor="name">Name</FieldLabel>
|
||||
<Input
|
||||
id="name"
|
||||
name="name"
|
||||
defaultValue={formState.values.name}
|
||||
disabled={pending}
|
||||
aria-invalid={!!formState.errors?.name?.length}
|
||||
autoComplete="off"
|
||||
/>
|
||||
<FieldDescription>Enter your name</FieldDescription>
|
||||
{formState.errors?.name && (
|
||||
<FieldError>{formState.errors.name[0]}</FieldError>
|
||||
)}
|
||||
</Field>
|
||||
<Field data-invalid={!!formState.errors?.email?.length}>
|
||||
<FieldLabel htmlFor="email">Email</FieldLabel>
|
||||
<Input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
defaultValue={formState.values.email}
|
||||
disabled={pending}
|
||||
aria-invalid={!!formState.errors?.email?.length}
|
||||
autoComplete="off"
|
||||
/>
|
||||
<FieldDescription>Enter your email address</FieldDescription>
|
||||
{formState.errors?.email && (
|
||||
<FieldError>{formState.errors.email[0]}</FieldError>
|
||||
)}
|
||||
</Field>
|
||||
<FieldSeparator />
|
||||
<FieldSet data-invalid={!!formState.errors?.plan?.length}>
|
||||
<FieldLegend>Subscription Plan</FieldLegend>
|
||||
<FieldDescription>
|
||||
Choose your subscription plan.
|
||||
</FieldDescription>
|
||||
<RadioGroup
|
||||
name="plan"
|
||||
defaultValue={formState.values.plan}
|
||||
disabled={pending}
|
||||
aria-invalid={!!formState.errors?.plan?.length}
|
||||
>
|
||||
<FieldLabel htmlFor="basic">
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldTitle>Basic</FieldTitle>
|
||||
<FieldDescription>
|
||||
For individuals and small teams
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<RadioGroupItem value="basic" id="basic" />
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
<FieldLabel htmlFor="pro">
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldTitle>Pro</FieldTitle>
|
||||
<FieldDescription>
|
||||
For businesses with higher demands
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<RadioGroupItem value="pro" id="pro" />
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
</RadioGroup>
|
||||
{formState.errors?.plan && (
|
||||
<FieldError>{formState.errors.plan[0]}</FieldError>
|
||||
)}
|
||||
</FieldSet>
|
||||
<FieldSeparator />
|
||||
<Field data-invalid={!!formState.errors?.billingPeriod?.length}>
|
||||
<FieldLabel htmlFor="billingPeriod">Billing Period</FieldLabel>
|
||||
<Select
|
||||
name="billingPeriod"
|
||||
defaultValue={formState.values.billingPeriod}
|
||||
disabled={pending}
|
||||
aria-invalid={!!formState.errors?.billingPeriod?.length}
|
||||
>
|
||||
<SelectTrigger id="billingPeriod">
|
||||
<SelectValue placeholder="Select billing period" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="monthly">Monthly</SelectItem>
|
||||
<SelectItem value="yearly">Yearly</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FieldDescription>
|
||||
Choose how often you want to be billed.
|
||||
</FieldDescription>
|
||||
{formState.errors?.billingPeriod && (
|
||||
<FieldError>{formState.errors.billingPeriod[0]}</FieldError>
|
||||
)}
|
||||
</Field>
|
||||
<FieldSeparator />
|
||||
<FieldSet data-invalid={!!formState.errors?.addons?.length}>
|
||||
<FieldLegend>Add-ons</FieldLegend>
|
||||
<FieldDescription>
|
||||
Select additional features you'd like to include.
|
||||
</FieldDescription>
|
||||
<FieldGroup data-slot="checkbox-group">
|
||||
{addons.map((addon) => (
|
||||
<Field key={addon.id} orientation="horizontal">
|
||||
<Checkbox
|
||||
id={addon.id}
|
||||
name="addons"
|
||||
value={addon.id}
|
||||
defaultChecked={formState.values.addons.includes(
|
||||
addon.id
|
||||
)}
|
||||
disabled={pending}
|
||||
aria-invalid={!!formState.errors?.addons?.length}
|
||||
/>
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor={addon.id}>
|
||||
{addon.title}
|
||||
</FieldLabel>
|
||||
<FieldDescription>{addon.description}</FieldDescription>
|
||||
</FieldContent>
|
||||
</Field>
|
||||
))}
|
||||
</FieldGroup>
|
||||
{formState.errors?.addons && (
|
||||
<FieldError>{formState.errors.addons[0]}</FieldError>
|
||||
)}
|
||||
</FieldSet>
|
||||
<FieldSeparator />
|
||||
<Field data-invalid={!!formState.errors?.teamSize?.length}>
|
||||
<FieldLabel htmlFor="teamSize">Team Size</FieldLabel>
|
||||
<Input
|
||||
id="teamSize"
|
||||
name="teamSize"
|
||||
type="number"
|
||||
min="1"
|
||||
max="50"
|
||||
defaultValue={formState.values.teamSize.toString()}
|
||||
disabled={pending}
|
||||
aria-invalid={!!formState.errors?.teamSize?.length}
|
||||
/>
|
||||
<FieldDescription>
|
||||
How many people will be using the subscription? (1-50)
|
||||
</FieldDescription>
|
||||
{formState.errors?.teamSize && (
|
||||
<FieldError>{formState.errors.teamSize[0]}</FieldError>
|
||||
)}
|
||||
</Field>
|
||||
<FieldSeparator />
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor="emailNotifications">
|
||||
Email Notifications
|
||||
</FieldLabel>
|
||||
<FieldDescription>
|
||||
Receive email updates about your subscription
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<Switch
|
||||
id="emailNotifications"
|
||||
name="emailNotifications"
|
||||
defaultChecked={formState.values.emailNotifications}
|
||||
disabled={pending}
|
||||
aria-invalid={!!formState.errors?.emailNotifications?.length}
|
||||
/>
|
||||
</Field>
|
||||
<FieldSeparator />
|
||||
<Field data-invalid={!!formState.errors?.startDate?.length}>
|
||||
<FieldLabel htmlFor="startDate">Start Date</FieldLabel>
|
||||
<Input
|
||||
id="startDate"
|
||||
name="startDate"
|
||||
type="date"
|
||||
defaultValue={
|
||||
formState.values.startDate.toISOString().split("T")[0]
|
||||
}
|
||||
disabled={pending}
|
||||
aria-invalid={!!formState.errors?.startDate?.length}
|
||||
/>
|
||||
<FieldDescription>
|
||||
Choose when your subscription should start
|
||||
</FieldDescription>
|
||||
{formState.errors?.startDate && (
|
||||
<FieldError>{formState.errors.startDate[0]}</FieldError>
|
||||
)}
|
||||
</Field>
|
||||
<FieldSeparator />
|
||||
<Field data-invalid={!!formState.errors?.theme?.length}>
|
||||
<FieldLabel htmlFor="theme">Theme Preference</FieldLabel>
|
||||
<Select
|
||||
name="theme"
|
||||
defaultValue={formState.values.theme}
|
||||
disabled={pending}
|
||||
aria-invalid={!!formState.errors?.theme?.length}
|
||||
>
|
||||
<SelectTrigger id="theme">
|
||||
<SelectValue placeholder="Select theme" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="light">Light</SelectItem>
|
||||
<SelectItem value="dark">Dark</SelectItem>
|
||||
<SelectItem value="system">System</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FieldDescription>
|
||||
Choose your preferred color theme
|
||||
</FieldDescription>
|
||||
{formState.errors?.theme && (
|
||||
<FieldError>{formState.errors.theme[0]}</FieldError>
|
||||
)}
|
||||
</Field>
|
||||
<FieldSeparator />
|
||||
<Field data-invalid={!!formState.errors?.password?.length}>
|
||||
<FieldLabel htmlFor="password">Password</FieldLabel>
|
||||
<Input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
defaultValue={formState.values.password}
|
||||
placeholder="Enter your password"
|
||||
disabled={pending}
|
||||
aria-invalid={!!formState.errors?.password?.length}
|
||||
/>
|
||||
<FieldDescription>
|
||||
Must contain uppercase, lowercase, number, and be 8+
|
||||
characters
|
||||
</FieldDescription>
|
||||
{formState.errors?.password && (
|
||||
<FieldError>{formState.errors.password[0]}</FieldError>
|
||||
)}
|
||||
</Field>
|
||||
<FieldSeparator />
|
||||
<Field data-invalid={!!formState.errors?.comments?.length}>
|
||||
<FieldLabel htmlFor="comments">Additional Comments</FieldLabel>
|
||||
<Textarea
|
||||
id="comments"
|
||||
name="comments"
|
||||
defaultValue={formState.values.comments}
|
||||
placeholder="Tell us more about your needs..."
|
||||
rows={3}
|
||||
disabled={pending}
|
||||
aria-invalid={!!formState.errors?.comments?.length}
|
||||
/>
|
||||
<FieldDescription>
|
||||
Share any additional requirements or feedback (10-240
|
||||
characters)
|
||||
</FieldDescription>
|
||||
{formState.errors?.comments && (
|
||||
<FieldError>{formState.errors.comments[0]}</FieldError>
|
||||
)}
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</Form>
|
||||
</CardContent>
|
||||
<CardFooter className="border-t">
|
||||
<Field orientation="horizontal" className="justify-end">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
disabled={pending}
|
||||
form="subscription-form"
|
||||
onClick={() => setFormKey(formKey + 1)}
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
<Button type="submit" disabled={pending} form="subscription-form">
|
||||
{pending && <Spinner />}
|
||||
Create Subscription
|
||||
</Button>
|
||||
</Field>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
<Dialog open={showResults} onOpenChange={setShowResults}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Subscription Created!</DialogTitle>
|
||||
<DialogDescription>
|
||||
Here are the details of your subscription.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<pre className="overflow-x-auto rounded-md bg-black p-4 font-mono text-sm text-white">
|
||||
<code>{JSON.stringify(formState.values, null, 2)}</code>
|
||||
</pre>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
)
|
||||
}
|
||||
9
apps/v4/app/(internal)/sink/(pages)/next-form/page.tsx
Normal file
9
apps/v4/app/(internal)/sink/(pages)/next-form/page.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import { ExampleForm } from "@/app/(internal)/sink/(pages)/next-form/example-form"
|
||||
|
||||
export default function NextFormPage() {
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center p-4">
|
||||
<ExampleForm />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,490 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { format } from "date-fns"
|
||||
import { Controller, useForm } from "react-hook-form"
|
||||
import z from "zod"
|
||||
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import { Calendar } from "@/registry/new-york-v4/ui/calendar"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/new-york-v4/ui/card"
|
||||
import { Checkbox } from "@/registry/new-york-v4/ui/checkbox"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/registry/new-york-v4/ui/dialog"
|
||||
import {
|
||||
Field,
|
||||
FieldContent,
|
||||
FieldDescription,
|
||||
FieldError,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldLegend,
|
||||
FieldSeparator,
|
||||
FieldSet,
|
||||
FieldTitle,
|
||||
} from "@/registry/new-york-v4/ui/field"
|
||||
import { Input } from "@/registry/new-york-v4/ui/input"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/registry/new-york-v4/ui/popover"
|
||||
import {
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
} from "@/registry/new-york-v4/ui/radio-group"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/registry/new-york-v4/ui/select"
|
||||
import { Slider } from "@/registry/new-york-v4/ui/slider"
|
||||
import { Switch } from "@/registry/new-york-v4/ui/switch"
|
||||
import { Textarea } from "@/registry/new-york-v4/ui/textarea"
|
||||
import {
|
||||
ToggleGroup,
|
||||
ToggleGroupItem,
|
||||
} from "@/registry/new-york-v4/ui/toggle-group"
|
||||
import { addons, exampleFormSchema } from "@/app/(internal)/sink/(pages)/schema"
|
||||
|
||||
export function ExampleForm() {
|
||||
const [values, setValues] = useState<z.infer<typeof exampleFormSchema>>()
|
||||
const [open, setOpen] = useState(false)
|
||||
const form = useForm<z.infer<typeof exampleFormSchema>>({
|
||||
resolver: zodResolver(exampleFormSchema),
|
||||
mode: "onChange",
|
||||
defaultValues: {
|
||||
name: "",
|
||||
email: "",
|
||||
plan: "basic" as const,
|
||||
billingPeriod: "",
|
||||
addons: ["analytics"],
|
||||
emailNotifications: false,
|
||||
teamSize: 1,
|
||||
comments: "",
|
||||
startDate: new Date(),
|
||||
theme: "system",
|
||||
password: "",
|
||||
},
|
||||
})
|
||||
|
||||
function onSubmit(data: z.infer<typeof exampleFormSchema>) {
|
||||
setValues(data)
|
||||
setOpen(true)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card className="w-full max-w-sm">
|
||||
<CardHeader className="border-b">
|
||||
<CardTitle>React Hook Form</CardTitle>
|
||||
<CardDescription>
|
||||
This form uses React Hook Form with Zod validation.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form id="subscription-form" onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<FieldGroup>
|
||||
<Controller
|
||||
name="name"
|
||||
control={form.control}
|
||||
render={({ field, fieldState }) => {
|
||||
const isInvalid = fieldState.invalid
|
||||
return (
|
||||
<Field data-invalid={isInvalid}>
|
||||
<FieldLabel htmlFor={field.name}>Name</FieldLabel>
|
||||
<Input
|
||||
{...field}
|
||||
id={field.name}
|
||||
aria-invalid={isInvalid}
|
||||
autoComplete="off"
|
||||
/>
|
||||
<FieldDescription>Enter your name</FieldDescription>
|
||||
{isInvalid && <FieldError errors={[fieldState.error]} />}
|
||||
</Field>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Controller
|
||||
name="email"
|
||||
control={form.control}
|
||||
render={({ field, fieldState }) => {
|
||||
const isInvalid = fieldState.invalid
|
||||
return (
|
||||
<Field data-invalid={isInvalid}>
|
||||
<FieldLabel htmlFor={field.name}>Email</FieldLabel>
|
||||
<Input
|
||||
{...field}
|
||||
type="email"
|
||||
id={field.name}
|
||||
aria-invalid={isInvalid}
|
||||
autoComplete="off"
|
||||
/>
|
||||
<FieldDescription>
|
||||
Enter your email address
|
||||
</FieldDescription>
|
||||
{isInvalid && <FieldError errors={[fieldState.error]} />}
|
||||
</Field>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<FieldSeparator />
|
||||
<Controller
|
||||
name="plan"
|
||||
control={form.control}
|
||||
render={({ field, fieldState }) => {
|
||||
const isInvalid = fieldState.invalid
|
||||
return (
|
||||
<FieldSet data-invalid={isInvalid}>
|
||||
<FieldLegend>Subscription Plan</FieldLegend>
|
||||
<FieldDescription>
|
||||
Choose your subscription plan.
|
||||
</FieldDescription>
|
||||
<RadioGroup
|
||||
name={field.name}
|
||||
value={field.value}
|
||||
onValueChange={field.onChange}
|
||||
aria-invalid={isInvalid}
|
||||
>
|
||||
<FieldLabel htmlFor="basic">
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldTitle>Basic</FieldTitle>
|
||||
<FieldDescription>
|
||||
For individuals and small teams
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<RadioGroupItem
|
||||
value="basic"
|
||||
id="basic"
|
||||
aria-invalid={isInvalid}
|
||||
/>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
<FieldLabel htmlFor="pro">
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldTitle>Pro</FieldTitle>
|
||||
<FieldDescription>
|
||||
For businesses with higher demands
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<RadioGroupItem
|
||||
value="pro"
|
||||
id="pro"
|
||||
aria-invalid={isInvalid}
|
||||
/>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
</RadioGroup>
|
||||
{isInvalid && <FieldError errors={[fieldState.error]} />}
|
||||
</FieldSet>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<FieldSeparator />
|
||||
<Controller
|
||||
name="billingPeriod"
|
||||
control={form.control}
|
||||
render={({ field, fieldState }) => {
|
||||
const isInvalid = fieldState.invalid
|
||||
return (
|
||||
<Field data-invalid={isInvalid}>
|
||||
<FieldLabel htmlFor={field.name}>
|
||||
Billing Period
|
||||
</FieldLabel>
|
||||
<Select
|
||||
name={field.name}
|
||||
value={field.value}
|
||||
onValueChange={field.onChange}
|
||||
aria-invalid={isInvalid}
|
||||
>
|
||||
<SelectTrigger id={field.name}>
|
||||
<SelectValue placeholder="Select billing period" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="monthly">Monthly</SelectItem>
|
||||
<SelectItem value="yearly">Yearly</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FieldDescription>
|
||||
Choose how often you want to be billed.
|
||||
</FieldDescription>
|
||||
{isInvalid && <FieldError errors={[fieldState.error]} />}
|
||||
</Field>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<FieldSeparator />
|
||||
<Controller
|
||||
name="addons"
|
||||
control={form.control}
|
||||
render={({ field, fieldState }) => {
|
||||
const isInvalid = fieldState.invalid
|
||||
return (
|
||||
<FieldSet data-invalid={isInvalid}>
|
||||
<FieldLegend>Add-ons</FieldLegend>
|
||||
<FieldDescription>
|
||||
Select additional features you'd like to include.
|
||||
</FieldDescription>
|
||||
<FieldGroup data-slot="checkbox-group">
|
||||
{addons.map((addon) => (
|
||||
<Field key={addon.id} orientation="horizontal">
|
||||
<Checkbox
|
||||
id={addon.id}
|
||||
name={field.name}
|
||||
aria-invalid={isInvalid}
|
||||
checked={field.value.includes(addon.id)}
|
||||
onCheckedChange={(checked) => {
|
||||
const newValue = checked
|
||||
? [...field.value, addon.id]
|
||||
: field.value.filter(
|
||||
(value) => value !== addon.id
|
||||
)
|
||||
field.onChange(newValue)
|
||||
field.onBlur()
|
||||
}}
|
||||
/>
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor={addon.id}>
|
||||
{addon.title}
|
||||
</FieldLabel>
|
||||
<FieldDescription>
|
||||
{addon.description}
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
</Field>
|
||||
))}
|
||||
</FieldGroup>
|
||||
{isInvalid && <FieldError errors={[fieldState.error]} />}
|
||||
</FieldSet>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<FieldSeparator />
|
||||
<Controller
|
||||
name="teamSize"
|
||||
control={form.control}
|
||||
render={({ field, fieldState }) => {
|
||||
const isInvalid = fieldState.invalid
|
||||
return (
|
||||
<Field data-invalid={isInvalid}>
|
||||
<FieldTitle>Team Size</FieldTitle>
|
||||
<FieldDescription>
|
||||
How many people will be using the subscription?
|
||||
</FieldDescription>
|
||||
<Slider
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
value={[field.value]}
|
||||
onValueChange={field.onChange}
|
||||
min={1}
|
||||
max={50}
|
||||
step={1}
|
||||
aria-invalid={isInvalid}
|
||||
/>
|
||||
{isInvalid && <FieldError errors={[fieldState.error]} />}
|
||||
</Field>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<FieldSeparator />
|
||||
<Controller
|
||||
name="emailNotifications"
|
||||
control={form.control}
|
||||
render={({ field, fieldState }) => {
|
||||
const isInvalid = fieldState.invalid
|
||||
return (
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor={field.name}>
|
||||
Email Notifications
|
||||
</FieldLabel>
|
||||
<FieldDescription>
|
||||
Receive email updates about your subscription
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<Switch
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
aria-invalid={isInvalid}
|
||||
/>
|
||||
{isInvalid && <FieldError errors={[fieldState.error]} />}
|
||||
</Field>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<FieldSeparator />
|
||||
<Controller
|
||||
name="startDate"
|
||||
control={form.control}
|
||||
render={({ field, fieldState }) => {
|
||||
const isInvalid = fieldState.invalid
|
||||
return (
|
||||
<Field data-invalid={isInvalid}>
|
||||
<FieldLabel htmlFor={field.name}>Start Date</FieldLabel>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
id={field.name}
|
||||
variant="outline"
|
||||
className="justify-start"
|
||||
aria-invalid={isInvalid}
|
||||
>
|
||||
{field.value ? (
|
||||
format(field.value, "PPP")
|
||||
) : (
|
||||
<span>Pick a date</span>
|
||||
)}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0" align="start">
|
||||
<Calendar
|
||||
required
|
||||
mode="single"
|
||||
selected={field.value}
|
||||
onSelect={field.onChange}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<FieldDescription>
|
||||
Choose when your subscription should start
|
||||
</FieldDescription>
|
||||
{isInvalid && <FieldError errors={[fieldState.error]} />}
|
||||
</Field>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<FieldSeparator />
|
||||
<Controller
|
||||
name="theme"
|
||||
control={form.control}
|
||||
render={({ field, fieldState }) => {
|
||||
const isInvalid = fieldState.invalid
|
||||
return (
|
||||
<Field data-invalid={isInvalid}>
|
||||
<FieldTitle>Theme Preference</FieldTitle>
|
||||
<ToggleGroup
|
||||
type="single"
|
||||
variant="outline"
|
||||
value={field.value}
|
||||
onValueChange={(value) =>
|
||||
value && field.onChange(value)
|
||||
}
|
||||
aria-invalid={isInvalid}
|
||||
>
|
||||
<ToggleGroupItem value="light">Light</ToggleGroupItem>
|
||||
<ToggleGroupItem value="dark">Dark</ToggleGroupItem>
|
||||
<ToggleGroupItem value="system">System</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
<FieldDescription>
|
||||
Choose your preferred color theme
|
||||
</FieldDescription>
|
||||
{isInvalid && <FieldError errors={[fieldState.error]} />}
|
||||
</Field>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<FieldSeparator />
|
||||
<Controller
|
||||
name="password"
|
||||
control={form.control}
|
||||
render={({ field, fieldState }) => {
|
||||
const isInvalid = fieldState.invalid
|
||||
return (
|
||||
<Field data-invalid={isInvalid}>
|
||||
<FieldLabel htmlFor={field.name}>Password</FieldLabel>
|
||||
<Input
|
||||
{...field}
|
||||
type="password"
|
||||
placeholder="Enter your password"
|
||||
id={field.name}
|
||||
aria-invalid={isInvalid}
|
||||
/>
|
||||
<FieldDescription>
|
||||
Must contain uppercase, lowercase, number, and be 8+
|
||||
characters
|
||||
</FieldDescription>
|
||||
{isInvalid && <FieldError errors={[fieldState.error]} />}
|
||||
</Field>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<FieldSeparator />
|
||||
<Controller
|
||||
name="comments"
|
||||
control={form.control}
|
||||
render={({ field, fieldState }) => {
|
||||
const isInvalid = fieldState.invalid
|
||||
return (
|
||||
<Field data-invalid={isInvalid}>
|
||||
<FieldLabel htmlFor={field.name}>
|
||||
Additional Comments
|
||||
</FieldLabel>
|
||||
<Textarea
|
||||
{...field}
|
||||
id={field.name}
|
||||
placeholder="Tell us more about your needs..."
|
||||
rows={3}
|
||||
aria-invalid={isInvalid}
|
||||
/>
|
||||
<FieldDescription>
|
||||
Share any additional requirements or feedback (10-240
|
||||
characters)
|
||||
</FieldDescription>
|
||||
{isInvalid && <FieldError errors={[fieldState.error]} />}
|
||||
</Field>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</FieldGroup>
|
||||
</form>
|
||||
</CardContent>
|
||||
<CardFooter className="border-t">
|
||||
<Field orientation="horizontal" className="justify-end">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => form.reset()}
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
<Button type="submit" form="subscription-form">
|
||||
Submit
|
||||
</Button>
|
||||
</Field>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Submitted Values</DialogTitle>
|
||||
<DialogDescription>
|
||||
Here are the values you submitted.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<pre className="overflow-x-auto rounded-md bg-black p-4 font-mono text-sm text-white">
|
||||
<code>{JSON.stringify(values, null, 2)}</code>
|
||||
</pre>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { ExampleForm } from "@/app/(internal)/sink/(pages)/react-hook-form/example-form"
|
||||
|
||||
export default function ReactHookFormPage() {
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center p-4">
|
||||
<ExampleForm />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
97
apps/v4/app/(internal)/sink/(pages)/schema.ts
Normal file
97
apps/v4/app/(internal)/sink/(pages)/schema.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import { z } from "zod"
|
||||
|
||||
export const addons = [
|
||||
{
|
||||
id: "analytics",
|
||||
title: "Analytics",
|
||||
description: "Advanced analytics and reporting",
|
||||
},
|
||||
{
|
||||
id: "backup",
|
||||
title: "Backup",
|
||||
description: "Automated daily backups",
|
||||
},
|
||||
{
|
||||
id: "support",
|
||||
title: "Priority Support",
|
||||
description: "24/7 premium customer support",
|
||||
},
|
||||
] as const
|
||||
|
||||
export const exampleFormSchema = z.object({
|
||||
name: z
|
||||
.string({
|
||||
required_error: "Name is required",
|
||||
invalid_type_error: "Name must be a string",
|
||||
})
|
||||
.min(2, "Name must be at least 2 characters")
|
||||
.max(50, "Name must be less than 50 characters")
|
||||
.refine((value) => !/\d/.test(value), {
|
||||
message: "Name must not contain numbers",
|
||||
}),
|
||||
|
||||
email: z
|
||||
.string({
|
||||
required_error: "Email is required",
|
||||
})
|
||||
.email("Please enter a valid email address"),
|
||||
|
||||
plan: z
|
||||
.string({
|
||||
required_error: "Please select a subscription plan",
|
||||
})
|
||||
.min(1, "Please select a subscription plan")
|
||||
.refine((value) => value === "basic" || value === "pro", {
|
||||
message: "Invalid plan selection. Please choose Basic or Pro",
|
||||
}),
|
||||
|
||||
billingPeriod: z
|
||||
.string({
|
||||
required_error: "Please select a billing period",
|
||||
})
|
||||
.min(1, "Please select a billing period"),
|
||||
|
||||
addons: z
|
||||
.array(z.string())
|
||||
.min(1, "Please select at least one add-on")
|
||||
.max(3, "You can select up to 3 add-ons"),
|
||||
|
||||
teamSize: z.number().min(1).max(10),
|
||||
emailNotifications: z.boolean({
|
||||
required_error: "Please choose email notification preference",
|
||||
}),
|
||||
comments: z
|
||||
.string()
|
||||
.min(10, "Comments must be at least 10 characters")
|
||||
.max(240, "Comments must not exceed 240 characters"),
|
||||
startDate: z
|
||||
.date({
|
||||
required_error: "Please select a start date",
|
||||
invalid_type_error: "Invalid date format",
|
||||
})
|
||||
.min(new Date(), "Start date cannot be in the past")
|
||||
.refine(
|
||||
(date) => {
|
||||
const now = new Date()
|
||||
const oneWeekFromNow = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000)
|
||||
return date <= oneWeekFromNow
|
||||
},
|
||||
{
|
||||
message: "Start date must be within the current week",
|
||||
}
|
||||
),
|
||||
theme: z
|
||||
.string({
|
||||
required_error: "Please select a theme",
|
||||
})
|
||||
.min(1, "Please select a theme"),
|
||||
password: z
|
||||
.string({
|
||||
required_error: "Password is required",
|
||||
})
|
||||
.min(8, "Password must be at least 8 characters")
|
||||
.regex(
|
||||
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/,
|
||||
"Password must contain at least one uppercase letter, one lowercase letter, and one number"
|
||||
),
|
||||
})
|
||||
@@ -0,0 +1,532 @@
|
||||
/* eslint-disable react/no-children-prop */
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { useForm } from "@tanstack/react-form"
|
||||
import { format } from "date-fns"
|
||||
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import { Calendar } from "@/registry/new-york-v4/ui/calendar"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/new-york-v4/ui/card"
|
||||
import { Checkbox } from "@/registry/new-york-v4/ui/checkbox"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/registry/new-york-v4/ui/dialog"
|
||||
import {
|
||||
Field,
|
||||
FieldContent,
|
||||
FieldDescription,
|
||||
FieldError,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldLegend,
|
||||
FieldSeparator,
|
||||
FieldSet,
|
||||
FieldTitle,
|
||||
} from "@/registry/new-york-v4/ui/field"
|
||||
import { Input } from "@/registry/new-york-v4/ui/input"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/registry/new-york-v4/ui/popover"
|
||||
import {
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
} from "@/registry/new-york-v4/ui/radio-group"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/registry/new-york-v4/ui/select"
|
||||
import { Slider } from "@/registry/new-york-v4/ui/slider"
|
||||
import { Switch } from "@/registry/new-york-v4/ui/switch"
|
||||
import { Textarea } from "@/registry/new-york-v4/ui/textarea"
|
||||
import {
|
||||
ToggleGroup,
|
||||
ToggleGroupItem,
|
||||
} from "@/registry/new-york-v4/ui/toggle-group"
|
||||
import { addons, exampleFormSchema } from "@/app/(internal)/sink/(pages)/schema"
|
||||
|
||||
export function ExampleForm() {
|
||||
const form = useForm({
|
||||
defaultValues: {
|
||||
name: "",
|
||||
email: "",
|
||||
plan: "",
|
||||
billingPeriod: "",
|
||||
addons: ["analytics"],
|
||||
emailNotifications: false,
|
||||
teamSize: 1,
|
||||
comments: "",
|
||||
startDate: new Date(),
|
||||
theme: "system",
|
||||
password: "",
|
||||
},
|
||||
validators: {
|
||||
onChange: exampleFormSchema,
|
||||
},
|
||||
onSubmit: async ({ value }) => {
|
||||
setValues(value)
|
||||
setOpen(true)
|
||||
},
|
||||
})
|
||||
const [values, setValues] = React.useState<typeof form.state.values>()
|
||||
const [open, setOpen] = React.useState(false)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card className="w-full max-w-sm">
|
||||
<CardHeader className="border-b">
|
||||
<CardTitle>Example Form</CardTitle>
|
||||
<CardDescription>
|
||||
This is an example form using TanStack Form.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form
|
||||
id="example-form"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
void form.handleSubmit()
|
||||
}}
|
||||
>
|
||||
<FieldGroup>
|
||||
<form.Field
|
||||
name="name"
|
||||
children={(field) => {
|
||||
const isInvalid =
|
||||
field.state.meta.isTouched && !field.state.meta.isValid
|
||||
return (
|
||||
<Field data-invalid={isInvalid}>
|
||||
<FieldLabel htmlFor={field.name}>Name</FieldLabel>
|
||||
<Input
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
value={field.state.value}
|
||||
onBlur={field.handleBlur}
|
||||
onChange={(e) => field.handleChange(e.target.value)}
|
||||
aria-invalid={isInvalid}
|
||||
autoComplete="off"
|
||||
/>
|
||||
<FieldDescription>Enter your name</FieldDescription>
|
||||
{isInvalid && (
|
||||
<FieldError errors={field.state.meta.errors} />
|
||||
)}
|
||||
</Field>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<form.Field
|
||||
name="email"
|
||||
children={(field) => {
|
||||
const isInvalid =
|
||||
field.state.meta.isTouched && !field.state.meta.isValid
|
||||
return (
|
||||
<Field data-invalid={isInvalid}>
|
||||
<FieldLabel htmlFor={field.name}>Email</FieldLabel>
|
||||
<Input
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
type="email"
|
||||
value={field.state.value}
|
||||
onBlur={field.handleBlur}
|
||||
onChange={(e) => field.handleChange(e.target.value)}
|
||||
aria-invalid={isInvalid}
|
||||
autoComplete="off"
|
||||
/>
|
||||
<FieldDescription>
|
||||
Enter your email address
|
||||
</FieldDescription>
|
||||
{isInvalid && (
|
||||
<FieldError errors={field.state.meta.errors} />
|
||||
)}
|
||||
</Field>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<FieldSeparator />
|
||||
<form.Field
|
||||
name="plan"
|
||||
children={(field) => {
|
||||
const isInvalid =
|
||||
field.state.meta.isTouched && !field.state.meta.isValid
|
||||
return (
|
||||
<FieldSet data-invalid={isInvalid}>
|
||||
<FieldLegend>Subscription Plan</FieldLegend>
|
||||
<FieldDescription>
|
||||
Choose your subscription plan.
|
||||
</FieldDescription>
|
||||
<RadioGroup
|
||||
name={field.name}
|
||||
value={field.state.value}
|
||||
onValueChange={field.handleChange}
|
||||
aria-invalid={isInvalid}
|
||||
>
|
||||
<FieldLabel htmlFor="basic">
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldTitle>Basic</FieldTitle>
|
||||
<FieldDescription>
|
||||
For individuals and small teams
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<RadioGroupItem
|
||||
value="basic"
|
||||
id="basic"
|
||||
aria-invalid={isInvalid}
|
||||
/>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
<FieldLabel htmlFor="pro">
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldTitle>Pro</FieldTitle>
|
||||
<FieldDescription>
|
||||
For businesses with higher demands
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<RadioGroupItem
|
||||
value="pro"
|
||||
id="pro"
|
||||
aria-invalid={isInvalid}
|
||||
/>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
</RadioGroup>
|
||||
{isInvalid && (
|
||||
<FieldError errors={field.state.meta.errors} />
|
||||
)}
|
||||
</FieldSet>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<FieldSeparator />
|
||||
<form.Field
|
||||
name="billingPeriod"
|
||||
children={(field) => {
|
||||
const isInvalid =
|
||||
field.state.meta.isTouched && !field.state.meta.isValid
|
||||
return (
|
||||
<Field data-invalid={isInvalid}>
|
||||
<FieldLabel htmlFor={field.name}>
|
||||
Billing Period
|
||||
</FieldLabel>
|
||||
<Select
|
||||
name={field.name}
|
||||
value={field.state.value}
|
||||
onValueChange={field.handleChange}
|
||||
aria-invalid={isInvalid}
|
||||
>
|
||||
<SelectTrigger id={field.name}>
|
||||
<SelectValue placeholder="Select billing period" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="monthly">Monthly</SelectItem>
|
||||
<SelectItem value="yearly">Yearly</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FieldDescription>
|
||||
Choose how often you want to be billed.
|
||||
</FieldDescription>
|
||||
{isInvalid && (
|
||||
<FieldError errors={field.state.meta.errors} />
|
||||
)}
|
||||
</Field>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<FieldSeparator />
|
||||
<form.Field
|
||||
name="addons"
|
||||
mode="array"
|
||||
children={(field) => {
|
||||
const isInvalid =
|
||||
field.state.meta.isTouched && !field.state.meta.isValid
|
||||
return (
|
||||
<FieldSet data-invalid={isInvalid}>
|
||||
<FieldLegend>Add-ons</FieldLegend>
|
||||
<FieldDescription>
|
||||
Select additional features you'd like to include.
|
||||
</FieldDescription>
|
||||
<FieldGroup data-slot="checkbox-group">
|
||||
{addons.map((addon) => (
|
||||
<Field key={addon.id} orientation="horizontal">
|
||||
<Checkbox
|
||||
id={addon.id}
|
||||
name={field.name}
|
||||
aria-invalid={isInvalid}
|
||||
checked={field.state.value.includes(addon.id)}
|
||||
onCheckedChange={(checked) => {
|
||||
if (checked) {
|
||||
field.pushValue(addon.id)
|
||||
} else {
|
||||
const index = field.state.value.indexOf(
|
||||
addon.id
|
||||
)
|
||||
if (index > -1) {
|
||||
field.removeValue(index)
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor={addon.id}>
|
||||
{addon.title}
|
||||
</FieldLabel>
|
||||
<FieldDescription>
|
||||
{addon.description}
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
</Field>
|
||||
))}
|
||||
</FieldGroup>
|
||||
{isInvalid && (
|
||||
<FieldError errors={field.state.meta.errors} />
|
||||
)}
|
||||
</FieldSet>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<FieldSeparator />
|
||||
<form.Field
|
||||
name="teamSize"
|
||||
children={(field) => {
|
||||
const isInvalid =
|
||||
field.state.meta.isTouched && !field.state.meta.isValid
|
||||
return (
|
||||
<Field data-invalid={isInvalid}>
|
||||
<FieldTitle>Team Size</FieldTitle>
|
||||
<FieldDescription>
|
||||
How many people will be using the subscription?
|
||||
</FieldDescription>
|
||||
<Slider
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
value={[field.state.value]}
|
||||
onValueChange={(value) => field.handleChange(value[0])}
|
||||
min={1}
|
||||
max={50}
|
||||
step={10}
|
||||
/>
|
||||
{isInvalid && (
|
||||
<FieldError errors={field.state.meta.errors} />
|
||||
)}
|
||||
</Field>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<FieldSeparator />
|
||||
<form.Field
|
||||
name="emailNotifications"
|
||||
children={(field) => {
|
||||
const isInvalid =
|
||||
field.state.meta.isTouched && !field.state.meta.isValid
|
||||
return (
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor={field.name}>
|
||||
Email Notifications
|
||||
</FieldLabel>
|
||||
<FieldDescription>
|
||||
Receive email updates about your subscription
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<Switch
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
checked={field.state.value}
|
||||
onCheckedChange={field.handleChange}
|
||||
/>
|
||||
{isInvalid && (
|
||||
<FieldError errors={field.state.meta.errors} />
|
||||
)}
|
||||
</Field>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<FieldSeparator />
|
||||
<form.Field
|
||||
name="startDate"
|
||||
children={(field) => {
|
||||
const isInvalid =
|
||||
field.state.meta.isTouched && !field.state.meta.isValid
|
||||
return (
|
||||
<Field data-invalid={isInvalid}>
|
||||
<FieldLabel htmlFor={field.name}>Start Date</FieldLabel>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
id={field.name}
|
||||
variant="outline"
|
||||
className="justify-start"
|
||||
>
|
||||
{field.state.value ? (
|
||||
format(field.state.value, "PPP")
|
||||
) : (
|
||||
<span>Pick a date</span>
|
||||
)}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0" align="start">
|
||||
<Calendar
|
||||
required
|
||||
mode="single"
|
||||
selected={field.state.value}
|
||||
onSelect={field.handleChange}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<FieldDescription>
|
||||
Choose when your subscription should start
|
||||
</FieldDescription>
|
||||
{isInvalid && (
|
||||
<FieldError errors={field.state.meta.errors} />
|
||||
)}
|
||||
</Field>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<FieldSeparator />
|
||||
<form.Field
|
||||
name="theme"
|
||||
children={(field) => {
|
||||
const isInvalid =
|
||||
field.state.meta.isTouched && !field.state.meta.isValid
|
||||
return (
|
||||
<Field data-invalid={isInvalid}>
|
||||
<FieldTitle>Theme Preference</FieldTitle>
|
||||
<ToggleGroup
|
||||
id={field.name}
|
||||
type="single"
|
||||
variant="outline"
|
||||
value={field.state.value}
|
||||
onValueChange={(value) =>
|
||||
value && field.handleChange(value)
|
||||
}
|
||||
aria-invalid={isInvalid}
|
||||
>
|
||||
<ToggleGroupItem value="light">Light</ToggleGroupItem>
|
||||
<ToggleGroupItem value="dark">Dark</ToggleGroupItem>
|
||||
<ToggleGroupItem value="system">System</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
<FieldDescription>
|
||||
Choose your preferred color theme
|
||||
</FieldDescription>
|
||||
{isInvalid && (
|
||||
<FieldError errors={field.state.meta.errors} />
|
||||
)}
|
||||
</Field>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<FieldSeparator />
|
||||
<form.Field
|
||||
name="password"
|
||||
children={(field) => {
|
||||
const isInvalid =
|
||||
field.state.meta.isTouched && !field.state.meta.isValid
|
||||
return (
|
||||
<Field data-invalid={isInvalid}>
|
||||
<FieldLabel htmlFor={field.name}>Password</FieldLabel>
|
||||
<Input
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
type="password"
|
||||
value={field.state.value}
|
||||
onChange={(e) => field.handleChange(e.target.value)}
|
||||
onBlur={field.handleBlur}
|
||||
placeholder="Enter your password"
|
||||
aria-invalid={isInvalid}
|
||||
/>
|
||||
<FieldDescription>
|
||||
Must contain uppercase, lowercase, number, and be 8+
|
||||
characters
|
||||
</FieldDescription>
|
||||
{isInvalid && (
|
||||
<FieldError errors={field.state.meta.errors} />
|
||||
)}
|
||||
</Field>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<FieldSeparator />
|
||||
<form.Field
|
||||
name="comments"
|
||||
children={(field) => {
|
||||
const isInvalid =
|
||||
field.state.meta.isTouched && !field.state.meta.isValid
|
||||
return (
|
||||
<Field data-invalid={isInvalid}>
|
||||
<FieldLabel htmlFor={field.name}>
|
||||
Additional Comments
|
||||
</FieldLabel>
|
||||
<Textarea
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
value={field.state.value}
|
||||
onChange={(e) => field.handleChange(e.target.value)}
|
||||
onBlur={field.handleBlur}
|
||||
placeholder="Tell us more about your needs..."
|
||||
rows={3}
|
||||
aria-invalid={isInvalid}
|
||||
/>
|
||||
<FieldDescription>
|
||||
Share any additional requirements or feedback (10-240
|
||||
characters)
|
||||
</FieldDescription>
|
||||
{isInvalid && (
|
||||
<FieldError errors={field.state.meta.errors} />
|
||||
)}
|
||||
</Field>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</FieldGroup>
|
||||
</form>
|
||||
</CardContent>
|
||||
<CardFooter className="border-t">
|
||||
<Field orientation="horizontal" className="justify-end">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => form.reset()}
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
<Button type="submit" form="example-form">
|
||||
Submit
|
||||
</Button>
|
||||
</Field>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Submitted Values</DialogTitle>
|
||||
<DialogDescription>
|
||||
Here are the values you submitted.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<pre className="overflow-x-auto rounded-md bg-black p-4 font-mono text-sm text-white">
|
||||
<code>{JSON.stringify(values, null, 2)}</code>
|
||||
</pre>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { ExampleForm } from "@/app/(internal)/sink/(pages)/tanstack-form/example-form"
|
||||
|
||||
export default function TanstackFormPage() {
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center p-4">
|
||||
<ExampleForm />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
54
apps/v4/app/(internal)/sink/[name]/page.tsx
Normal file
54
apps/v4/app/(internal)/sink/[name]/page.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import { Metadata } from "next"
|
||||
import { notFound } from "next/navigation"
|
||||
|
||||
import { componentRegistry } from "@/app/(internal)/sink/component-registry"
|
||||
|
||||
export const dynamic = "force-static"
|
||||
export const revalidate = false
|
||||
|
||||
export async function generateStaticParams() {
|
||||
return Object.keys(componentRegistry).map((name) => ({
|
||||
name,
|
||||
}))
|
||||
}
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ name: string }>
|
||||
}): Promise<Metadata> {
|
||||
const { name } = await params
|
||||
const component = componentRegistry[name as keyof typeof componentRegistry]
|
||||
|
||||
if (!component) {
|
||||
return {
|
||||
title: "Component Not Found",
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
title: `${component.name} - Kitchen Sink`,
|
||||
description: `Demo page for ${component.name} component`,
|
||||
}
|
||||
}
|
||||
|
||||
export default async function ComponentPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ name: string }>
|
||||
}) {
|
||||
const { name } = await params
|
||||
const component = componentRegistry[name as keyof typeof componentRegistry]
|
||||
|
||||
if (!component) {
|
||||
notFound()
|
||||
}
|
||||
|
||||
const Component = component.component
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<Component />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
429
apps/v4/app/(internal)/sink/component-registry.ts
Normal file
429
apps/v4/app/(internal)/sink/component-registry.ts
Normal file
@@ -0,0 +1,429 @@
|
||||
import FormsPage from "@/app/(internal)/sink/(pages)/forms/page"
|
||||
|
||||
import NextFormPage from "./(pages)/next-form/page"
|
||||
import ReactHookFormPage from "./(pages)/react-hook-form/page"
|
||||
import TanstackFormPage from "./(pages)/tanstack-form/page"
|
||||
import { AccordionDemo } from "./components/accordion-demo"
|
||||
import { AlertDemo } from "./components/alert-demo"
|
||||
import { AlertDialogDemo } from "./components/alert-dialog-demo"
|
||||
import { AspectRatioDemo } from "./components/aspect-ratio-demo"
|
||||
import { AvatarDemo } from "./components/avatar-demo"
|
||||
import { BadgeDemo } from "./components/badge-demo"
|
||||
import { BreadcrumbDemo } from "./components/breadcrumb-demo"
|
||||
import { ButtonDemo } from "./components/button-demo"
|
||||
import { ButtonGroupDemo } from "./components/button-group-demo"
|
||||
import { CalendarDemo } from "./components/calendar-demo"
|
||||
import { CardDemo } from "./components/card-demo"
|
||||
import { CarouselDemo } from "./components/carousel-demo"
|
||||
import { ChartDemo } from "./components/chart-demo"
|
||||
import { CheckboxDemo } from "./components/checkbox-demo"
|
||||
import { CollapsibleDemo } from "./components/collapsible-demo"
|
||||
import { ComboboxDemo } from "./components/combobox-demo"
|
||||
import { CommandDemo } from "./components/command-demo"
|
||||
import { ContextMenuDemo } from "./components/context-menu-demo"
|
||||
import { DatePickerDemo } from "./components/date-picker-demo"
|
||||
import { DialogDemo } from "./components/dialog-demo"
|
||||
import { DrawerDemo } from "./components/drawer-demo"
|
||||
import { DropdownMenuDemo } from "./components/dropdown-menu-demo"
|
||||
import { EmptyDemo } from "./components/empty-demo"
|
||||
import { FieldDemo } from "./components/field-demo"
|
||||
import { FormDemo } from "./components/form-demo"
|
||||
import { HoverCardDemo } from "./components/hover-card-demo"
|
||||
import { InputDemo } from "./components/input-demo"
|
||||
import { InputGroupDemo } from "./components/input-group-demo"
|
||||
import { InputOTPDemo } from "./components/input-otp-demo"
|
||||
import { ItemDemo } from "./components/item-demo"
|
||||
import { KbdDemo } from "./components/kbd-demo"
|
||||
import { LabelDemo } from "./components/label-demo"
|
||||
import { MenubarDemo } from "./components/menubar-demo"
|
||||
import { NavigationMenuDemo } from "./components/navigation-menu-demo"
|
||||
import { PaginationDemo } from "./components/pagination-demo"
|
||||
import { PopoverDemo } from "./components/popover-demo"
|
||||
import { ProgressDemo } from "./components/progress-demo"
|
||||
import { RadioGroupDemo } from "./components/radio-group-demo"
|
||||
import { ResizableDemo } from "./components/resizable-demo"
|
||||
import { ScrollAreaDemo } from "./components/scroll-area-demo"
|
||||
import { SelectDemo } from "./components/select-demo"
|
||||
import { SeparatorDemo } from "./components/separator-demo"
|
||||
import { SheetDemo } from "./components/sheet-demo"
|
||||
import { SkeletonDemo } from "./components/skeleton-demo"
|
||||
import { SliderDemo } from "./components/slider-demo"
|
||||
import { SonnerDemo } from "./components/sonner-demo"
|
||||
import { SpinnerDemo } from "./components/spinner-demo"
|
||||
import { SwitchDemo } from "./components/switch-demo"
|
||||
import { TableDemo } from "./components/table-demo"
|
||||
import { TabsDemo } from "./components/tabs-demo"
|
||||
import { TextareaDemo } from "./components/textarea-demo"
|
||||
import { ToggleDemo } from "./components/toggle-demo"
|
||||
import { ToggleGroupDemo } from "./components/toggle-group-demo"
|
||||
import { TooltipDemo } from "./components/tooltip-demo"
|
||||
|
||||
type ComponentConfig = {
|
||||
name: string
|
||||
component: React.ComponentType
|
||||
className?: string
|
||||
type: "registry:ui" | "registry:page" | "registry:block"
|
||||
href: string
|
||||
label?: string
|
||||
}
|
||||
|
||||
export const componentRegistry: Record<string, ComponentConfig> = {
|
||||
accordion: {
|
||||
name: "Accordion",
|
||||
component: AccordionDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/accordion",
|
||||
},
|
||||
alert: {
|
||||
name: "Alert",
|
||||
component: AlertDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/alert",
|
||||
},
|
||||
"alert-dialog": {
|
||||
name: "Alert Dialog",
|
||||
component: AlertDialogDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/alert-dialog",
|
||||
},
|
||||
"aspect-ratio": {
|
||||
name: "Aspect Ratio",
|
||||
component: AspectRatioDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/aspect-ratio",
|
||||
},
|
||||
avatar: {
|
||||
name: "Avatar",
|
||||
component: AvatarDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/avatar",
|
||||
},
|
||||
badge: {
|
||||
name: "Badge",
|
||||
component: BadgeDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/badge",
|
||||
},
|
||||
breadcrumb: {
|
||||
name: "Breadcrumb",
|
||||
component: BreadcrumbDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/breadcrumb",
|
||||
},
|
||||
button: {
|
||||
name: "Button",
|
||||
component: ButtonDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/button",
|
||||
},
|
||||
"button-group": {
|
||||
name: "Button Group",
|
||||
component: ButtonGroupDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/button-group",
|
||||
label: "New",
|
||||
},
|
||||
calendar: {
|
||||
name: "Calendar",
|
||||
component: CalendarDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/calendar",
|
||||
},
|
||||
card: {
|
||||
name: "Card",
|
||||
component: CardDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/card",
|
||||
},
|
||||
carousel: {
|
||||
name: "Carousel",
|
||||
component: CarouselDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/carousel",
|
||||
},
|
||||
chart: {
|
||||
name: "Chart",
|
||||
component: ChartDemo,
|
||||
className: "w-full",
|
||||
type: "registry:ui",
|
||||
href: "/sink/chart",
|
||||
},
|
||||
checkbox: {
|
||||
name: "Checkbox",
|
||||
component: CheckboxDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/checkbox",
|
||||
},
|
||||
collapsible: {
|
||||
name: "Collapsible",
|
||||
component: CollapsibleDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/collapsible",
|
||||
},
|
||||
combobox: {
|
||||
name: "Combobox",
|
||||
component: ComboboxDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/combobox",
|
||||
},
|
||||
command: {
|
||||
name: "Command",
|
||||
component: CommandDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/command",
|
||||
},
|
||||
"context-menu": {
|
||||
name: "Context Menu",
|
||||
component: ContextMenuDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/context-menu",
|
||||
},
|
||||
"date-picker": {
|
||||
name: "Date Picker",
|
||||
component: DatePickerDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/date-picker",
|
||||
},
|
||||
dialog: {
|
||||
name: "Dialog",
|
||||
component: DialogDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/dialog",
|
||||
},
|
||||
drawer: {
|
||||
name: "Drawer",
|
||||
component: DrawerDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/drawer",
|
||||
},
|
||||
"dropdown-menu": {
|
||||
name: "Dropdown Menu",
|
||||
component: DropdownMenuDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/dropdown-menu",
|
||||
},
|
||||
empty: {
|
||||
name: "Empty",
|
||||
component: EmptyDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/empty",
|
||||
label: "New",
|
||||
},
|
||||
field: {
|
||||
name: "Field",
|
||||
component: FieldDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/field",
|
||||
label: "New",
|
||||
},
|
||||
form: {
|
||||
name: "Form",
|
||||
component: FormDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/form",
|
||||
},
|
||||
"hover-card": {
|
||||
name: "Hover Card",
|
||||
component: HoverCardDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/hover-card",
|
||||
},
|
||||
input: {
|
||||
name: "Input",
|
||||
component: InputDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/input",
|
||||
},
|
||||
"input-group": {
|
||||
name: "Input Group",
|
||||
component: InputGroupDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/input-group",
|
||||
label: "New",
|
||||
},
|
||||
"input-otp": {
|
||||
name: "Input OTP",
|
||||
component: InputOTPDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/input-otp",
|
||||
},
|
||||
item: {
|
||||
name: "Item",
|
||||
component: ItemDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/item",
|
||||
label: "New",
|
||||
},
|
||||
kbd: {
|
||||
name: "Kbd",
|
||||
component: KbdDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/kbd",
|
||||
label: "New",
|
||||
},
|
||||
label: {
|
||||
name: "Label",
|
||||
component: LabelDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/label",
|
||||
},
|
||||
menubar: {
|
||||
name: "Menubar",
|
||||
component: MenubarDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/menubar",
|
||||
},
|
||||
"navigation-menu": {
|
||||
name: "Navigation Menu",
|
||||
component: NavigationMenuDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/navigation-menu",
|
||||
},
|
||||
pagination: {
|
||||
name: "Pagination",
|
||||
component: PaginationDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/pagination",
|
||||
},
|
||||
popover: {
|
||||
name: "Popover",
|
||||
component: PopoverDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/popover",
|
||||
},
|
||||
progress: {
|
||||
name: "Progress",
|
||||
component: ProgressDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/progress",
|
||||
},
|
||||
"radio-group": {
|
||||
name: "Radio Group",
|
||||
component: RadioGroupDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/radio-group",
|
||||
},
|
||||
resizable: {
|
||||
name: "Resizable",
|
||||
component: ResizableDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/resizable",
|
||||
},
|
||||
"scroll-area": {
|
||||
name: "Scroll Area",
|
||||
component: ScrollAreaDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/scroll-area",
|
||||
},
|
||||
select: {
|
||||
name: "Select",
|
||||
component: SelectDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/select",
|
||||
},
|
||||
separator: {
|
||||
name: "Separator",
|
||||
component: SeparatorDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/separator",
|
||||
},
|
||||
sheet: {
|
||||
name: "Sheet",
|
||||
component: SheetDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/sheet",
|
||||
},
|
||||
skeleton: {
|
||||
name: "Skeleton",
|
||||
component: SkeletonDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/skeleton",
|
||||
},
|
||||
slider: {
|
||||
name: "Slider",
|
||||
component: SliderDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/slider",
|
||||
},
|
||||
sonner: {
|
||||
name: "Sonner",
|
||||
component: SonnerDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/sonner",
|
||||
},
|
||||
spinner: {
|
||||
name: "Spinner",
|
||||
component: SpinnerDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/spinner",
|
||||
label: "New",
|
||||
},
|
||||
switch: {
|
||||
name: "Switch",
|
||||
component: SwitchDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/switch",
|
||||
},
|
||||
table: {
|
||||
name: "Table",
|
||||
component: TableDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/table",
|
||||
},
|
||||
tabs: {
|
||||
name: "Tabs",
|
||||
component: TabsDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/tabs",
|
||||
},
|
||||
textarea: {
|
||||
name: "Textarea",
|
||||
component: TextareaDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/textarea",
|
||||
},
|
||||
toggle: {
|
||||
name: "Toggle",
|
||||
component: ToggleDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/toggle",
|
||||
},
|
||||
"toggle-group": {
|
||||
name: "Toggle Group",
|
||||
component: ToggleGroupDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/toggle-group",
|
||||
},
|
||||
tooltip: {
|
||||
name: "Tooltip",
|
||||
component: TooltipDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/tooltip",
|
||||
},
|
||||
blocks: {
|
||||
name: "Forms",
|
||||
component: FormsPage,
|
||||
type: "registry:page",
|
||||
href: "/sink/forms",
|
||||
},
|
||||
"next-form": {
|
||||
name: "Next.js Form",
|
||||
component: NextFormPage,
|
||||
type: "registry:page",
|
||||
href: "/sink/next-form",
|
||||
},
|
||||
"tanstack-form": {
|
||||
name: "Tanstack Form",
|
||||
component: TanstackFormPage,
|
||||
type: "registry:page",
|
||||
href: "/sink/tanstack-form",
|
||||
},
|
||||
"react-hook-form": {
|
||||
name: "React Hook Form",
|
||||
component: ReactHookFormPage,
|
||||
type: "registry:page",
|
||||
href: "/sink/react-hook-form",
|
||||
},
|
||||
}
|
||||
|
||||
export type ComponentKey = keyof typeof componentRegistry
|
||||
43
apps/v4/app/(internal)/sink/components/app-breadcrumbs.tsx
Normal file
43
apps/v4/app/(internal)/sink/components/app-breadcrumbs.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
"use client"
|
||||
|
||||
import { useParams } from "next/navigation"
|
||||
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbList,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
} from "@/registry/new-york-v4/ui/breadcrumb"
|
||||
|
||||
export function AppBreadcrumbs() {
|
||||
const params = useParams()
|
||||
const { name } = params
|
||||
|
||||
if (!name) {
|
||||
return (
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbPage>Kitchen Sink</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink href="/sink">Kitchen Sink</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator className="hidden sm:flex" />
|
||||
<BreadcrumbItem className="hidden sm:block">
|
||||
<BreadcrumbPage className="capitalize">{name}</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
)
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import * as React from "react"
|
||||
import Link from "next/link"
|
||||
import { usePathname } from "next/navigation"
|
||||
import {
|
||||
AudioWaveform,
|
||||
BookOpen,
|
||||
@@ -9,7 +10,7 @@ import {
|
||||
ChevronRightIcon,
|
||||
Command,
|
||||
GalleryVerticalEnd,
|
||||
Search,
|
||||
SearchIcon,
|
||||
Settings2,
|
||||
SquareTerminal,
|
||||
} from "lucide-react"
|
||||
@@ -22,6 +23,11 @@ import {
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from "@/registry/new-york-v4/ui/collapsible"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupInput,
|
||||
} from "@/registry/new-york-v4/ui/input-group"
|
||||
import { Label } from "@/registry/new-york-v4/ui/label"
|
||||
import {
|
||||
Sidebar,
|
||||
@@ -31,7 +37,6 @@ import {
|
||||
SidebarGroupContent,
|
||||
SidebarGroupLabel,
|
||||
SidebarHeader,
|
||||
SidebarInput,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
@@ -40,6 +45,7 @@ import {
|
||||
SidebarMenuSubItem,
|
||||
SidebarRail,
|
||||
} from "@/registry/new-york-v4/ui/sidebar"
|
||||
import { componentRegistry } from "@/app/(internal)/sink/component-registry"
|
||||
|
||||
// This is sample data.
|
||||
const data = {
|
||||
@@ -163,8 +169,9 @@ const data = {
|
||||
}
|
||||
|
||||
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
const pathname = usePathname()
|
||||
return (
|
||||
<Sidebar collapsible="icon" {...props}>
|
||||
<Sidebar side="left" collapsible="icon" {...props}>
|
||||
<SidebarHeader>
|
||||
<TeamSwitcher teams={data.teams} />
|
||||
<SidebarGroup className="py-0 group-data-[collapsible=icon]:hidden">
|
||||
@@ -173,12 +180,17 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
<Label htmlFor="search" className="sr-only">
|
||||
Search
|
||||
</Label>
|
||||
<SidebarInput
|
||||
id="search"
|
||||
placeholder="Search the docs..."
|
||||
className="pl-8"
|
||||
/>
|
||||
<Search className="pointer-events-none absolute top-1/2 left-2 size-4 -translate-y-1/2 opacity-50 select-none" />
|
||||
<InputGroup className="bg-background h-8 shadow-none">
|
||||
<InputGroupInput
|
||||
id="search"
|
||||
placeholder="Search the docs..."
|
||||
className="h-7"
|
||||
data-slot="input-group-control"
|
||||
/>
|
||||
<InputGroupAddon>
|
||||
<SearchIcon className="text-muted-foreground" />
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</form>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
@@ -221,17 +233,58 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
</SidebarMenu>
|
||||
</SidebarGroup>
|
||||
<SidebarGroup className="group-data-[collapsible=icon]:hidden">
|
||||
<SidebarGroupLabel>Components</SidebarGroupLabel>
|
||||
<SidebarMenu>
|
||||
{data.components.map((item) => (
|
||||
<SidebarMenuItem key={item.name}>
|
||||
<SidebarMenuButton asChild>
|
||||
<Link href={`/sink#${item.name}`}>
|
||||
<span>{getComponentName(item.name)}</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
))}
|
||||
{["registry:ui", "registry:page", "registry:block"].map((type) => {
|
||||
const typeComponents = Object.entries(componentRegistry).filter(
|
||||
([, item]) => item.type === type
|
||||
)
|
||||
if (typeComponents.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Collapsible
|
||||
key={type}
|
||||
asChild
|
||||
defaultOpen={pathname.includes("/sink/")}
|
||||
className="group/collapsible"
|
||||
>
|
||||
<SidebarMenuItem>
|
||||
<CollapsibleTrigger asChild>
|
||||
<SidebarMenuButton>
|
||||
<span>
|
||||
{type === "registry:ui"
|
||||
? "Components"
|
||||
: type === "registry:page"
|
||||
? "Pages"
|
||||
: "Blocks"}
|
||||
</span>
|
||||
<ChevronRightIcon className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
|
||||
</SidebarMenuButton>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<SidebarMenuSub>
|
||||
{typeComponents.map(([key, item]) => (
|
||||
<SidebarMenuSubItem key={key}>
|
||||
<SidebarMenuSubButton
|
||||
asChild
|
||||
isActive={pathname === item.href}
|
||||
>
|
||||
<Link href={item.href}>
|
||||
<span>{item.name}</span>
|
||||
{item.label && (
|
||||
<span className="flex size-2 rounded-full bg-blue-500" />
|
||||
)}
|
||||
</Link>
|
||||
</SidebarMenuSubButton>
|
||||
</SidebarMenuSubItem>
|
||||
))}
|
||||
</SidebarMenuSub>
|
||||
</CollapsibleContent>
|
||||
</SidebarMenuItem>
|
||||
</Collapsible>
|
||||
)
|
||||
})}
|
||||
</SidebarMenu>
|
||||
</SidebarGroup>
|
||||
</SidebarContent>
|
||||
@@ -242,8 +295,3 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
</Sidebar>
|
||||
)
|
||||
}
|
||||
|
||||
function getComponentName(name: string) {
|
||||
// convert kebab-case to title case
|
||||
return name.replace(/-/g, " ").replace(/\b\w/g, (char) => char.toUpperCase())
|
||||
}
|
||||
|
||||
@@ -31,7 +31,10 @@ export function AvatarDemo() {
|
||||
<AvatarFallback>CN</AvatarFallback>
|
||||
</Avatar>
|
||||
<Avatar>
|
||||
<AvatarImage src="https://github.com/leerob.png" alt="@leerob" />
|
||||
<AvatarImage
|
||||
src="https://github.com/maxleiter.png"
|
||||
alt="@maxleiter"
|
||||
/>
|
||||
<AvatarFallback>LR</AvatarFallback>
|
||||
</Avatar>
|
||||
<Avatar>
|
||||
@@ -48,7 +51,10 @@ export function AvatarDemo() {
|
||||
<AvatarFallback>CN</AvatarFallback>
|
||||
</Avatar>
|
||||
<Avatar>
|
||||
<AvatarImage src="https://github.com/leerob.png" alt="@leerob" />
|
||||
<AvatarImage
|
||||
src="https://github.com/maxleiter.png"
|
||||
alt="@maxleiter"
|
||||
/>
|
||||
<AvatarFallback>LR</AvatarFallback>
|
||||
</Avatar>
|
||||
<Avatar>
|
||||
@@ -65,7 +71,10 @@ export function AvatarDemo() {
|
||||
<AvatarFallback>CN</AvatarFallback>
|
||||
</Avatar>
|
||||
<Avatar>
|
||||
<AvatarImage src="https://github.com/leerob.png" alt="@leerob" />
|
||||
<AvatarImage
|
||||
src="https://github.com/maxleiter.png"
|
||||
alt="@maxleiter"
|
||||
/>
|
||||
<AvatarFallback>LR</AvatarFallback>
|
||||
</Avatar>
|
||||
<Avatar>
|
||||
|
||||
581
apps/v4/app/(internal)/sink/components/button-group-demo.tsx
Normal file
581
apps/v4/app/(internal)/sink/components/button-group-demo.tsx
Normal file
@@ -0,0 +1,581 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import {
|
||||
IconArrowRight,
|
||||
IconBrandGithubCopilot,
|
||||
IconChevronDown,
|
||||
IconCircleCheck,
|
||||
IconCloudCode,
|
||||
IconHeart,
|
||||
IconMinus,
|
||||
IconPin,
|
||||
IconPlus,
|
||||
IconUserCircle,
|
||||
} from "@tabler/icons-react"
|
||||
import {
|
||||
AlertTriangleIcon,
|
||||
ArrowLeftIcon,
|
||||
ArrowRightIcon,
|
||||
AudioLinesIcon,
|
||||
CheckIcon,
|
||||
ChevronDownIcon,
|
||||
CopyIcon,
|
||||
FlipHorizontalIcon,
|
||||
FlipVerticalIcon,
|
||||
MoreHorizontalIcon,
|
||||
PercentIcon,
|
||||
RotateCwIcon,
|
||||
SearchIcon,
|
||||
ShareIcon,
|
||||
TrashIcon,
|
||||
UserRoundXIcon,
|
||||
VolumeOffIcon,
|
||||
} from "lucide-react"
|
||||
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import {
|
||||
ButtonGroup,
|
||||
ButtonGroupSeparator,
|
||||
ButtonGroupText,
|
||||
} from "@/registry/new-york-v4/ui/button-group"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/registry/new-york-v4/ui/dropdown-menu"
|
||||
import { Field, FieldGroup } from "@/registry/new-york-v4/ui/field"
|
||||
import { Input } from "@/registry/new-york-v4/ui/input"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupInput,
|
||||
} from "@/registry/new-york-v4/ui/input-group"
|
||||
import { Label } from "@/registry/new-york-v4/ui/label"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/registry/new-york-v4/ui/popover"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/registry/new-york-v4/ui/select"
|
||||
import { Separator } from "@/registry/new-york-v4/ui/separator"
|
||||
import { Textarea } from "@/registry/new-york-v4/ui/textarea"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/registry/new-york-v4/ui/tooltip"
|
||||
|
||||
export function ButtonGroupDemo() {
|
||||
const [currency, setCurrency] = useState("$")
|
||||
return (
|
||||
<div className="flex gap-12">
|
||||
<div className="flex max-w-sm flex-col gap-6">
|
||||
<ButtonGroup>
|
||||
<Button>Button</Button>
|
||||
<Button>
|
||||
Get Started <IconArrowRight />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup>
|
||||
<Button>Button</Button>
|
||||
<ButtonGroupSeparator className="bg-primary/80" />
|
||||
<Button>
|
||||
Get Started <IconArrowRight />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup>
|
||||
<Button variant="outline">Button</Button>
|
||||
<Input placeholder="Type something here..." />
|
||||
</ButtonGroup>
|
||||
<ButtonGroup>
|
||||
<Input placeholder="Type something here..." />
|
||||
<Button variant="outline">Button</Button>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup>
|
||||
<Button variant="outline">Button</Button>
|
||||
<Button variant="outline">Another Button</Button>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup>
|
||||
<ButtonGroupText>Text</ButtonGroupText>
|
||||
<Button variant="outline">Another Button</Button>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup>
|
||||
<ButtonGroupText asChild>
|
||||
<Label htmlFor="input">
|
||||
<IconCloudCode /> GPU Size
|
||||
</Label>
|
||||
</ButtonGroupText>
|
||||
<Input id="input" placeholder="Type something here..." />
|
||||
</ButtonGroup>
|
||||
<ButtonGroup>
|
||||
<ButtonGroupText>Prefix</ButtonGroupText>
|
||||
<Input id="input" placeholder="Type something here..." />
|
||||
<ButtonGroupText>Suffix</ButtonGroupText>
|
||||
</ButtonGroup>
|
||||
<div className="flex gap-4">
|
||||
<ButtonGroup>
|
||||
<Button variant="outline">Update</Button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline">
|
||||
<ChevronDownIcon />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem>Disable</DropdownMenuItem>
|
||||
<DropdownMenuItem variant="destructive">
|
||||
Uninstall
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup className="[--radius:9999px]">
|
||||
<Button variant="outline">Follow</Button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" className="!pl-2">
|
||||
<ChevronDownIcon />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="[--radius:0.95rem]">
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>
|
||||
<VolumeOffIcon />
|
||||
Mute Conversation
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<CheckIcon />
|
||||
Mark as Read
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<AlertTriangleIcon />
|
||||
Report Conversation
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<UserRoundXIcon />
|
||||
Block User
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<ShareIcon />
|
||||
Share Conversation
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<CopyIcon />
|
||||
Copy Conversation
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem variant="destructive">
|
||||
<TrashIcon />
|
||||
Delete Conversation
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup className="[--radius:0.9rem]">
|
||||
<Button variant="secondary">Actions</Button>
|
||||
<ButtonGroupSeparator />
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="secondary">
|
||||
<MoreHorizontalIcon />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="[--radius:0.9rem]">
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>
|
||||
<IconCircleCheck />
|
||||
Select Messages
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<IconPin />
|
||||
Edit Pins
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<IconUserCircle />
|
||||
Set Up Name & Photo
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
<Field>
|
||||
<Label htmlFor="amount">Amount</Label>
|
||||
<ButtonGroup>
|
||||
<Select value={currency} onValueChange={setCurrency}>
|
||||
<SelectTrigger className="font-mono">{currency}</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="$">$</SelectItem>
|
||||
<SelectItem value="€">€</SelectItem>
|
||||
<SelectItem value="£">£</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Input placeholder="Enter amount to send" />
|
||||
<Button variant="outline">
|
||||
<ArrowRightIcon />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</Field>
|
||||
</div>
|
||||
<div className="flex max-w-xs flex-col gap-6">
|
||||
<ButtonGroup className="[--spacing:0.2rem]">
|
||||
<Button variant="outline">
|
||||
<FlipHorizontalIcon />
|
||||
</Button>
|
||||
<Button variant="outline">
|
||||
<FlipVerticalIcon />
|
||||
</Button>
|
||||
<Button variant="outline">
|
||||
<RotateCwIcon />
|
||||
</Button>
|
||||
<InputGroup>
|
||||
<InputGroupInput placeholder="0.00" />
|
||||
<InputGroupAddon
|
||||
align="inline-end"
|
||||
className="text-muted-foreground"
|
||||
>
|
||||
<PercentIcon />
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</ButtonGroup>
|
||||
<div className="flex gap-2 [--radius:0.95rem] [--ring:var(--color-blue-300)] [--spacing:0.22rem] **:[.shadow-xs]:shadow-none">
|
||||
<InputGroup>
|
||||
<InputGroupInput placeholder="Type to search..." />
|
||||
<InputGroupAddon
|
||||
align="inline-start"
|
||||
className="text-muted-foreground"
|
||||
>
|
||||
<SearchIcon />
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<ButtonGroup>
|
||||
<Button variant="outline">
|
||||
<IconBrandGithubCopilot />
|
||||
</Button>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline">
|
||||
<IconCloudCode />
|
||||
<IconChevronDown />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent align="end" className="rounded-xl p-0 text-sm">
|
||||
<div className="px-4 py-3">
|
||||
<div className="text-sm font-medium">Agent Tasks</div>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="p-4 *:[p:not(:last-child)]:mb-2">
|
||||
<Textarea
|
||||
placeholder="Describe your task in natural language."
|
||||
className="mb-4 resize-none"
|
||||
/>
|
||||
<p className="font-medium">Start a new task with Copilot</p>
|
||||
<p className="text-muted-foreground">
|
||||
Describe your task in natural language. Copilot will work in
|
||||
the background and open a pull request for your review.
|
||||
</p>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
<FieldGroup className="grid grid-cols-2 gap-4 [--spacing:0.22rem]">
|
||||
<Field>
|
||||
<Label htmlFor="width">Width</Label>
|
||||
<ButtonGroup>
|
||||
<InputGroup>
|
||||
<InputGroupInput id="width" />
|
||||
<InputGroupAddon className="text-muted-foreground">
|
||||
W
|
||||
</InputGroupAddon>
|
||||
<InputGroupAddon
|
||||
align="inline-end"
|
||||
className="text-muted-foreground"
|
||||
>
|
||||
px
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<Button variant="outline" size="icon">
|
||||
<IconMinus />
|
||||
</Button>
|
||||
<Button variant="outline" size="icon">
|
||||
<IconPlus />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</Field>
|
||||
<Field className="w-full">
|
||||
<Label htmlFor="color">Color</Label>
|
||||
<ButtonGroup className="w-full">
|
||||
<InputGroup>
|
||||
<InputGroupInput id="color" />
|
||||
<InputGroupAddon align="inline-start">
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<InputGroupButton>
|
||||
<span className="size-4 rounded-xs bg-blue-600" />
|
||||
</InputGroupButton>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
align="start"
|
||||
className="max-w-48 rounded-lg p-2"
|
||||
alignOffset={-8}
|
||||
sideOffset={8}
|
||||
>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{[
|
||||
"#EA4335", // Red
|
||||
"#FBBC04", // Yellow
|
||||
"#34A853", // Green
|
||||
"#4285F4", // Blue
|
||||
"#9333EA", // Purple
|
||||
"#EC4899", // Pink
|
||||
"#10B981", // Emerald
|
||||
"#F97316", // Orange
|
||||
"#6366F1", // Indigo
|
||||
"#14B8A6", // Teal
|
||||
"#8B5CF6", // Violet
|
||||
"#F59E0B", // Amber
|
||||
].map((color) => (
|
||||
<div
|
||||
key={color}
|
||||
className="size-6 cursor-pointer rounded-sm transition-transform hover:scale-110"
|
||||
style={{ backgroundColor: color }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</InputGroupAddon>
|
||||
<InputGroupAddon
|
||||
align="inline-end"
|
||||
className="text-muted-foreground"
|
||||
>
|
||||
%
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</ButtonGroup>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
<ButtonGroup>
|
||||
<Button variant="outline">
|
||||
<IconHeart /> Like
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
asChild
|
||||
className="text-muted-foreground pointer-events-none px-2"
|
||||
>
|
||||
<span>1.2K</span>
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<ExportButtonGroup />
|
||||
<ButtonGroup>
|
||||
<Select defaultValue="hours">
|
||||
<SelectTrigger id="duration">
|
||||
<SelectValue placeholder="Select duration" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="hours">Hours</SelectItem>
|
||||
<SelectItem value="days">Days</SelectItem>
|
||||
<SelectItem value="weeks">Weeks</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Input />
|
||||
</ButtonGroup>
|
||||
<ButtonGroup className="[--radius:9999rem]">
|
||||
<ButtonGroup>
|
||||
<Button variant="outline" size="icon">
|
||||
<IconPlus />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup>
|
||||
<InputGroup>
|
||||
<InputGroupInput placeholder="Send a message..." />
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<InputGroupAddon align="inline-end">
|
||||
<AudioLinesIcon />
|
||||
</InputGroupAddon>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Voice Mode</TooltipContent>
|
||||
</Tooltip>
|
||||
</InputGroup>
|
||||
</ButtonGroup>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup>
|
||||
<Button variant="outline" size="sm">
|
||||
<ArrowLeftIcon />
|
||||
Previous
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
1
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
2
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
3
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
4
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
5
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
Next
|
||||
<ArrowRightIcon />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup className="[--radius:0.9rem] [--spacing:0.22rem]">
|
||||
<ButtonGroup>
|
||||
<Button variant="outline">1</Button>
|
||||
<Button variant="outline">2</Button>
|
||||
<Button variant="outline">3</Button>
|
||||
<Button variant="outline">4</Button>
|
||||
<Button variant="outline">5</Button>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup>
|
||||
<Button variant="outline" size="icon">
|
||||
<ArrowLeftIcon />
|
||||
</Button>
|
||||
<Button variant="outline" size="icon">
|
||||
<ArrowRightIcon />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup>
|
||||
<ButtonGroup>
|
||||
<Button variant="outline">
|
||||
<ArrowLeftIcon />
|
||||
</Button>
|
||||
<Button variant="outline">
|
||||
<ArrowRightIcon />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup aria-label="Single navigation button">
|
||||
<Button variant="outline" size="icon">
|
||||
<ArrowLeftIcon />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
<div className="flex max-w-xs flex-col gap-6">
|
||||
<Field>
|
||||
<Label id="alignment-label">Text Alignment</Label>
|
||||
<ButtonGroup aria-labelledby="alignment-label">
|
||||
<Button variant="outline" size="sm">
|
||||
Left
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
Center
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
Right
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
Justify
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</Field>
|
||||
<div className="flex gap-6">
|
||||
<ButtonGroup
|
||||
orientation="vertical"
|
||||
aria-label="Media controls"
|
||||
className="h-fit"
|
||||
>
|
||||
<Button variant="outline" size="icon">
|
||||
<IconPlus />
|
||||
</Button>
|
||||
<Button variant="outline" size="icon">
|
||||
<IconMinus />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup orientation="vertical" aria-label="Design tools palette">
|
||||
<ButtonGroup orientation="vertical">
|
||||
<Button variant="outline" size="icon">
|
||||
<SearchIcon />
|
||||
</Button>
|
||||
<Button variant="outline" size="icon">
|
||||
<CopyIcon />
|
||||
</Button>
|
||||
<Button variant="outline" size="icon">
|
||||
<ShareIcon />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup orientation="vertical">
|
||||
<Button variant="outline" size="icon">
|
||||
<FlipHorizontalIcon />
|
||||
</Button>
|
||||
<Button variant="outline" size="icon">
|
||||
<FlipVerticalIcon />
|
||||
</Button>
|
||||
<Button variant="outline" size="icon">
|
||||
<RotateCwIcon />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup>
|
||||
<Button variant="outline" size="icon">
|
||||
<TrashIcon />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup orientation="vertical">
|
||||
<Button variant="outline" size="sm">
|
||||
<IconPlus /> Increase
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
<IconMinus /> Decrease
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup orientation="vertical">
|
||||
<Button variant="secondary" size="sm">
|
||||
<IconPlus /> Increase
|
||||
</Button>
|
||||
<ButtonGroupSeparator orientation="horizontal" />
|
||||
<Button variant="secondary" size="sm">
|
||||
<IconMinus /> Decrease
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ExportButtonGroup() {
|
||||
const [exportType, setExportType] = useState("pdf")
|
||||
|
||||
return (
|
||||
<ButtonGroup>
|
||||
<Input />
|
||||
<Select value={exportType} onValueChange={setExportType}>
|
||||
<SelectTrigger>
|
||||
<SelectValue asChild>
|
||||
<span>{exportType}</span>
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent align="end">
|
||||
<SelectItem value="pdf">pdf</SelectItem>
|
||||
<SelectItem value="xlsx">xlsx</SelectItem>
|
||||
<SelectItem value="csv">csv</SelectItem>
|
||||
<SelectItem value="json">json</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</ButtonGroup>
|
||||
)
|
||||
}
|
||||
@@ -98,7 +98,10 @@ export function CardDemo() {
|
||||
<AvatarFallback>CN</AvatarFallback>
|
||||
</Avatar>
|
||||
<Avatar>
|
||||
<AvatarImage src="https://github.com/leerob.png" alt="@leerob" />
|
||||
<AvatarImage
|
||||
src="https://github.com/maxleiter.png"
|
||||
alt="@maxleiter"
|
||||
/>
|
||||
<AvatarFallback>LR</AvatarFallback>
|
||||
</Avatar>
|
||||
<Avatar>
|
||||
|
||||
@@ -62,7 +62,7 @@ const users = [
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
username: "leerob",
|
||||
username: "maxleiter",
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
|
||||
@@ -274,7 +274,10 @@ function DropdownMenuAvatarOnly() {
|
||||
className="size-8 rounded-full border-none p-0"
|
||||
>
|
||||
<Avatar>
|
||||
<AvatarImage src="https://github.com/leerob.png" alt="leerob" />
|
||||
<AvatarImage
|
||||
src="https://github.com/maxleiter.png"
|
||||
alt="maxleiter"
|
||||
/>
|
||||
<AvatarFallback className="rounded-lg">LR</AvatarFallback>
|
||||
</Avatar>
|
||||
</Button>
|
||||
@@ -286,13 +289,16 @@ function DropdownMenuAvatarOnly() {
|
||||
<DropdownMenuLabel className="p-0 font-normal">
|
||||
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
|
||||
<Avatar>
|
||||
<AvatarImage src="https://github.com/leerob.png" alt="leerob" />
|
||||
<AvatarImage
|
||||
src="https://github.com/maxleiter.png"
|
||||
alt="maxleiter"
|
||||
/>
|
||||
<AvatarFallback className="rounded-lg">LR</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||
<span className="truncate font-semibold">leerob</span>
|
||||
<span className="truncate font-semibold">maxleiter</span>
|
||||
<span className="text-muted-foreground truncate text-xs">
|
||||
leerob@example.com
|
||||
maxleiter@example.com
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
250
apps/v4/app/(internal)/sink/components/empty-demo.tsx
Normal file
250
apps/v4/app/(internal)/sink/components/empty-demo.tsx
Normal file
@@ -0,0 +1,250 @@
|
||||
import { IconArrowUpRight, IconFolderCode } from "@tabler/icons-react"
|
||||
import { PlusIcon, SearchIcon } from "lucide-react"
|
||||
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import { Card, CardContent } from "@/registry/new-york-v4/ui/card"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/registry/new-york-v4/ui/dialog"
|
||||
import {
|
||||
Empty,
|
||||
EmptyContent,
|
||||
EmptyDescription,
|
||||
EmptyHeader,
|
||||
EmptyMedia,
|
||||
EmptyTitle,
|
||||
} from "@/registry/new-york-v4/ui/empty"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupInput,
|
||||
} from "@/registry/new-york-v4/ui/input-group"
|
||||
import { Kbd } from "@/registry/new-york-v4/ui/kbd"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/registry/new-york-v4/ui/popover"
|
||||
|
||||
export function EmptyDemo() {
|
||||
return (
|
||||
<div className="grid w-full gap-8">
|
||||
<Empty className="min-h-[80svh]">
|
||||
<EmptyHeader>
|
||||
<EmptyMedia variant="icon">
|
||||
<IconFolderCode />
|
||||
</EmptyMedia>
|
||||
<EmptyTitle>No projects yet</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
You haven't created any projects yet. Get started by creating
|
||||
your first project.
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
<EmptyContent>
|
||||
<div className="flex gap-2">
|
||||
<Button asChild>
|
||||
<a href="#">Create project</a>
|
||||
</Button>
|
||||
<Button variant="outline">Import project</Button>
|
||||
</div>
|
||||
<Button variant="link" asChild className="text-muted-foreground">
|
||||
<a href="#">
|
||||
Learn more <IconArrowUpRight />
|
||||
</a>
|
||||
</Button>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
<Empty className="bg-muted min-h-[80svh]">
|
||||
<EmptyHeader>
|
||||
<EmptyTitle>No results found</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
No results found for your search. Try adjusting your search terms.
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
<EmptyContent>
|
||||
<Button>Try again</Button>
|
||||
<Button variant="link" asChild className="text-muted-foreground">
|
||||
<a href="#">
|
||||
Learn more <IconArrowUpRight />
|
||||
</a>
|
||||
</Button>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
<Empty className="min-h-[80svh] border">
|
||||
<EmptyHeader>
|
||||
<EmptyTitle>404 - Not Found</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
The page you're looking for doesn't exist. Try searching
|
||||
for what you need below.
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
<EmptyContent>
|
||||
<InputGroup className="w-3/4">
|
||||
<InputGroupInput placeholder="Try searching for pages..." />
|
||||
<InputGroupAddon>
|
||||
<SearchIcon />
|
||||
</InputGroupAddon>
|
||||
<InputGroupAddon align="inline-end">
|
||||
<Kbd>/</Kbd>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<EmptyDescription>
|
||||
Need help? <a href="#">Contact support</a>
|
||||
</EmptyDescription>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
<Empty className="min-h-[80svh]">
|
||||
<EmptyHeader>
|
||||
<EmptyTitle>Nothing to see here</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
No posts have been created yet. Get started by{" "}
|
||||
<a href="#">creating your first post</a> to share with the
|
||||
community.
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
<EmptyContent>
|
||||
<Button variant="outline">
|
||||
<PlusIcon />
|
||||
New Post
|
||||
</Button>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
<div className="bg-muted flex min-h-[800px] items-center justify-center rounded-lg p-20">
|
||||
<Card className="max-w-sm">
|
||||
<CardContent>
|
||||
<Empty className="p-4">
|
||||
<EmptyHeader>
|
||||
<EmptyTitle>404 - Not Found</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
The page you're looking for doesn't exist. Try
|
||||
searching for what you need below.
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
<EmptyContent>
|
||||
<InputGroup className="w-3/4">
|
||||
<InputGroupInput placeholder="Try searching for pages..." />
|
||||
<InputGroupAddon>
|
||||
<SearchIcon />
|
||||
</InputGroupAddon>
|
||||
<InputGroupAddon align="inline-end">
|
||||
<Kbd>/</Kbd>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<EmptyDescription>
|
||||
Need help? <a href="#">Contact support</a>
|
||||
</EmptyDescription>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
<div className="bg-muted flex min-h-[800px] items-center justify-center rounded-lg p-20">
|
||||
<Card className="max-w-sm">
|
||||
<CardContent>
|
||||
<Empty className="p-4">
|
||||
<EmptyHeader>
|
||||
<EmptyMedia variant="icon">
|
||||
<IconFolderCode />
|
||||
</EmptyMedia>
|
||||
<EmptyTitle>No projects yet</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
You haven't created any projects yet. Get started by
|
||||
creating your first project.
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
<EmptyContent>
|
||||
<div className="flex gap-2">
|
||||
<Button asChild>
|
||||
<a href="#">Create project</a>
|
||||
</Button>
|
||||
<Button variant="outline">Import project</Button>
|
||||
</div>
|
||||
<Button
|
||||
variant="link"
|
||||
asChild
|
||||
className="text-muted-foreground"
|
||||
>
|
||||
<a href="#">
|
||||
Learn more <IconArrowUpRight />
|
||||
</a>
|
||||
</Button>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-6">
|
||||
<div className="flex gap-4">
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button>Open Dialog</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader className="sr-only">
|
||||
<DialogTitle>Dialog Title</DialogTitle>
|
||||
<DialogDescription>Dialog Description</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Empty className="p-4">
|
||||
<EmptyHeader>
|
||||
<EmptyMedia variant="icon">
|
||||
<IconFolderCode />
|
||||
</EmptyMedia>
|
||||
<EmptyTitle>No projects yet</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
You haven't created any projects yet. Get started by
|
||||
creating your first project.
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
<EmptyContent>
|
||||
<div className="flex gap-2">
|
||||
<Button asChild>
|
||||
<a href="#">Create project</a>
|
||||
</Button>
|
||||
<Button variant="outline">Import project</Button>
|
||||
</div>
|
||||
<Button
|
||||
variant="link"
|
||||
asChild
|
||||
className="text-muted-foreground"
|
||||
>
|
||||
<a href="#">
|
||||
Learn more <IconArrowUpRight />
|
||||
</a>
|
||||
</Button>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline">Open Popover</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="rounded-2xl p-2">
|
||||
<Empty className="rounded-sm p-6">
|
||||
<EmptyHeader>
|
||||
<EmptyTitle>Nothing to see here</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
No posts have been created yet.{" "}
|
||||
<a href="#">Create your first post</a> to share with the
|
||||
community.
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
<EmptyContent>
|
||||
<Button variant="outline">
|
||||
<PlusIcon />
|
||||
New Post
|
||||
</Button>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
4957
apps/v4/app/(internal)/sink/components/field-demo.tsx
Normal file
4957
apps/v4/app/(internal)/sink/components/field-demo.tsx
Normal file
File diff suppressed because it is too large
Load Diff
663
apps/v4/app/(internal)/sink/components/input-group-demo.tsx
Normal file
663
apps/v4/app/(internal)/sink/components/input-group-demo.tsx
Normal file
@@ -0,0 +1,663 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import {
|
||||
IconBrandJavascript,
|
||||
IconCheck,
|
||||
IconChevronDown,
|
||||
IconCopy,
|
||||
IconInfoCircle,
|
||||
IconLoader2,
|
||||
IconMicrophone,
|
||||
IconPlayerRecordFilled,
|
||||
IconPlus,
|
||||
IconRefresh,
|
||||
IconSearch,
|
||||
IconServerSpark,
|
||||
IconStar,
|
||||
IconTrash,
|
||||
} from "@tabler/icons-react"
|
||||
import {
|
||||
ArrowRightIcon,
|
||||
ArrowUpIcon,
|
||||
ChevronDownIcon,
|
||||
EyeClosedIcon,
|
||||
FlipVerticalIcon,
|
||||
SearchIcon,
|
||||
} from "lucide-react"
|
||||
import { toast } from "sonner"
|
||||
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import {
|
||||
ButtonGroup,
|
||||
ButtonGroupText,
|
||||
} from "@/registry/new-york-v4/ui/button-group"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/registry/new-york-v4/ui/dropdown-menu"
|
||||
import {
|
||||
Field,
|
||||
FieldDescription,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
} from "@/registry/new-york-v4/ui/field"
|
||||
import { Input } from "@/registry/new-york-v4/ui/input"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupInput,
|
||||
InputGroupText,
|
||||
InputGroupTextarea,
|
||||
} from "@/registry/new-york-v4/ui/input-group"
|
||||
import { Kbd, KbdGroup } from "@/registry/new-york-v4/ui/kbd"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/registry/new-york-v4/ui/popover"
|
||||
import { Spinner } from "@/registry/new-york-v4/ui/spinner"
|
||||
import { Textarea } from "@/registry/new-york-v4/ui/textarea"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/registry/new-york-v4/ui/tooltip"
|
||||
|
||||
export function InputGroupDemo() {
|
||||
const [country, setCountry] = useState("+1")
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-wrap gap-12 pb-72 *:[div]:w-full *:[div]:max-w-sm">
|
||||
<div className="flex flex-col gap-10">
|
||||
<Field>
|
||||
<FieldLabel htmlFor="input-default-01">
|
||||
Default (No Input Group)
|
||||
</FieldLabel>
|
||||
<Input placeholder="Default" id="input-default-01" />
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="input-group-02">Input Group</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupInput id="input-group-02" placeholder="Default" />
|
||||
</InputGroup>
|
||||
</Field>
|
||||
<Field data-disabled="true">
|
||||
<FieldLabel htmlFor="input-disabled-03">Disabled</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupInput
|
||||
id="input-disabled-03"
|
||||
placeholder="This field is disabled"
|
||||
disabled
|
||||
/>
|
||||
</InputGroup>
|
||||
</Field>
|
||||
<Field data-invalid="true">
|
||||
<FieldLabel htmlFor="input-invalid-04">Invalid</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupInput
|
||||
id="input-invalid-04"
|
||||
placeholder="This field is invalid"
|
||||
aria-invalid="true"
|
||||
/>
|
||||
</InputGroup>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="input-icon-left-05">Icon (left)</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupInput id="input-icon-left-05" />
|
||||
<InputGroupAddon>
|
||||
<SearchIcon className="text-muted-foreground" />
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<InputGroup>
|
||||
<InputGroupInput id="input-icon-left-06" />
|
||||
<InputGroupAddon>
|
||||
<FlipVerticalIcon className="text-muted-foreground" />
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="input-icon-right-07">Icon (right)</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupInput id="input-icon-right-07" />
|
||||
<InputGroupAddon align="inline-end">
|
||||
<EyeClosedIcon />
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<InputGroup>
|
||||
<InputGroupInput id="input-icon-right-08" />
|
||||
<InputGroupAddon align="inline-end">
|
||||
<IconLoader2 className="text-muted-foreground animate-spin" />
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="input-icon-both-09">Icon (both)</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupInput id="input-icon-both-09" />
|
||||
<InputGroupAddon>
|
||||
<IconMicrophone className="text-muted-foreground" />
|
||||
</InputGroupAddon>
|
||||
<InputGroupAddon align="inline-end">
|
||||
<IconPlayerRecordFilled className="animate-pulse text-red-500" />
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="input-icon-both-10">Multiple Icons</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupInput id="input-icon-both-10" />
|
||||
<InputGroupAddon align="inline-end">
|
||||
<IconStar />
|
||||
<InputGroupButton
|
||||
size="icon-xs"
|
||||
onClick={() => toast.success("Copied to clipboard")}
|
||||
>
|
||||
<IconCopy />
|
||||
</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
<InputGroupAddon>
|
||||
<IconPlayerRecordFilled className="animate-pulse text-red-500" />
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="input-description-10">Description</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupInput id="input-description-10" />
|
||||
<InputGroupAddon align="inline-end">
|
||||
<IconInfoCircle />
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<FieldDescription>
|
||||
This is a description of the input group.
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
<FieldGroup className="grid grid-cols-2 gap-4">
|
||||
<Field>
|
||||
<FieldLabel htmlFor="input-group-11">First Name</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupInput id="input-group-11" placeholder="First Name" />
|
||||
<InputGroupAddon align="inline-end">
|
||||
<IconInfoCircle />
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="input-group-12">Last Name</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupInput id="input-group-12" placeholder="Last Name" />
|
||||
<InputGroupAddon align="inline-end">
|
||||
<IconInfoCircle />
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</div>
|
||||
<div className="flex flex-col gap-10">
|
||||
<Field>
|
||||
<FieldLabel htmlFor="input-tooltip-20">Tooltip</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupInput id="input-tooltip-20" />
|
||||
<InputGroupAddon align="inline-end">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<InputGroupButton className="rounded-full" size="icon-xs">
|
||||
<IconInfoCircle />
|
||||
</InputGroupButton>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>This is content in a tooltip.</TooltipContent>
|
||||
</Tooltip>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="input-dropdown-21">Dropdown</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupInput id="input-dropdown-21" />
|
||||
<InputGroupAddon>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<InputGroupButton className="text-muted-foreground tabular-nums">
|
||||
{country} <ChevronDownIcon />
|
||||
</InputGroupButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="start"
|
||||
className="min-w-16"
|
||||
sideOffset={10}
|
||||
alignOffset={-8}
|
||||
>
|
||||
<DropdownMenuItem onClick={() => setCountry("+1")}>
|
||||
+1
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setCountry("+44")}>
|
||||
+44
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setCountry("+46")}>
|
||||
+46
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="input-label-10">Label</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupAddon>
|
||||
<FieldLabel htmlFor="input-label-10">Label</FieldLabel>
|
||||
</InputGroupAddon>
|
||||
<InputGroupInput id="input-label-10" />
|
||||
</InputGroup>
|
||||
<InputGroup className="gap-0">
|
||||
<InputGroupAddon>
|
||||
<FieldLabel
|
||||
htmlFor="input-prefix-11"
|
||||
className="text-muted-foreground"
|
||||
>
|
||||
example.com/
|
||||
</FieldLabel>
|
||||
</InputGroupAddon>
|
||||
<InputGroupInput id="input-prefix-11" />
|
||||
</InputGroup>
|
||||
<InputGroup>
|
||||
<InputGroupInput id="input-optional-12" />
|
||||
<InputGroupAddon align="inline-end">
|
||||
<InputGroupText>(optional)</InputGroupText>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="input-button-13">Button</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupInput id="input-button-13" />
|
||||
<InputGroupAddon>
|
||||
<InputGroupButton>Button</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<InputGroup>
|
||||
<InputGroupInput id="input-button-14" />
|
||||
<InputGroupAddon>
|
||||
<InputGroupButton variant="outline">Button</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<InputGroup>
|
||||
<InputGroupInput id="input-button-15" />
|
||||
<InputGroupAddon>
|
||||
<InputGroupButton variant="secondary">Button</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<InputGroup>
|
||||
<InputGroupInput id="input-button-16" />
|
||||
<InputGroupAddon align="inline-end">
|
||||
<InputGroupButton variant="secondary">Button</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<InputGroup>
|
||||
<InputGroupInput id="input-button-17" />
|
||||
<InputGroupAddon align="inline-end">
|
||||
<InputGroupButton size="icon-xs">
|
||||
<IconCopy />
|
||||
</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<InputGroup>
|
||||
<InputGroupInput id="input-button-18" />
|
||||
<InputGroupAddon align="inline-end">
|
||||
<InputGroupButton variant="secondary" size="icon-xs">
|
||||
<IconTrash />
|
||||
</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<InputGroup className="[--radius:9999px]">
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<InputGroupAddon>
|
||||
<InputGroupButton variant="secondary" size="icon-xs">
|
||||
<IconInfoCircle />
|
||||
</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
align="start"
|
||||
className="flex flex-col gap-1 rounded-xl text-sm"
|
||||
>
|
||||
<p className="font-medium">Your connection is not secure.</p>
|
||||
<p>
|
||||
You should not enter any sensitive information on this site.
|
||||
</p>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<InputGroupAddon className="text-muted-foreground">
|
||||
https://
|
||||
</InputGroupAddon>
|
||||
<InputGroupInput id="input-secure-19" />
|
||||
<InputGroupAddon align="inline-end">
|
||||
<InputGroupButton
|
||||
size="icon-xs"
|
||||
onClick={() => toast.success("Added to favorites")}
|
||||
>
|
||||
<IconStar />
|
||||
</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="input-addon-20">Addon (block-start)</FieldLabel>
|
||||
<InputGroup className="h-auto">
|
||||
<InputGroupInput id="input-addon-20" />
|
||||
<InputGroupAddon align="block-start">
|
||||
<InputGroupText>First Name</InputGroupText>
|
||||
<IconInfoCircle className="text-muted-foreground ml-auto" />
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="input-addon-21">Addon (block-end)</FieldLabel>
|
||||
<InputGroup className="h-auto">
|
||||
<InputGroupInput id="input-addon-21" />
|
||||
<InputGroupAddon align="block-end">
|
||||
<InputGroupText>20/240 characters</InputGroupText>
|
||||
<IconInfoCircle className="text-muted-foreground ml-auto" />
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="textarea-comment-33">Default Button</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupTextarea
|
||||
id="textarea-comment-33"
|
||||
placeholder="Share your thoughts..."
|
||||
className="py-2.5"
|
||||
/>
|
||||
<InputGroupAddon align="block-end">
|
||||
<ButtonGroup>
|
||||
<Button variant="outline" size="sm">
|
||||
Button
|
||||
</Button>
|
||||
<Button variant="outline" size="icon" className="size-8">
|
||||
<IconChevronDown />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<Button variant="ghost" className="ml-auto" size="sm">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="default" size="sm">
|
||||
Post <ArrowRightIcon />
|
||||
</Button>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</Field>
|
||||
</div>
|
||||
<div className="flex flex-col gap-10">
|
||||
<Field>
|
||||
<FieldLabel htmlFor="input-kbd-22">Input Group with Kbd</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupInput id="input-kbd-22" />
|
||||
<InputGroupAddon>
|
||||
<Kbd>⌘K</Kbd>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<InputGroup>
|
||||
<InputGroupInput id="input-kbd-23" />
|
||||
<InputGroupAddon align="inline-end">
|
||||
<Kbd>⌘K</Kbd>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<InputGroup>
|
||||
<InputGroupInput
|
||||
id="input-search-apps-24"
|
||||
placeholder="Search for Apps..."
|
||||
/>
|
||||
<InputGroupAddon align="inline-end">Ask AI</InputGroupAddon>
|
||||
<InputGroupAddon align="inline-end">
|
||||
<Kbd>Tab</Kbd>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<InputGroup>
|
||||
<InputGroupInput
|
||||
id="input-search-type-25"
|
||||
placeholder="Type to search..."
|
||||
/>
|
||||
<InputGroupAddon align="inline-start">
|
||||
<IconServerSpark />
|
||||
</InputGroupAddon>
|
||||
<InputGroupAddon align="inline-end">
|
||||
<KbdGroup>
|
||||
<Kbd>Ctrl</Kbd>
|
||||
<Kbd>C</Kbd>
|
||||
</KbdGroup>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="input-username-26">Username</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupInput id="input-username-26" defaultValue="shadcn" />
|
||||
<InputGroupAddon align="inline-end">
|
||||
<div className="flex size-4 items-center justify-center rounded-full bg-green-500 dark:bg-green-800">
|
||||
<IconCheck className="size-3 text-white" />
|
||||
</div>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<FieldDescription className="text-green-700">
|
||||
This username is available.
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
<InputGroup>
|
||||
<InputGroupInput
|
||||
id="input-search-docs-27"
|
||||
placeholder="Search documentation..."
|
||||
/>
|
||||
<InputGroupAddon>
|
||||
<IconSearch />
|
||||
</InputGroupAddon>
|
||||
<InputGroupAddon align="inline-end">12 results</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<InputGroup data-disabled="true">
|
||||
<InputGroupInput
|
||||
id="input-search-disabled-28"
|
||||
placeholder="Search documentation..."
|
||||
disabled
|
||||
/>
|
||||
<InputGroupAddon>
|
||||
<IconSearch />
|
||||
</InputGroupAddon>
|
||||
<InputGroupAddon align="inline-end">Disabled</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="url">With Button Group</FieldLabel>
|
||||
<ButtonGroup>
|
||||
<ButtonGroupText>https://</ButtonGroupText>
|
||||
<InputGroup>
|
||||
<InputGroupInput id="url" />
|
||||
<InputGroupAddon align="inline-end">
|
||||
<IconInfoCircle />
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<ButtonGroupText>.com</ButtonGroupText>
|
||||
</ButtonGroup>
|
||||
<FieldDescription>
|
||||
This is a description of the input group.
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
<Field data-disabled="true">
|
||||
<FieldLabel htmlFor="input-group-29">Loading</FieldLabel>
|
||||
<FieldDescription>
|
||||
This is a description of the input group.
|
||||
</FieldDescription>
|
||||
<InputGroup>
|
||||
<InputGroupInput
|
||||
id="input-group-29"
|
||||
disabled
|
||||
defaultValue="shadcn"
|
||||
/>
|
||||
<InputGroupAddon align="inline-end">
|
||||
<Spinner />
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
<FieldLabel htmlFor="textarea-code-32">Code Editor</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupTextarea
|
||||
id="textarea-code-32"
|
||||
placeholder="console.log('Hello, world!');"
|
||||
className="min-h-[300px] py-3"
|
||||
/>
|
||||
<InputGroupAddon align="block-start" className="border-b">
|
||||
<InputGroupText className="font-mono font-medium">
|
||||
<IconBrandJavascript />
|
||||
script.js
|
||||
</InputGroupText>
|
||||
<InputGroupButton size="icon-xs" className="ml-auto">
|
||||
<IconRefresh />
|
||||
</InputGroupButton>
|
||||
<InputGroupButton size="icon-xs" variant="ghost">
|
||||
<IconCopy />
|
||||
</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
<InputGroupAddon align="block-end" className="border-t">
|
||||
<InputGroupText>Line 1, Column 1</InputGroupText>
|
||||
<InputGroupText className="ml-auto">JavaScript</InputGroupText>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</Field>
|
||||
</div>
|
||||
<div className="flex flex-col gap-10">
|
||||
<Field>
|
||||
<FieldLabel htmlFor="textarea-header-footer-12">Default</FieldLabel>
|
||||
<Textarea
|
||||
id="textarea-header-footer-12"
|
||||
placeholder="Enter your text here..."
|
||||
/>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="textarea-header-footer-13">
|
||||
Input Group
|
||||
</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupTextarea
|
||||
id="textarea-header-footer-13"
|
||||
placeholder="Enter your text here..."
|
||||
/>
|
||||
</InputGroup>
|
||||
<FieldDescription>
|
||||
This is a description of the input group.
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
<Field data-invalid="true">
|
||||
<FieldLabel htmlFor="textarea-header-footer-14">Invalid</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupTextarea
|
||||
id="textarea-header-footer-14"
|
||||
placeholder="Enter your text here..."
|
||||
aria-invalid="true"
|
||||
/>
|
||||
</InputGroup>
|
||||
<FieldDescription>
|
||||
This is a description of the input group.
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
<Field data-disabled="true">
|
||||
<FieldLabel htmlFor="textarea-header-footer-15">Disabled</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupTextarea
|
||||
id="textarea-header-footer-15"
|
||||
placeholder="Enter your text here..."
|
||||
disabled
|
||||
/>
|
||||
</InputGroup>
|
||||
<FieldDescription>
|
||||
This is a description of the input group.
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="textarea-header-footer-30">Textarea</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupTextarea
|
||||
id="textarea-header-footer-30"
|
||||
placeholder="Enter your text here..."
|
||||
/>
|
||||
<InputGroupAddon align="block-end">
|
||||
<InputGroupText>0/280 characters</InputGroupText>
|
||||
<InputGroupButton
|
||||
variant="default"
|
||||
size="icon-xs"
|
||||
className="ml-auto rounded-full"
|
||||
>
|
||||
<ArrowUpIcon />
|
||||
<span className="sr-only">Send</span>
|
||||
</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="prompt-31">Enter your prompt</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupTextarea
|
||||
id="prompt-31"
|
||||
placeholder="Ask, Search or Chat..."
|
||||
/>
|
||||
<InputGroupAddon align="block-end">
|
||||
<InputGroupButton
|
||||
variant="outline"
|
||||
className="rounded-full"
|
||||
size="icon-xs"
|
||||
>
|
||||
<IconPlus />
|
||||
</InputGroupButton>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<InputGroupButton variant="ghost">Auto</InputGroupButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent side="top" align="start">
|
||||
<DropdownMenuItem>Auto</DropdownMenuItem>
|
||||
<DropdownMenuItem>Agent</DropdownMenuItem>
|
||||
<DropdownMenuItem>Manual</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<InputGroupText className="ml-auto">
|
||||
12 messages left
|
||||
</InputGroupText>
|
||||
<InputGroupButton
|
||||
variant="default"
|
||||
className="rounded-full"
|
||||
size="icon-xs"
|
||||
>
|
||||
<ArrowUpIcon />
|
||||
<span className="sr-only">Send</span>
|
||||
</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<FieldDescription>
|
||||
This is a description of the input group.
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="textarea-comment-31">Comment Box</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupTextarea
|
||||
id="textarea-comment-31"
|
||||
placeholder="Share your thoughts..."
|
||||
/>
|
||||
<InputGroupAddon align="block-end">
|
||||
<InputGroupButton variant="ghost" className="ml-auto" size="sm">
|
||||
Cancel
|
||||
</InputGroupButton>
|
||||
<InputGroupButton variant="default" size="sm">
|
||||
Post Comment
|
||||
</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</Field>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
392
apps/v4/app/(internal)/sink/components/item-demo.tsx
Normal file
392
apps/v4/app/(internal)/sink/components/item-demo.tsx
Normal file
@@ -0,0 +1,392 @@
|
||||
import * as React from "react"
|
||||
import Image from "next/image"
|
||||
import { IconChevronRight, IconDownload } from "@tabler/icons-react"
|
||||
import { PlusIcon, TicketIcon } from "lucide-react"
|
||||
|
||||
import {
|
||||
Avatar,
|
||||
AvatarFallback,
|
||||
AvatarImage,
|
||||
} from "@/registry/new-york-v4/ui/avatar"
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import {
|
||||
Field,
|
||||
FieldContent,
|
||||
FieldDescription,
|
||||
FieldLabel,
|
||||
FieldTitle,
|
||||
} from "@/registry/new-york-v4/ui/field"
|
||||
import {
|
||||
Item,
|
||||
ItemActions,
|
||||
ItemContent,
|
||||
ItemDescription,
|
||||
ItemFooter,
|
||||
ItemGroup,
|
||||
ItemHeader,
|
||||
ItemMedia,
|
||||
ItemSeparator,
|
||||
ItemTitle,
|
||||
} from "@/registry/new-york-v4/ui/item"
|
||||
import { Progress } from "@/registry/new-york-v4/ui/progress"
|
||||
import { Spinner } from "@/registry/new-york-v4/ui/spinner"
|
||||
|
||||
const people = [
|
||||
{
|
||||
username: "shadcn",
|
||||
avatar: "https://github.com/shadcn.png",
|
||||
message: "Just shipped a component that fixes itself",
|
||||
},
|
||||
{
|
||||
username: "pranathip",
|
||||
avatar: "https://github.com/pranathip.png",
|
||||
message: "My code is so clean, it does its own laundry",
|
||||
},
|
||||
{
|
||||
username: "evilrabbit",
|
||||
avatar: "https://github.com/evilrabbit.png",
|
||||
message:
|
||||
"Debugging is like being a detective in a crime movie where you're also the murderer",
|
||||
},
|
||||
{
|
||||
username: "maxleiter",
|
||||
avatar: "https://github.com/maxleiter.png",
|
||||
message:
|
||||
"I don't always test my code, but when I do, I test it in production",
|
||||
},
|
||||
]
|
||||
|
||||
const music = [
|
||||
{
|
||||
title: "Midnight City Lights",
|
||||
artist: "Neon Dreams",
|
||||
album: "Electric Nights",
|
||||
duration: "3:45",
|
||||
},
|
||||
{
|
||||
title: "Coffee Shop Conversations",
|
||||
artist: "The Morning Brew",
|
||||
album: "Urban Stories",
|
||||
duration: "4:05",
|
||||
},
|
||||
{
|
||||
title: "Digital Rain",
|
||||
artist: "Cyber Symphony",
|
||||
album: "Binary Beats",
|
||||
duration: "3:30",
|
||||
},
|
||||
{
|
||||
title: "Sunset Boulevard",
|
||||
artist: "Golden Hour",
|
||||
album: "California Dreams",
|
||||
duration: "3:55",
|
||||
},
|
||||
{
|
||||
title: "Neon Sign Romance",
|
||||
artist: "Retro Wave",
|
||||
album: "80s Forever",
|
||||
duration: "4:10",
|
||||
},
|
||||
{
|
||||
title: "Ocean Depths",
|
||||
artist: "Deep Blue",
|
||||
album: "Underwater Symphony",
|
||||
duration: "3:40",
|
||||
},
|
||||
{
|
||||
title: "Space Station Alpha",
|
||||
artist: "Cosmic Explorers",
|
||||
album: "Galactic Journey",
|
||||
duration: "3:50",
|
||||
},
|
||||
{
|
||||
title: "Forest Whispers",
|
||||
artist: "Nature's Choir",
|
||||
album: "Woodland Tales",
|
||||
duration: "3:35",
|
||||
},
|
||||
]
|
||||
|
||||
const issues = [
|
||||
{
|
||||
number: 1247,
|
||||
date: "March 15, 2024",
|
||||
title:
|
||||
"Button component doesn't respect disabled state when using custom variants",
|
||||
description:
|
||||
"When applying custom variants to the Button component, the disabled prop is ignored and the button remains clickable. This affects accessibility and user experience.",
|
||||
},
|
||||
{
|
||||
number: 892,
|
||||
date: "February 8, 2024",
|
||||
title: "Dialog component causes scroll lock on mobile devices",
|
||||
description:
|
||||
"The Dialog component prevents scrolling on the background content but doesn't restore scroll position properly on mobile Safari and Chrome, causing layout shifts.",
|
||||
},
|
||||
{
|
||||
number: 1156,
|
||||
date: "January 22, 2024",
|
||||
title: "TypeScript errors with Select component in strict mode",
|
||||
description:
|
||||
"Using the Select component with TypeScript strict mode enabled throws type errors related to generic constraints and value prop typing.",
|
||||
},
|
||||
{
|
||||
number: 734,
|
||||
date: "December 3, 2023",
|
||||
title: "Dark mode toggle causes flash of unstyled content",
|
||||
description:
|
||||
"When switching between light and dark themes, there's a brief moment where components render with incorrect styling before the theme transition completes.",
|
||||
},
|
||||
{
|
||||
number: 1389,
|
||||
date: "April 2, 2024",
|
||||
title: "Form validation messages overlap with floating labels",
|
||||
description:
|
||||
"Error messages in Form components with floating labels appear underneath the label text, making them difficult to read. Need better positioning logic for validation feedback.",
|
||||
},
|
||||
]
|
||||
|
||||
export function ItemDemo() {
|
||||
return (
|
||||
<div className="@container w-full">
|
||||
<div className="flex flex-wrap gap-6 2xl:gap-12">
|
||||
<div className="flex max-w-sm flex-col gap-6">
|
||||
<Item>
|
||||
<ItemContent>
|
||||
<ItemTitle>Item Title</ItemTitle>
|
||||
</ItemContent>
|
||||
<ItemActions>
|
||||
<Button variant="outline">Button</Button>
|
||||
</ItemActions>
|
||||
</Item>
|
||||
<Item variant="outline">
|
||||
<ItemContent>
|
||||
<ItemTitle>Item Title</ItemTitle>
|
||||
</ItemContent>
|
||||
<ItemActions>
|
||||
<Button variant="outline">Button</Button>
|
||||
</ItemActions>
|
||||
</Item>
|
||||
<Item>
|
||||
<ItemContent>
|
||||
<ItemTitle>Item Title</ItemTitle>
|
||||
<ItemDescription>Item Description</ItemDescription>
|
||||
</ItemContent>
|
||||
<ItemActions>
|
||||
<Button variant="outline">Button</Button>
|
||||
</ItemActions>
|
||||
</Item>
|
||||
<Item variant="outline">
|
||||
<ItemContent>
|
||||
<ItemTitle>Item Title</ItemTitle>
|
||||
<ItemDescription>Item Description</ItemDescription>
|
||||
</ItemContent>
|
||||
</Item>
|
||||
<Item variant="muted">
|
||||
<ItemContent>
|
||||
<ItemTitle>Item Title</ItemTitle>
|
||||
<ItemDescription>Item Description</ItemDescription>
|
||||
</ItemContent>
|
||||
</Item>
|
||||
<Item variant="muted">
|
||||
<ItemContent>
|
||||
<ItemTitle>Item Title</ItemTitle>
|
||||
<ItemDescription>Item Description</ItemDescription>
|
||||
</ItemContent>
|
||||
<ItemActions>
|
||||
<Button variant="outline">Button</Button>
|
||||
<Button variant="outline">Button</Button>
|
||||
</ItemActions>
|
||||
</Item>
|
||||
<Item variant="outline">
|
||||
<ItemMedia variant="icon">
|
||||
<TicketIcon />
|
||||
</ItemMedia>
|
||||
<ItemContent>
|
||||
<ItemTitle>Item Title</ItemTitle>
|
||||
</ItemContent>
|
||||
<ItemActions>
|
||||
<Button size="sm">Purchase</Button>
|
||||
</ItemActions>
|
||||
</Item>
|
||||
<Item variant="muted">
|
||||
<ItemMedia variant="icon">
|
||||
<TicketIcon />
|
||||
</ItemMedia>
|
||||
<ItemContent>
|
||||
<ItemTitle>Item Title</ItemTitle>
|
||||
<ItemDescription>Item Description</ItemDescription>
|
||||
</ItemContent>
|
||||
<ItemActions>
|
||||
<Button size="sm">Upgrade</Button>
|
||||
</ItemActions>
|
||||
</Item>
|
||||
<FieldLabel>
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldTitle>Field Title</FieldTitle>
|
||||
<FieldDescription>Field Description</FieldDescription>
|
||||
</FieldContent>
|
||||
<Button variant="outline">Button</Button>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
</div>
|
||||
<div className="flex max-w-sm flex-col gap-6">
|
||||
<ItemGroup>
|
||||
{people.map((person, index) => (
|
||||
<React.Fragment key={person.username}>
|
||||
<Item>
|
||||
<ItemMedia>
|
||||
<Avatar>
|
||||
<AvatarImage src={person.avatar} />
|
||||
<AvatarFallback>
|
||||
{person.username.charAt(0)}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
</ItemMedia>
|
||||
<ItemContent>
|
||||
<ItemTitle>{person.username}</ItemTitle>
|
||||
<ItemDescription>{person.message}</ItemDescription>
|
||||
</ItemContent>
|
||||
<ItemActions>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="size-8 rounded-full"
|
||||
>
|
||||
<PlusIcon />
|
||||
</Button>
|
||||
</ItemActions>
|
||||
</Item>
|
||||
{index !== people.length - 1 && <ItemSeparator />}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</ItemGroup>
|
||||
<Item variant="outline">
|
||||
<ItemMedia>
|
||||
<div className="*:data-[slot=avatar]:ring-background flex -space-x-2 *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:grayscale">
|
||||
<Avatar>
|
||||
<AvatarImage
|
||||
src="https://github.com/shadcn.png"
|
||||
alt="@shadcn"
|
||||
/>
|
||||
<AvatarFallback>CN</AvatarFallback>
|
||||
</Avatar>
|
||||
<Avatar>
|
||||
<AvatarImage
|
||||
src="https://github.com/maxleiter.png"
|
||||
alt="@maxleiter"
|
||||
/>
|
||||
<AvatarFallback>LR</AvatarFallback>
|
||||
</Avatar>
|
||||
<Avatar>
|
||||
<AvatarImage
|
||||
src="https://github.com/evilrabbit.png"
|
||||
alt="@evilrabbit"
|
||||
/>
|
||||
<AvatarFallback>ER</AvatarFallback>
|
||||
</Avatar>
|
||||
</div>
|
||||
</ItemMedia>
|
||||
<ItemContent>
|
||||
<ItemTitle>Design Department</ItemTitle>
|
||||
<ItemDescription>
|
||||
Meet our team of designers, engineers, and researchers.
|
||||
</ItemDescription>
|
||||
</ItemContent>
|
||||
<ItemActions className="self-start">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="size-8 rounded-full"
|
||||
>
|
||||
<IconChevronRight />
|
||||
</Button>
|
||||
</ItemActions>
|
||||
</Item>
|
||||
<Item variant="outline">
|
||||
<ItemHeader>Your download has started.</ItemHeader>
|
||||
<ItemMedia variant="icon">
|
||||
<Spinner />
|
||||
</ItemMedia>
|
||||
<ItemContent>
|
||||
<ItemTitle>Downloading...</ItemTitle>
|
||||
<ItemDescription>129 MB / 1000 MB</ItemDescription>
|
||||
</ItemContent>
|
||||
<ItemActions>
|
||||
<Button variant="outline" size="sm">
|
||||
Cancel
|
||||
</Button>
|
||||
</ItemActions>
|
||||
<ItemFooter>
|
||||
<Progress value={50} />
|
||||
</ItemFooter>
|
||||
</Item>
|
||||
</div>
|
||||
<div className="flex max-w-lg flex-col gap-6">
|
||||
<ItemGroup className="gap-4">
|
||||
{music.map((song) => (
|
||||
<Item key={song.title} variant="outline" asChild role="listitem">
|
||||
<a href="#">
|
||||
<ItemMedia variant="image">
|
||||
<Image
|
||||
src={`https://avatar.vercel.sh/${song.title}`}
|
||||
alt={song.title}
|
||||
width={32}
|
||||
height={32}
|
||||
className="grayscale"
|
||||
/>
|
||||
</ItemMedia>
|
||||
<ItemContent>
|
||||
<ItemTitle className="line-clamp-1">
|
||||
{song.title} -{" "}
|
||||
<span className="text-muted-foreground">
|
||||
{song.album}
|
||||
</span>
|
||||
</ItemTitle>
|
||||
<ItemDescription>{song.artist}</ItemDescription>
|
||||
</ItemContent>
|
||||
<ItemContent className="flex-none text-center">
|
||||
<ItemDescription>{song.duration}</ItemDescription>
|
||||
</ItemContent>
|
||||
<ItemActions>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-8 rounded-full"
|
||||
aria-label="Download"
|
||||
>
|
||||
<IconDownload />
|
||||
</Button>
|
||||
</ItemActions>
|
||||
</a>
|
||||
</Item>
|
||||
))}
|
||||
</ItemGroup>
|
||||
</div>
|
||||
<div className="flex max-w-lg flex-col gap-6">
|
||||
<ItemGroup>
|
||||
{issues.map((issue) => (
|
||||
<React.Fragment key={issue.number}>
|
||||
<Item asChild className="rounded-none">
|
||||
<a href="#">
|
||||
<ItemContent>
|
||||
<ItemTitle className="line-clamp-1">
|
||||
{issue.title}
|
||||
</ItemTitle>
|
||||
<ItemDescription>{issue.description}</ItemDescription>
|
||||
</ItemContent>
|
||||
<ItemContent className="self-start">
|
||||
#{issue.number}
|
||||
</ItemContent>
|
||||
</a>
|
||||
</Item>
|
||||
<ItemSeparator />
|
||||
</React.Fragment>
|
||||
))}
|
||||
</ItemGroup>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
103
apps/v4/app/(internal)/sink/components/kbd-demo.tsx
Normal file
103
apps/v4/app/(internal)/sink/components/kbd-demo.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
import { IconArrowLeft, IconArrowRight } from "@tabler/icons-react"
|
||||
import { CommandIcon, WavesIcon } from "lucide-react"
|
||||
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import { ButtonGroup } from "@/registry/new-york-v4/ui/button-group"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupInput,
|
||||
} from "@/registry/new-york-v4/ui/input-group"
|
||||
import { Kbd, KbdGroup } from "@/registry/new-york-v4/ui/kbd"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/registry/new-york-v4/ui/tooltip"
|
||||
|
||||
export function KbdDemo() {
|
||||
return (
|
||||
<div className="flex max-w-xs flex-col items-start gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Kbd>Ctrl</Kbd>
|
||||
<Kbd>⌘K</Kbd>
|
||||
<Kbd>Ctrl + B</Kbd>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Kbd>⌘</Kbd>
|
||||
<Kbd>C</Kbd>
|
||||
</div>
|
||||
<KbdGroup>
|
||||
<Kbd>Ctrl</Kbd>
|
||||
<Kbd>Shift</Kbd>
|
||||
<Kbd>P</Kbd>
|
||||
</KbdGroup>
|
||||
<div className="flex items-center gap-2">
|
||||
<Kbd>↑</Kbd>
|
||||
<Kbd>↓</Kbd>
|
||||
<Kbd>←</Kbd>
|
||||
<Kbd>→</Kbd>
|
||||
</div>
|
||||
<KbdGroup>
|
||||
<Kbd>
|
||||
<CommandIcon />
|
||||
</Kbd>
|
||||
<Kbd>
|
||||
<IconArrowLeft />
|
||||
</Kbd>
|
||||
<Kbd>
|
||||
<IconArrowRight />
|
||||
</Kbd>
|
||||
</KbdGroup>
|
||||
<KbdGroup>
|
||||
<Kbd>
|
||||
<IconArrowLeft />
|
||||
Left
|
||||
</Kbd>
|
||||
<Kbd>
|
||||
<WavesIcon />
|
||||
Voice Enabled
|
||||
</Kbd>
|
||||
</KbdGroup>
|
||||
<InputGroup>
|
||||
<InputGroupInput />
|
||||
<InputGroupAddon>
|
||||
<Kbd>Space</Kbd>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<ButtonGroup>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button size="sm" variant="outline">
|
||||
Save
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<div className="flex items-center gap-2">
|
||||
Save Changes <Kbd>S</Kbd>
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button size="sm" variant="outline">
|
||||
Print
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<div className="flex items-center gap-2">
|
||||
Print Document{" "}
|
||||
<KbdGroup>
|
||||
<Kbd>Ctrl</Kbd>
|
||||
<Kbd>P</Kbd>
|
||||
</KbdGroup>
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</ButtonGroup>
|
||||
<Kbd>
|
||||
<samp>File</samp>
|
||||
</Kbd>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
106
apps/v4/app/(internal)/sink/components/spinner-demo.tsx
Normal file
106
apps/v4/app/(internal)/sink/components/spinner-demo.tsx
Normal file
@@ -0,0 +1,106 @@
|
||||
import { ArrowRightIcon } from "lucide-react"
|
||||
|
||||
import { Badge } from "@/registry/new-york-v4/ui/badge"
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import {
|
||||
Empty,
|
||||
EmptyContent,
|
||||
EmptyDescription,
|
||||
EmptyHeader,
|
||||
EmptyMedia,
|
||||
EmptyTitle,
|
||||
} from "@/registry/new-york-v4/ui/empty"
|
||||
import { Field, FieldLabel } from "@/registry/new-york-v4/ui/field"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupInput,
|
||||
} from "@/registry/new-york-v4/ui/input-group"
|
||||
import { Spinner } from "@/registry/new-york-v4/ui/spinner"
|
||||
|
||||
export function SpinnerDemo() {
|
||||
return (
|
||||
<div className="flex w-full flex-col gap-12">
|
||||
<div className="flex items-center gap-6">
|
||||
<Spinner />
|
||||
<Spinner className="size-8" />
|
||||
</div>
|
||||
<div className="flex items-center gap-6">
|
||||
<Button>
|
||||
<Spinner /> Submit
|
||||
</Button>
|
||||
<Button disabled>
|
||||
<Spinner /> Disabled
|
||||
</Button>
|
||||
<Button size="sm">
|
||||
<Spinner /> Small
|
||||
</Button>
|
||||
<Button variant="outline" disabled>
|
||||
<Spinner /> Outline
|
||||
</Button>
|
||||
<Button variant="outline" size="icon" disabled>
|
||||
<Spinner />
|
||||
<span className="sr-only">Loading...</span>
|
||||
</Button>
|
||||
<Button variant="destructive" disabled>
|
||||
<Spinner />
|
||||
Remove
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex items-center gap-6">
|
||||
<Badge>
|
||||
<Spinner />
|
||||
Badge
|
||||
</Badge>
|
||||
<Badge variant="secondary">
|
||||
<Spinner />
|
||||
Badge
|
||||
</Badge>
|
||||
<Badge variant="destructive">
|
||||
<Spinner />
|
||||
Badge
|
||||
</Badge>
|
||||
<Badge variant="outline">
|
||||
<Spinner />
|
||||
Badge
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex max-w-xs items-center gap-6">
|
||||
<Field>
|
||||
<FieldLabel htmlFor="input-group-spinner">Input Group</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupInput id="input-group-spinner" />
|
||||
<InputGroupAddon>
|
||||
<Spinner />
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</Field>
|
||||
</div>
|
||||
<Empty className="min-h-[80svh]">
|
||||
<EmptyHeader>
|
||||
<EmptyMedia variant="icon">
|
||||
<Spinner />
|
||||
</EmptyMedia>
|
||||
<EmptyTitle>No projects yet</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
You haven't created any projects yet. Get started by creating
|
||||
your first project.
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
<EmptyContent>
|
||||
<div className="flex gap-2">
|
||||
<Button asChild>
|
||||
<a href="#">Create project</a>
|
||||
</Button>
|
||||
<Button variant="outline">Import project</Button>
|
||||
</div>
|
||||
<Button variant="link" asChild className="text-muted-foreground">
|
||||
<a href="#">
|
||||
Learn more <ArrowRightIcon />
|
||||
</a>
|
||||
</Button>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
147
apps/v4/app/(internal)/sink/components/theme-selector.tsx
Normal file
147
apps/v4/app/(internal)/sink/components/theme-selector.tsx
Normal file
@@ -0,0 +1,147 @@
|
||||
"use client"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { useThemeConfig } from "@/components/active-theme"
|
||||
import { Label } from "@/registry/new-york-v4/ui/label"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectSeparator,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/registry/new-york-v4/ui/select"
|
||||
|
||||
const THEMES = {
|
||||
sizes: [
|
||||
{
|
||||
name: "Default",
|
||||
value: "default",
|
||||
},
|
||||
{
|
||||
name: "Scaled",
|
||||
value: "scaled",
|
||||
},
|
||||
{
|
||||
name: "Mono",
|
||||
value: "mono",
|
||||
},
|
||||
],
|
||||
colors: [
|
||||
{
|
||||
name: "Blue",
|
||||
value: "blue",
|
||||
},
|
||||
{
|
||||
name: "Green",
|
||||
value: "green",
|
||||
},
|
||||
{
|
||||
name: "Amber",
|
||||
value: "amber",
|
||||
},
|
||||
{
|
||||
name: "Rose",
|
||||
value: "rose",
|
||||
},
|
||||
{
|
||||
name: "Purple",
|
||||
value: "purple",
|
||||
},
|
||||
{
|
||||
name: "Orange",
|
||||
value: "orange",
|
||||
},
|
||||
{
|
||||
name: "Teal",
|
||||
value: "teal",
|
||||
},
|
||||
],
|
||||
fonts: [
|
||||
{
|
||||
name: "Inter",
|
||||
value: "inter",
|
||||
},
|
||||
{
|
||||
name: "Noto Sans",
|
||||
value: "noto-sans",
|
||||
},
|
||||
{
|
||||
name: "Nunito Sans",
|
||||
value: "nunito-sans",
|
||||
},
|
||||
{
|
||||
name: "Figtree",
|
||||
value: "figtree",
|
||||
},
|
||||
],
|
||||
radius: [
|
||||
{
|
||||
name: "None",
|
||||
value: "rounded-none",
|
||||
},
|
||||
{
|
||||
name: "Small",
|
||||
value: "rounded-small",
|
||||
},
|
||||
{
|
||||
name: "Medium",
|
||||
value: "rounded-medium",
|
||||
},
|
||||
{
|
||||
name: "Large",
|
||||
value: "rounded-large",
|
||||
},
|
||||
{
|
||||
name: "Full",
|
||||
value: "rounded-full",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export function ThemeSelector({ className }: React.ComponentProps<"div">) {
|
||||
const { activeTheme, setActiveTheme } = useThemeConfig()
|
||||
|
||||
return (
|
||||
<div className={cn("flex items-center gap-2", className)}>
|
||||
<Label htmlFor="theme-selector" className="sr-only">
|
||||
Theme
|
||||
</Label>
|
||||
<Select value={activeTheme} onValueChange={setActiveTheme}>
|
||||
<SelectTrigger
|
||||
id="theme-selector"
|
||||
size="sm"
|
||||
className="bg-secondary text-secondary-foreground border-secondary justify-start shadow-none *:data-[slot=select-value]:w-16"
|
||||
>
|
||||
<SelectValue placeholder="Select" />
|
||||
</SelectTrigger>
|
||||
<SelectContent align="end">
|
||||
{Object.entries(THEMES).map(
|
||||
([key, themes], index) =>
|
||||
themes.length > 0 && (
|
||||
<div key={key}>
|
||||
{index > 0 && <SelectSeparator />}
|
||||
<SelectGroup>
|
||||
<SelectLabel>
|
||||
{key.charAt(0).toUpperCase() + key.slice(1)}
|
||||
</SelectLabel>
|
||||
{themes.map((theme) => (
|
||||
<SelectItem
|
||||
key={theme.name}
|
||||
value={theme.value}
|
||||
className="data-[state=checked]:opacity-50"
|
||||
>
|
||||
{theme.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
66
apps/v4/app/(internal)/sink/layout.tsx
Normal file
66
apps/v4/app/(internal)/sink/layout.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import { Figtree, Inter, Noto_Sans, Nunito_Sans } from "next/font/google"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { ModeSwitcher } from "@/components/mode-switcher"
|
||||
import { Separator } from "@/registry/new-york-v4/ui/separator"
|
||||
import {
|
||||
SidebarInset,
|
||||
SidebarProvider,
|
||||
SidebarTrigger,
|
||||
} from "@/registry/new-york-v4/ui/sidebar"
|
||||
import { AppBreadcrumbs } from "@/app/(internal)/sink/components/app-breadcrumbs"
|
||||
import { AppSidebar } from "@/app/(internal)/sink/components/app-sidebar"
|
||||
import { ThemeSelector } from "@/app/(internal)/sink/components/theme-selector"
|
||||
|
||||
const inter = Inter({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-inter",
|
||||
})
|
||||
|
||||
const notoSans = Noto_Sans({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-noto-sans",
|
||||
})
|
||||
|
||||
const nunitoSans = Nunito_Sans({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-nunito-sans",
|
||||
})
|
||||
|
||||
const figtree = Figtree({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-figtree",
|
||||
})
|
||||
|
||||
export default async function SinkLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<SidebarProvider
|
||||
defaultOpen={true}
|
||||
className={cn(
|
||||
"theme-container",
|
||||
inter.variable,
|
||||
notoSans.variable,
|
||||
nunitoSans.variable,
|
||||
figtree.variable
|
||||
)}
|
||||
>
|
||||
<AppSidebar />
|
||||
<SidebarInset>
|
||||
<header className="bg-background sticky top-0 z-10 flex h-14 items-center border-b p-4">
|
||||
<SidebarTrigger />
|
||||
<Separator orientation="vertical" className="mr-4 ml-2 !h-4" />
|
||||
<AppBreadcrumbs />
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
<ModeSwitcher />
|
||||
<ThemeSelector />
|
||||
</div>
|
||||
</header>
|
||||
{children}
|
||||
</SidebarInset>
|
||||
</SidebarProvider>
|
||||
)
|
||||
}
|
||||
@@ -1,62 +1,7 @@
|
||||
import { Metadata } from "next"
|
||||
import { cookies } from "next/headers"
|
||||
|
||||
import { ThemeSelector } from "@/components/theme-selector"
|
||||
import { Separator } from "@/registry/new-york-v4/ui/separator"
|
||||
import {
|
||||
SidebarInset,
|
||||
SidebarProvider,
|
||||
SidebarTrigger,
|
||||
} from "@/registry/new-york-v4/ui/sidebar"
|
||||
import { AccordionDemo } from "@/app/(internal)/sink/components/accordion-demo"
|
||||
import { AlertDemo } from "@/app/(internal)/sink/components/alert-demo"
|
||||
import { AlertDialogDemo } from "@/app/(internal)/sink/components/alert-dialog-demo"
|
||||
import { AppSidebar } from "@/app/(internal)/sink/components/app-sidebar"
|
||||
import { AspectRatioDemo } from "@/app/(internal)/sink/components/aspect-ratio-demo"
|
||||
import { AvatarDemo } from "@/app/(internal)/sink/components/avatar-demo"
|
||||
import { BadgeDemo } from "@/app/(internal)/sink/components/badge-demo"
|
||||
import { BreadcrumbDemo } from "@/app/(internal)/sink/components/breadcrumb-demo"
|
||||
import { ButtonDemo } from "@/app/(internal)/sink/components/button-demo"
|
||||
import { CalendarDemo } from "@/app/(internal)/sink/components/calendar-demo"
|
||||
import { CardDemo } from "@/app/(internal)/sink/components/card-demo"
|
||||
import { CarouselDemo } from "@/app/(internal)/sink/components/carousel-demo"
|
||||
import { ChartDemo } from "@/app/(internal)/sink/components/chart-demo"
|
||||
import { CheckboxDemo } from "@/app/(internal)/sink/components/checkbox-demo"
|
||||
import { CollapsibleDemo } from "@/app/(internal)/sink/components/collapsible-demo"
|
||||
import { ComboboxDemo } from "@/app/(internal)/sink/components/combobox-demo"
|
||||
import { CommandDemo } from "@/app/(internal)/sink/components/command-demo"
|
||||
import { componentRegistry } from "@/app/(internal)/sink/component-registry"
|
||||
import { ComponentWrapper } from "@/app/(internal)/sink/components/component-wrapper"
|
||||
import { ContextMenuDemo } from "@/app/(internal)/sink/components/context-menu-demo"
|
||||
import { DatePickerDemo } from "@/app/(internal)/sink/components/date-picker-demo"
|
||||
import { DialogDemo } from "@/app/(internal)/sink/components/dialog-demo"
|
||||
import { DrawerDemo } from "@/app/(internal)/sink/components/drawer-demo"
|
||||
import { DropdownMenuDemo } from "@/app/(internal)/sink/components/dropdown-menu-demo"
|
||||
import { FormDemo } from "@/app/(internal)/sink/components/form-demo"
|
||||
import { HoverCardDemo } from "@/app/(internal)/sink/components/hover-card-demo"
|
||||
import { InputDemo } from "@/app/(internal)/sink/components/input-demo"
|
||||
import { InputOTPDemo } from "@/app/(internal)/sink/components/input-otp-demo"
|
||||
import { LabelDemo } from "@/app/(internal)/sink/components/label-demo"
|
||||
import { MenubarDemo } from "@/app/(internal)/sink/components/menubar-demo"
|
||||
import { NavigationMenuDemo } from "@/app/(internal)/sink/components/navigation-menu-demo"
|
||||
import { PaginationDemo } from "@/app/(internal)/sink/components/pagination-demo"
|
||||
import { PopoverDemo } from "@/app/(internal)/sink/components/popover-demo"
|
||||
import { ProgressDemo } from "@/app/(internal)/sink/components/progress-demo"
|
||||
import { RadioGroupDemo } from "@/app/(internal)/sink/components/radio-group-demo"
|
||||
import { ResizableDemo } from "@/app/(internal)/sink/components/resizable-demo"
|
||||
import { ScrollAreaDemo } from "@/app/(internal)/sink/components/scroll-area-demo"
|
||||
import { SelectDemo } from "@/app/(internal)/sink/components/select-demo"
|
||||
import { SeparatorDemo } from "@/app/(internal)/sink/components/separator-demo"
|
||||
import { SheetDemo } from "@/app/(internal)/sink/components/sheet-demo"
|
||||
import { SkeletonDemo } from "@/app/(internal)/sink/components/skeleton-demo"
|
||||
import { SliderDemo } from "@/app/(internal)/sink/components/slider-demo"
|
||||
import { SonnerDemo } from "@/app/(internal)/sink/components/sonner-demo"
|
||||
import { SwitchDemo } from "@/app/(internal)/sink/components/switch-demo"
|
||||
import { TableDemo } from "@/app/(internal)/sink/components/table-demo"
|
||||
import { TabsDemo } from "@/app/(internal)/sink/components/tabs-demo"
|
||||
import { TextareaDemo } from "@/app/(internal)/sink/components/textarea-demo"
|
||||
import { ToggleDemo } from "@/app/(internal)/sink/components/toggle-demo"
|
||||
import { ToggleGroupDemo } from "@/app/(internal)/sink/components/toggle-group-demo"
|
||||
import { TooltipDemo } from "@/app/(internal)/sink/components/tooltip-demo"
|
||||
|
||||
export const dynamic = "force-static"
|
||||
export const revalidate = false
|
||||
@@ -66,164 +11,25 @@ export const metadata: Metadata = {
|
||||
description: "A page with all components for testing purposes.",
|
||||
}
|
||||
|
||||
export default async function SinkPage() {
|
||||
const cookieStore = await cookies()
|
||||
const defaultOpen = cookieStore.get("sidebar_state")?.value === "true"
|
||||
|
||||
export default function SinkPage() {
|
||||
return (
|
||||
<SidebarProvider defaultOpen={defaultOpen} className="theme-container">
|
||||
<AppSidebar />
|
||||
<SidebarInset>
|
||||
<header className="bg-background sticky top-0 z-10 flex h-14 items-center border-b p-4">
|
||||
<SidebarTrigger />
|
||||
<Separator orientation="vertical" className="mr-4 ml-2 !h-4" />
|
||||
<h1 className="text-base font-medium">Kitchen Sink</h1>
|
||||
<ThemeSelector className="ml-auto" />
|
||||
</header>
|
||||
<div className="@container grid flex-1 gap-4 p-4">
|
||||
<ComponentWrapper name="accordion">
|
||||
<AccordionDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="alert">
|
||||
<AlertDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="alert-dialog">
|
||||
<AlertDialogDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="aspect-ratio">
|
||||
<AspectRatioDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="avatar">
|
||||
<AvatarDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="badge">
|
||||
<BadgeDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="breadcrumb">
|
||||
<BreadcrumbDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="button">
|
||||
<ButtonDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="calendar">
|
||||
<CalendarDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="card">
|
||||
<CardDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="carousel">
|
||||
<CarouselDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="chart" className="w-full">
|
||||
<ChartDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="checkbox">
|
||||
<CheckboxDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="collapsible">
|
||||
<CollapsibleDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="combobox">
|
||||
<ComboboxDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="command">
|
||||
<CommandDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="context-menu">
|
||||
<ContextMenuDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="date-picker">
|
||||
<DatePickerDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="dialog">
|
||||
<DialogDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="drawer">
|
||||
<DrawerDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="dropdown-menu">
|
||||
<DropdownMenuDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="form">
|
||||
<FormDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="hover-card">
|
||||
<HoverCardDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="input">
|
||||
<InputDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="input-otp">
|
||||
<InputOTPDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="label">
|
||||
<LabelDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="menubar">
|
||||
<MenubarDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="navigation-menu">
|
||||
<NavigationMenuDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="pagination">
|
||||
<PaginationDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="popover">
|
||||
<PopoverDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="progress">
|
||||
<ProgressDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="radio-group">
|
||||
<RadioGroupDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="resizable">
|
||||
<ResizableDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="scroll-area">
|
||||
<ScrollAreaDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="select">
|
||||
<SelectDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="separator">
|
||||
<SeparatorDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="sheet">
|
||||
<SheetDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="skeleton">
|
||||
<SkeletonDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="slider">
|
||||
<SliderDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="sonner">
|
||||
<SonnerDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="switch">
|
||||
<SwitchDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="table">
|
||||
<TableDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="tabs">
|
||||
<TabsDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="textarea">
|
||||
<TextareaDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="toggle">
|
||||
<ToggleDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="toggle-group">
|
||||
<ToggleGroupDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="tooltip">
|
||||
<TooltipDemo />
|
||||
</ComponentWrapper>
|
||||
</div>
|
||||
</SidebarInset>
|
||||
</SidebarProvider>
|
||||
<div className="@container grid flex-1 gap-4 p-4">
|
||||
{Object.entries(componentRegistry)
|
||||
.filter(([, component]) => {
|
||||
return component.type === "registry:ui"
|
||||
})
|
||||
.map(([key, component]) => {
|
||||
const Component = component.component
|
||||
return (
|
||||
<ComponentWrapper
|
||||
key={key}
|
||||
name={key}
|
||||
className={component.className || ""}
|
||||
>
|
||||
<Component />
|
||||
</ComponentWrapper>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -84,13 +84,13 @@ export default function RootLayout({
|
||||
</head>
|
||||
<body
|
||||
className={cn(
|
||||
"text-foreground group/body overscroll-none font-sans antialiased [--footer-height:calc(var(--spacing)*14)] [--header-height:calc(var(--spacing)*14)] xl:[--footer-height:calc(var(--spacing)*24)]",
|
||||
"text-foreground group/body theme-blue overscroll-none font-sans antialiased [--footer-height:calc(var(--spacing)*14)] [--header-height:calc(var(--spacing)*14)] xl:[--footer-height:calc(var(--spacing)*24)]",
|
||||
fontVariables
|
||||
)}
|
||||
>
|
||||
<ThemeProvider>
|
||||
<LayoutProvider>
|
||||
<ActiveThemeProvider>
|
||||
<ActiveThemeProvider initialTheme="blue">
|
||||
{children}
|
||||
<TailwindIndicator />
|
||||
<Toaster position="top-center" />
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
useState,
|
||||
} from "react"
|
||||
|
||||
const DEFAULT_THEME = "default"
|
||||
const DEFAULT_THEME = "blue"
|
||||
|
||||
type ThemeContextType = {
|
||||
activeTheme: string
|
||||
|
||||
@@ -6,8 +6,9 @@ import { Badge } from "@/registry/new-york-v4/ui/badge"
|
||||
export function Announcement() {
|
||||
return (
|
||||
<Badge asChild variant="secondary" className="rounded-full">
|
||||
<Link href="/docs/components/calendar">
|
||||
New Calendar Component <ArrowRightIcon />
|
||||
<Link href="/docs/changelog">
|
||||
<span className="flex size-2 rounded-full bg-blue-500" title="New" />
|
||||
New Components: Field, Input Group, Item and more <ArrowRightIcon />
|
||||
</Link>
|
||||
</Badge>
|
||||
)
|
||||
|
||||
@@ -10,12 +10,17 @@ export function Callout({
|
||||
children,
|
||||
icon,
|
||||
className,
|
||||
variant = "default",
|
||||
...props
|
||||
}: React.ComponentProps<typeof Alert> & { icon?: React.ReactNode }) {
|
||||
}: React.ComponentProps<typeof Alert> & {
|
||||
icon?: React.ReactNode
|
||||
variant?: "default" | "info" | "warning"
|
||||
}) {
|
||||
return (
|
||||
<Alert
|
||||
data-variant={variant}
|
||||
className={cn(
|
||||
"bg-surface text-surface-foreground mt-6 w-auto border-none md:-mx-1",
|
||||
"bg-background text-foreground mt-6 w-auto border md:-mx-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -32,7 +32,12 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/registry/new-york-v4/ui/dialog"
|
||||
import { Input } from "@/registry/new-york-v4/ui/input"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupInput,
|
||||
} from "@/registry/new-york-v4/ui/input-group"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
@@ -159,23 +164,25 @@ export function CardsChat() {
|
||||
}}
|
||||
className="relative w-full"
|
||||
>
|
||||
<Input
|
||||
id="message"
|
||||
placeholder="Type your message..."
|
||||
className="flex-1 pr-10"
|
||||
autoComplete="off"
|
||||
value={input}
|
||||
onChange={(event) => setInput(event.target.value)}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
size="icon"
|
||||
className="absolute top-1/2 right-2 size-6 -translate-y-1/2 rounded-full"
|
||||
disabled={inputLength === 0}
|
||||
>
|
||||
<ArrowUpIcon className="size-3.5" />
|
||||
<span className="sr-only">Send</span>
|
||||
</Button>
|
||||
<InputGroup>
|
||||
<InputGroupInput
|
||||
id="message"
|
||||
placeholder="Type your message..."
|
||||
autoComplete="off"
|
||||
value={input}
|
||||
onChange={(event) => setInput(event.target.value)}
|
||||
/>
|
||||
<InputGroupAddon align="inline-end">
|
||||
<InputGroupButton
|
||||
type="submit"
|
||||
size="icon-xs"
|
||||
className="rounded-full"
|
||||
>
|
||||
<ArrowUpIcon />
|
||||
<span className="sr-only">Send</span>
|
||||
</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</form>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
|
||||
@@ -5,11 +5,15 @@ import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/new-york-v4/ui/card"
|
||||
import { Label } from "@/registry/new-york-v4/ui/label"
|
||||
import {
|
||||
Field,
|
||||
FieldContent,
|
||||
FieldDescription,
|
||||
FieldLabel,
|
||||
} from "@/registry/new-york-v4/ui/field"
|
||||
import { Switch } from "@/registry/new-york-v4/ui/switch"
|
||||
|
||||
export function CardsCookieSettings() {
|
||||
@@ -20,32 +24,20 @@ export function CardsCookieSettings() {
|
||||
<CardDescription>Manage your cookie settings here.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-6">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<Label htmlFor="necessary" className="flex flex-col items-start">
|
||||
<span>Strictly Necessary</span>
|
||||
<span className="text-muted-foreground leading-snug font-normal">
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor="necessary">Strictly Necessary</FieldLabel>
|
||||
<FieldDescription>
|
||||
These cookies are essential in order to use the website and use
|
||||
its features.
|
||||
</span>
|
||||
</Label>
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<Switch id="necessary" defaultChecked aria-label="Necessary" />
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<Label htmlFor="functional" className="flex flex-col items-start">
|
||||
<span>Functional Cookies</span>
|
||||
<span className="text-muted-foreground leading-snug font-normal">
|
||||
These cookies allow the website to provide personalized
|
||||
functionality.
|
||||
</span>
|
||||
</Label>
|
||||
<Switch id="functional" aria-label="Functional" />
|
||||
</div>
|
||||
</Field>
|
||||
<Field>
|
||||
<Button variant="outline">Save preferences</Button>
|
||||
</Field>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button variant="outline" className="w-full">
|
||||
Save preferences
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,12 +5,16 @@ import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/new-york-v4/ui/card"
|
||||
import {
|
||||
Field,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldSeparator,
|
||||
} from "@/registry/new-york-v4/ui/field"
|
||||
import { Input } from "@/registry/new-york-v4/ui/input"
|
||||
import { Label } from "@/registry/new-york-v4/ui/label"
|
||||
|
||||
export function CardsCreateAccount() {
|
||||
return (
|
||||
@@ -21,53 +25,48 @@ export function CardsCreateAccount() {
|
||||
Enter your email below to create your account
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<Button variant="outline">
|
||||
<svg viewBox="0 0 438.549 438.549">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M409.132 114.573c-19.608-33.596-46.205-60.194-79.798-79.8-33.598-19.607-70.277-29.408-110.063-29.408-39.781 0-76.472 9.804-110.063 29.408-33.596 19.605-60.192 46.204-79.8 79.8C9.803 148.168 0 184.854 0 224.63c0 47.78 13.94 90.745 41.827 128.906 27.884 38.164 63.906 64.572 108.063 79.227 5.14.954 8.945.283 11.419-1.996 2.475-2.282 3.711-5.14 3.711-8.562 0-.571-.049-5.708-.144-15.417a2549.81 2549.81 0 01-.144-25.406l-6.567 1.136c-4.187.767-9.469 1.092-15.846 1-6.374-.089-12.991-.757-19.842-1.999-6.854-1.231-13.229-4.086-19.13-8.559-5.898-4.473-10.085-10.328-12.56-17.556l-2.855-6.57c-1.903-4.374-4.899-9.233-8.992-14.559-4.093-5.331-8.232-8.945-12.419-10.848l-1.999-1.431c-1.332-.951-2.568-2.098-3.711-3.429-1.142-1.331-1.997-2.663-2.568-3.997-.572-1.335-.098-2.43 1.427-3.289 1.525-.859 4.281-1.276 8.28-1.276l5.708.853c3.807.763 8.516 3.042 14.133 6.851 5.614 3.806 10.229 8.754 13.846 14.842 4.38 7.806 9.657 13.754 15.846 17.847 6.184 4.093 12.419 6.136 18.699 6.136 6.28 0 11.704-.476 16.274-1.423 4.565-.952 8.848-2.383 12.847-4.285 1.713-12.758 6.377-22.559 13.988-29.41-10.848-1.14-20.601-2.857-29.264-5.14-8.658-2.286-17.605-5.996-26.835-11.14-9.235-5.137-16.896-11.516-22.985-19.126-6.09-7.614-11.088-17.61-14.987-29.979-3.901-12.374-5.852-26.648-5.852-42.826 0-23.035 7.52-42.637 22.557-58.817-7.044-17.318-6.379-36.732 1.997-58.24 5.52-1.715 13.706-.428 24.554 3.853 10.85 4.283 18.794 7.952 23.84 10.994 5.046 3.041 9.089 5.618 12.135 7.708 17.705-4.947 35.976-7.421 54.818-7.421s37.117 2.474 54.823 7.421l10.849-6.849c7.419-4.57 16.18-8.758 26.262-12.565 10.088-3.805 17.802-4.853 23.134-3.138 8.562 21.509 9.325 40.922 2.279 58.24 15.036 16.18 22.559 35.787 22.559 58.817 0 16.178-1.958 30.497-5.853 42.966-3.9 12.471-8.941 22.457-15.125 29.979-6.191 7.521-13.901 13.85-23.131 18.986-9.232 5.14-18.182 8.85-26.84 11.136-8.662 2.286-18.415 4.004-29.263 5.146 9.894 8.562 14.842 22.077 14.842 40.539v60.237c0 3.422 1.19 6.279 3.572 8.562 2.379 2.279 6.136 2.95 11.276 1.995 44.163-14.653 80.185-41.062 108.068-79.226 27.88-38.161 41.825-81.126 41.825-128.906-.01-39.771-9.818-76.454-29.414-110.049z"
|
||||
></path>
|
||||
</svg>
|
||||
GitHub
|
||||
</Button>
|
||||
<Button variant="outline">
|
||||
<svg role="img" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z"
|
||||
/>
|
||||
</svg>
|
||||
Google
|
||||
</Button>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
<span className="w-full border-t" />
|
||||
</div>
|
||||
<div className="relative flex justify-center text-xs uppercase">
|
||||
<span className="bg-card text-muted-foreground px-2">
|
||||
Or continue with
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<Label htmlFor="email-create-account">Email</Label>
|
||||
<Input
|
||||
id="email-create-account"
|
||||
type="email"
|
||||
placeholder="m@example.com"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<Label htmlFor="password-create-account">Password</Label>
|
||||
<Input id="password-create-account" type="password" />
|
||||
</div>
|
||||
<CardContent>
|
||||
<FieldGroup>
|
||||
<Field className="grid grid-cols-2 gap-6">
|
||||
<Button variant="outline">
|
||||
<svg viewBox="0 0 438.549 438.549">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M409.132 114.573c-19.608-33.596-46.205-60.194-79.798-79.8-33.598-19.607-70.277-29.408-110.063-29.408-39.781 0-76.472 9.804-110.063 29.408-33.596 19.605-60.192 46.204-79.8 79.8C9.803 148.168 0 184.854 0 224.63c0 47.78 13.94 90.745 41.827 128.906 27.884 38.164 63.906 64.572 108.063 79.227 5.14.954 8.945.283 11.419-1.996 2.475-2.282 3.711-5.14 3.711-8.562 0-.571-.049-5.708-.144-15.417a2549.81 2549.81 0 01-.144-25.406l-6.567 1.136c-4.187.767-9.469 1.092-15.846 1-6.374-.089-12.991-.757-19.842-1.999-6.854-1.231-13.229-4.086-19.13-8.559-5.898-4.473-10.085-10.328-12.56-17.556l-2.855-6.57c-1.903-4.374-4.899-9.233-8.992-14.559-4.093-5.331-8.232-8.945-12.419-10.848l-1.999-1.431c-1.332-.951-2.568-2.098-3.711-3.429-1.142-1.331-1.997-2.663-2.568-3.997-.572-1.335-.098-2.43 1.427-3.289 1.525-.859 4.281-1.276 8.28-1.276l5.708.853c3.807.763 8.516 3.042 14.133 6.851 5.614 3.806 10.229 8.754 13.846 14.842 4.38 7.806 9.657 13.754 15.846 17.847 6.184 4.093 12.419 6.136 18.699 6.136 6.28 0 11.704-.476 16.274-1.423 4.565-.952 8.848-2.383 12.847-4.285 1.713-12.758 6.377-22.559 13.988-29.41-10.848-1.14-20.601-2.857-29.264-5.14-8.658-2.286-17.605-5.996-26.835-11.14-9.235-5.137-16.896-11.516-22.985-19.126-6.09-7.614-11.088-17.61-14.987-29.979-3.901-12.374-5.852-26.648-5.852-42.826 0-23.035 7.52-42.637 22.557-58.817-7.044-17.318-6.379-36.732 1.997-58.24 5.52-1.715 13.706-.428 24.554 3.853 10.85 4.283 18.794 7.952 23.84 10.994 5.046 3.041 9.089 5.618 12.135 7.708 17.705-4.947 35.976-7.421 54.818-7.421s37.117 2.474 54.823 7.421l10.849-6.849c7.419-4.57 16.18-8.758 26.262-12.565 10.088-3.805 17.802-4.853 23.134-3.138 8.562 21.509 9.325 40.922 2.279 58.24 15.036 16.18 22.559 35.787 22.559 58.817 0 16.178-1.958 30.497-5.853 42.966-3.9 12.471-8.941 22.457-15.125 29.979-6.191 7.521-13.901 13.85-23.131 18.986-9.232 5.14-18.182 8.85-26.84 11.136-8.662 2.286-18.415 4.004-29.263 5.146 9.894 8.562 14.842 22.077 14.842 40.539v60.237c0 3.422 1.19 6.279 3.572 8.562 2.379 2.279 6.136 2.95 11.276 1.995 44.163-14.653 80.185-41.062 108.068-79.226 27.88-38.161 41.825-81.126 41.825-128.906-.01-39.771-9.818-76.454-29.414-110.049z"
|
||||
></path>
|
||||
</svg>
|
||||
GitHub
|
||||
</Button>
|
||||
<Button variant="outline">
|
||||
<svg role="img" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z"
|
||||
/>
|
||||
</svg>
|
||||
Google
|
||||
</Button>
|
||||
</Field>
|
||||
<FieldSeparator className="*:data-[slot=field-separator-content]:bg-card">
|
||||
Or continue with
|
||||
</FieldSeparator>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="email-create-account">Email</FieldLabel>
|
||||
<Input
|
||||
id="email-create-account"
|
||||
type="email"
|
||||
placeholder="m@example.com"
|
||||
/>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="password-create-account">Password</FieldLabel>
|
||||
<Input id="password-create-account" type="password" />
|
||||
</Field>
|
||||
<Field>
|
||||
<Button>Create Account</Button>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button className="w-full">Create account</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,13 +5,21 @@ import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/new-york-v4/ui/card"
|
||||
import { Checkbox } from "@/registry/new-york-v4/ui/checkbox"
|
||||
import {
|
||||
Field,
|
||||
FieldContent,
|
||||
FieldDescription,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldLegend,
|
||||
FieldSet,
|
||||
FieldTitle,
|
||||
} from "@/registry/new-york-v4/ui/field"
|
||||
import { Input } from "@/registry/new-york-v4/ui/input"
|
||||
import { Label } from "@/registry/new-york-v4/ui/label"
|
||||
import {
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
@@ -22,7 +30,7 @@ const plans = [
|
||||
{
|
||||
id: "starter",
|
||||
name: "Starter Plan",
|
||||
description: "Perfect for small businesses.",
|
||||
description: "For small businesses.",
|
||||
price: "$10",
|
||||
},
|
||||
{
|
||||
@@ -37,91 +45,96 @@ export function CardsForms() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">Upgrade your subscription</CardTitle>
|
||||
<CardTitle className="text-lg">Upgrade your Subscription</CardTitle>
|
||||
<CardDescription className="text-balance">
|
||||
You are currently on the free plan. Upgrade to the pro plan to get
|
||||
access to all features.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex flex-col gap-3 md:flex-row">
|
||||
<div className="flex flex-1 flex-col gap-2">
|
||||
<Label htmlFor="name">Name</Label>
|
||||
<Input id="name" placeholder="Evil Rabbit" />
|
||||
</div>
|
||||
<div className="flex flex-1 flex-col gap-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input id="email" placeholder="example@acme.com" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="card-number">Card Number</Label>
|
||||
<div className="grid grid-cols-2 gap-3 md:grid-cols-[1fr_80px_60px]">
|
||||
<Input
|
||||
id="card-number"
|
||||
placeholder="1234 1234 1234 1234"
|
||||
className="col-span-2 md:col-span-1"
|
||||
/>
|
||||
<Input id="card-number-expiry" placeholder="MM/YY" />
|
||||
<Input id="card-number-cvc" placeholder="CVC" />
|
||||
</div>
|
||||
</div>
|
||||
<fieldset className="flex flex-col gap-3">
|
||||
<legend className="text-sm font-medium">Plan</legend>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Select the plan that best fits your needs.
|
||||
</p>
|
||||
<RadioGroup
|
||||
defaultValue="starter"
|
||||
className="grid gap-3 md:grid-cols-2"
|
||||
>
|
||||
{plans.map((plan) => (
|
||||
<Label
|
||||
className="has-[[data-state=checked]]:border-ring has-[[data-state=checked]]:bg-input/20 flex items-start gap-3 rounded-lg border p-3"
|
||||
key={plan.id}
|
||||
>
|
||||
<RadioGroupItem
|
||||
value={plan.id}
|
||||
id={plan.name}
|
||||
className="data-[state=checked]:border-primary"
|
||||
/>
|
||||
<div className="grid gap-1 font-normal">
|
||||
<div className="font-medium">{plan.name}</div>
|
||||
<div className="text-muted-foreground text-xs leading-snug text-balance">
|
||||
{plan.description}
|
||||
</div>
|
||||
</div>
|
||||
</Label>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</fieldset>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="notes">Notes</Label>
|
||||
<Textarea id="notes" placeholder="Enter notes" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox id="terms" />
|
||||
<Label htmlFor="terms" className="font-normal">
|
||||
I agree to the terms and conditions
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox id="newsletter" defaultChecked />
|
||||
<Label htmlFor="newsletter" className="font-normal">
|
||||
Allow us to send you emails
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<form>
|
||||
<FieldGroup>
|
||||
<FieldGroup className="grid grid-cols-2">
|
||||
<Field>
|
||||
<FieldLabel htmlFor="name">Name</FieldLabel>
|
||||
<Input id="name" placeholder="Max Leiter" />
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="email">Email</FieldLabel>
|
||||
<Input id="email" placeholder="mail@acme.com" />
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
<FieldGroup className="grid grid-cols-2 gap-3 md:grid-cols-[1fr_80px_60px]">
|
||||
<Field>
|
||||
<FieldLabel htmlFor="card-number">Card Number</FieldLabel>
|
||||
<Input
|
||||
id="card-number"
|
||||
placeholder="1234 1234 1234 1234"
|
||||
className="col-span-2 md:col-span-1"
|
||||
/>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="card-number-expiry">
|
||||
Expiry Date
|
||||
</FieldLabel>
|
||||
<Input id="card-number-expiry" placeholder="MM/YY" />
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="card-number-cvc">CVC</FieldLabel>
|
||||
<Input id="card-number-cvc" placeholder="CVC" />
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
<FieldSet>
|
||||
<FieldLegend>Plan</FieldLegend>
|
||||
<FieldDescription>
|
||||
Select the plan that best fits your needs.
|
||||
</FieldDescription>
|
||||
<RadioGroup
|
||||
defaultValue="starter"
|
||||
className="grid grid-cols-2 gap-2"
|
||||
>
|
||||
{plans.map((plan) => (
|
||||
<FieldLabel key={plan.id}>
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldTitle>{plan.name}</FieldTitle>
|
||||
<FieldDescription className="text-xs">
|
||||
{plan.description}
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<RadioGroupItem value={plan.id} id={plan.name} />
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</FieldSet>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="notes">Notes</FieldLabel>
|
||||
<Textarea id="notes" placeholder="Enter notes" />
|
||||
</Field>
|
||||
<Field>
|
||||
<Field orientation="horizontal">
|
||||
<Checkbox id="terms" />
|
||||
<FieldLabel htmlFor="terms" className="font-normal">
|
||||
I agree to the terms and conditions
|
||||
</FieldLabel>
|
||||
</Field>
|
||||
<Field orientation="horizontal">
|
||||
<Checkbox id="newsletter" defaultChecked />
|
||||
<FieldLabel htmlFor="newsletter" className="font-normal">
|
||||
Allow us to send you emails
|
||||
</FieldLabel>
|
||||
</Field>
|
||||
</Field>
|
||||
<Field orientation="horizontal">
|
||||
<Button variant="outline" size="sm">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button size="sm">Upgrade Plan</Button>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</form>
|
||||
</CardContent>
|
||||
<CardFooter className="flex justify-between">
|
||||
<Button variant="outline" size="sm">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button size="sm">Upgrade Plan</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -7,12 +7,11 @@ import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/new-york-v4/ui/card"
|
||||
import { Field, FieldGroup, FieldLabel } from "@/registry/new-york-v4/ui/field"
|
||||
import { Input } from "@/registry/new-york-v4/ui/input"
|
||||
import { Label } from "@/registry/new-york-v4/ui/label"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -33,65 +32,69 @@ export function CardsReportIssue() {
|
||||
What area are you having problems with?
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-6">
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<div className="flex flex-col gap-3">
|
||||
<Label htmlFor={`area-${id}`}>Area</Label>
|
||||
<Select defaultValue="billing">
|
||||
<SelectTrigger
|
||||
id={`area-${id}`}
|
||||
aria-label="Area"
|
||||
className="w-full"
|
||||
>
|
||||
<SelectValue placeholder="Select" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="team">Team</SelectItem>
|
||||
<SelectItem value="billing">Billing</SelectItem>
|
||||
<SelectItem value="account">Account</SelectItem>
|
||||
<SelectItem value="deployments">Deployments</SelectItem>
|
||||
<SelectItem value="support">Support</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<Label htmlFor={`security-level-${id}`}>Security Level</Label>
|
||||
<Select defaultValue="2">
|
||||
<SelectTrigger
|
||||
id={`security-level-${id}`}
|
||||
className="w-full [&_span]:!block [&_span]:truncate"
|
||||
aria-label="Security Level"
|
||||
>
|
||||
<SelectValue placeholder="Select level" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="1">Severity 1 (Highest)</SelectItem>
|
||||
<SelectItem value="2">Severity 2</SelectItem>
|
||||
<SelectItem value="3">Severity 3</SelectItem>
|
||||
<SelectItem value="4">Severity 4 (Lowest)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<Label htmlFor={`subject-${id}`}>Subject</Label>
|
||||
<Input id={`subject-${id}`} placeholder="I need help with..." />
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<Label htmlFor={`description-${id}`}>Description</Label>
|
||||
<Textarea
|
||||
id={`description-${id}`}
|
||||
placeholder="Please include all information relevant to your issue."
|
||||
className="min-h-28"
|
||||
/>
|
||||
</div>
|
||||
<CardContent>
|
||||
<FieldGroup>
|
||||
<FieldGroup className="grid gap-4 sm:grid-cols-2">
|
||||
<Field>
|
||||
<FieldLabel htmlFor={`area-${id}`}>Area</FieldLabel>
|
||||
<Select defaultValue="billing">
|
||||
<SelectTrigger
|
||||
id={`area-${id}`}
|
||||
aria-label="Area"
|
||||
className="w-full"
|
||||
>
|
||||
<SelectValue placeholder="Select" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="team">Team</SelectItem>
|
||||
<SelectItem value="billing">Billing</SelectItem>
|
||||
<SelectItem value="account">Account</SelectItem>
|
||||
<SelectItem value="deployments">Deployments</SelectItem>
|
||||
<SelectItem value="support">Support</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor={`security-level-${id}`}>
|
||||
Security Level
|
||||
</FieldLabel>
|
||||
<Select defaultValue="2">
|
||||
<SelectTrigger
|
||||
id={`security-level-${id}`}
|
||||
className="w-full [&_span]:!block [&_span]:truncate"
|
||||
aria-label="Security Level"
|
||||
>
|
||||
<SelectValue placeholder="Select level" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="1">Severity 1 (Highest)</SelectItem>
|
||||
<SelectItem value="2">Severity 2</SelectItem>
|
||||
<SelectItem value="3">Severity 3</SelectItem>
|
||||
<SelectItem value="4">Severity 4 (Lowest)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
<Field>
|
||||
<FieldLabel htmlFor={`subject-${id}`}>Subject</FieldLabel>
|
||||
<Input id={`subject-${id}`} placeholder="I need help with..." />
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor={`description-${id}`}>Description</FieldLabel>
|
||||
<Textarea
|
||||
id={`description-${id}`}
|
||||
placeholder="Please include all information relevant to your issue."
|
||||
className="min-h-24"
|
||||
/>
|
||||
</Field>
|
||||
<Field orientation="horizontal" className="justify-end">
|
||||
<Button variant="ghost" size="sm">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button size="sm">Submit</Button>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</CardContent>
|
||||
<CardFooter className="justify-end gap-2">
|
||||
<Button variant="ghost" size="sm">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button size="sm">Submit</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -14,6 +14,14 @@ import {
|
||||
CardTitle,
|
||||
} from "@/registry/new-york-v4/ui/card"
|
||||
import { Input } from "@/registry/new-york-v4/ui/input"
|
||||
import {
|
||||
Item,
|
||||
ItemActions,
|
||||
ItemContent,
|
||||
ItemDescription,
|
||||
ItemGroup,
|
||||
ItemTitle,
|
||||
} from "@/registry/new-york-v4/ui/item"
|
||||
import { Label } from "@/registry/new-york-v4/ui/label"
|
||||
import {
|
||||
Select,
|
||||
@@ -73,42 +81,35 @@ export function CardsShare() {
|
||||
<Separator className="my-4" />
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="text-sm font-medium">People with access</div>
|
||||
<div className="grid gap-6">
|
||||
<ItemGroup>
|
||||
{people.map((person) => (
|
||||
<div
|
||||
key={person.email}
|
||||
className="flex items-center justify-between gap-4"
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
<Avatar>
|
||||
<AvatarImage src={person.avatar} alt="Image" />
|
||||
<AvatarFallback>{person.name.charAt(0)}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div>
|
||||
<p className="text-sm leading-none font-medium">
|
||||
{person.name}
|
||||
</p>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{person.email}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Select defaultValue="edit">
|
||||
<SelectTrigger
|
||||
className="ml-auto pr-2"
|
||||
aria-label="Edit"
|
||||
size="sm"
|
||||
>
|
||||
<SelectValue placeholder="Select" />
|
||||
</SelectTrigger>
|
||||
<SelectContent align="end">
|
||||
<SelectItem value="edit">Can edit</SelectItem>
|
||||
<SelectItem value="view">Can view</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<Item key={person.email} className="px-0 py-2">
|
||||
<Avatar>
|
||||
<AvatarImage src={person.avatar} alt="Image" />
|
||||
<AvatarFallback>{person.name.charAt(0)}</AvatarFallback>
|
||||
</Avatar>
|
||||
<ItemContent>
|
||||
<ItemTitle>{person.name}</ItemTitle>
|
||||
<ItemDescription>{person.email}</ItemDescription>
|
||||
</ItemContent>
|
||||
<ItemActions>
|
||||
<Select defaultValue="edit">
|
||||
<SelectTrigger
|
||||
className="ml-auto pr-2"
|
||||
aria-label="Edit"
|
||||
size="sm"
|
||||
>
|
||||
<SelectValue placeholder="Select" />
|
||||
</SelectTrigger>
|
||||
<SelectContent align="end">
|
||||
<SelectItem value="edit">Can edit</SelectItem>
|
||||
<SelectItem value="view">Can view</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</ItemActions>
|
||||
</Item>
|
||||
))}
|
||||
</div>
|
||||
</ItemGroup>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -23,6 +23,13 @@ import {
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@/registry/new-york-v4/ui/command"
|
||||
import {
|
||||
Item,
|
||||
ItemActions,
|
||||
ItemContent,
|
||||
ItemDescription,
|
||||
ItemTitle,
|
||||
} from "@/registry/new-york-v4/ui/item"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
@@ -71,63 +78,58 @@ const roles = [
|
||||
|
||||
export function CardsTeamMembers() {
|
||||
return (
|
||||
<Card>
|
||||
<Card className="gap-4">
|
||||
<CardHeader>
|
||||
<CardTitle>Team Members</CardTitle>
|
||||
<CardDescription>
|
||||
Invite your team members to collaborate.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-6">
|
||||
<CardContent>
|
||||
{teamMembers.map((member) => (
|
||||
<div
|
||||
key={member.name}
|
||||
className="flex items-center justify-between gap-4"
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
<Avatar className="border">
|
||||
<AvatarImage src={member.avatar} alt="Image" />
|
||||
<AvatarFallback>{member.name.charAt(0)}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<p className="text-sm leading-none font-medium">
|
||||
{member.name}
|
||||
</p>
|
||||
<p className="text-muted-foreground text-xs">{member.email}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="ml-auto shadow-none"
|
||||
>
|
||||
{member.role} <ChevronDown />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0" align="end">
|
||||
<Command>
|
||||
<CommandInput placeholder="Select role..." />
|
||||
<CommandList>
|
||||
<CommandEmpty>No roles found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{roles.map((role) => (
|
||||
<CommandItem key={role.name}>
|
||||
<div className="flex flex-col">
|
||||
<p className="text-sm font-medium">{role.name}</p>
|
||||
<p className="text-muted-foreground">
|
||||
{role.description}
|
||||
</p>
|
||||
</div>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
<Item key={member.name} size="sm" className="gap-4 px-0">
|
||||
<Avatar className="shrink-0 self-start border">
|
||||
<AvatarImage src={member.avatar} alt="Image" />
|
||||
<AvatarFallback>{member.name.charAt(0)}</AvatarFallback>
|
||||
</Avatar>
|
||||
<ItemContent>
|
||||
<ItemTitle>{member.name}</ItemTitle>
|
||||
<ItemDescription>{member.email}</ItemDescription>
|
||||
</ItemContent>
|
||||
<ItemActions>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="ml-auto shadow-none"
|
||||
>
|
||||
{member.role} <ChevronDown />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0" align="end">
|
||||
<Command>
|
||||
<CommandInput placeholder="Select role..." />
|
||||
<CommandList>
|
||||
<CommandEmpty>No roles found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{roles.map((role) => (
|
||||
<CommandItem key={role.name}>
|
||||
<div className="flex flex-col">
|
||||
<p className="text-sm font-medium">{role.name}</p>
|
||||
<p className="text-muted-foreground">
|
||||
{role.description}
|
||||
</p>
|
||||
</div>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</ItemActions>
|
||||
</Item>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -7,6 +7,7 @@ import { IconArrowRight } from "@tabler/icons-react"
|
||||
import { CornerDownLeftIcon, SquareDashedIcon } from "lucide-react"
|
||||
|
||||
import { type Color, type ColorPalette } from "@/lib/colors"
|
||||
import { showMcpDocs } from "@/lib/flags"
|
||||
import { source } from "@/lib/source"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { useConfig } from "@/hooks/use-config"
|
||||
@@ -30,6 +31,7 @@ import {
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/registry/new-york-v4/ui/dialog"
|
||||
import { Kbd, KbdGroup } from "@/registry/new-york-v4/ui/kbd"
|
||||
import { Separator } from "@/registry/new-york-v4/ui/separator"
|
||||
|
||||
export function CommandMenu({
|
||||
@@ -143,7 +145,7 @@ export function CommandMenu({
|
||||
<Button
|
||||
variant="secondary"
|
||||
className={cn(
|
||||
"bg-surface text-surface-foreground/60 dark:bg-card relative h-8 w-full justify-start pl-2.5 font-normal shadow-none sm:pr-12 md:w-40 lg:w-56 xl:w-64"
|
||||
"bg-surface text-foreground dark:bg-card relative h-8 w-full justify-start pl-3 font-medium shadow-none sm:pr-12 md:w-48 lg:w-56 xl:w-64"
|
||||
)}
|
||||
onClick={() => setOpen(true)}
|
||||
{...props}
|
||||
@@ -151,8 +153,10 @@ export function CommandMenu({
|
||||
<span className="hidden lg:inline-flex">Search documentation...</span>
|
||||
<span className="inline-flex lg:hidden">Search...</span>
|
||||
<div className="absolute top-1.5 right-1.5 hidden gap-1 sm:flex">
|
||||
<CommandMenuKbd>{isMac ? "⌘" : "Ctrl"}</CommandMenuKbd>
|
||||
<CommandMenuKbd className="aspect-square">K</CommandMenuKbd>
|
||||
<KbdGroup>
|
||||
<Kbd className="border">{isMac ? "⌘" : "Ctrl"}</Kbd>
|
||||
<Kbd className="border">K</Kbd>
|
||||
</KbdGroup>
|
||||
</div>
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
@@ -214,6 +218,10 @@ export function CommandMenu({
|
||||
if (item.type === "page") {
|
||||
const isComponent = item.url.includes("/components/")
|
||||
|
||||
if (!showMcpDocs && item.url.includes("/mcp")) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<CommandMenuItem
|
||||
key={item.url}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import Link from "next/link"
|
||||
|
||||
import { PAGES_NEW } from "@/lib/docs"
|
||||
import { source } from "@/lib/source"
|
||||
|
||||
export function ComponentsList() {
|
||||
@@ -21,9 +22,15 @@ export function ComponentsList() {
|
||||
<Link
|
||||
key={component.$id}
|
||||
href={component.url}
|
||||
className="text-lg font-medium underline-offset-4 hover:underline md:text-base"
|
||||
className="inline-flex items-center gap-2 text-lg font-medium underline-offset-4 hover:underline md:text-base"
|
||||
>
|
||||
{component.name}
|
||||
{PAGES_NEW.includes(component.url) && (
|
||||
<span
|
||||
className="flex size-2 rounded-full bg-blue-500"
|
||||
title="New"
|
||||
/>
|
||||
)}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
import Link from "next/link"
|
||||
import { usePathname } from "next/navigation"
|
||||
|
||||
import { PAGES_NEW } from "@/lib/docs"
|
||||
import { showMcpDocs } from "@/lib/flags"
|
||||
import type { source } from "@/lib/source"
|
||||
import {
|
||||
Sidebar,
|
||||
@@ -15,6 +17,32 @@ import {
|
||||
SidebarMenuItem,
|
||||
} from "@/registry/new-york-v4/ui/sidebar"
|
||||
|
||||
const TOP_LEVEL_SECTIONS = [
|
||||
{ name: "Get Started", href: "/docs" },
|
||||
{
|
||||
name: "Components",
|
||||
href: "/docs/components",
|
||||
},
|
||||
{
|
||||
name: "Registry",
|
||||
href: "/docs/registry",
|
||||
},
|
||||
{
|
||||
name: "MCP Server",
|
||||
href: "/docs/mcp",
|
||||
},
|
||||
{
|
||||
name: "Forms",
|
||||
href: "/docs/forms",
|
||||
},
|
||||
{
|
||||
name: "Changelog",
|
||||
href: "/docs/changelog",
|
||||
},
|
||||
]
|
||||
const EXCLUDED_SECTIONS = ["installation", "dark-mode"]
|
||||
const EXCLUDED_PAGES = ["/docs", "/docs/changelog"]
|
||||
|
||||
export function DocsSidebar({
|
||||
tree,
|
||||
...props
|
||||
@@ -23,40 +51,96 @@ export function DocsSidebar({
|
||||
|
||||
return (
|
||||
<Sidebar
|
||||
className="sticky top-[calc(var(--header-height)+1px)] z-30 hidden h-[calc(100svh-var(--header-height)-var(--footer-height))] bg-transparent lg:flex"
|
||||
className="sticky top-[calc(var(--header-height)+1px)] z-30 hidden h-[calc(100svh-var(--footer-height)+2rem)] bg-transparent lg:flex"
|
||||
collapsible="none"
|
||||
{...props}
|
||||
>
|
||||
<SidebarContent className="no-scrollbar px-2 pb-12">
|
||||
<SidebarContent className="no-scrollbar overflow-x-hidden px-2 pb-12">
|
||||
<div className="h-(--top-spacing) shrink-0" />
|
||||
{tree.children.map((item) => (
|
||||
<SidebarGroup key={item.$id}>
|
||||
<SidebarGroupLabel className="text-muted-foreground font-medium">
|
||||
{item.name}
|
||||
</SidebarGroupLabel>
|
||||
<SidebarGroupContent>
|
||||
{item.type === "folder" && (
|
||||
<SidebarMenu className="gap-0.5">
|
||||
{item.children.map((item) => {
|
||||
return (
|
||||
item.type === "page" && (
|
||||
<SidebarMenuItem key={item.url}>
|
||||
<SidebarMenuButton
|
||||
asChild
|
||||
isActive={item.url === pathname}
|
||||
className="data-[active=true]:bg-accent data-[active=true]:border-accent 3xl:fixed:w-full 3xl:fixed:max-w-48 relative h-[30px] w-fit overflow-visible border border-transparent text-[0.8rem] font-medium after:absolute after:inset-x-0 after:-inset-y-1 after:z-0 after:rounded-md"
|
||||
>
|
||||
<Link href={item.url}>{item.name}</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarGroup>
|
||||
<SidebarGroupLabel className="text-muted-foreground font-medium">
|
||||
Sections
|
||||
</SidebarGroupLabel>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
{TOP_LEVEL_SECTIONS.map(({ name, href }) => {
|
||||
if (!showMcpDocs && href.includes("/mcp")) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<SidebarMenuItem key={name}>
|
||||
<SidebarMenuButton
|
||||
asChild
|
||||
isActive={
|
||||
href === "/docs"
|
||||
? pathname === href
|
||||
: pathname.startsWith(href)
|
||||
}
|
||||
className="data-[active=true]:bg-accent data-[active=true]:border-accent 3xl:fixed:w-full 3xl:fixed:max-w-48 relative h-[30px] w-fit overflow-visible border border-transparent text-[0.8rem] font-medium after:absolute after:inset-x-0 after:-inset-y-1 after:z-0 after:rounded-md"
|
||||
>
|
||||
<Link href={href}>
|
||||
<span className="absolute inset-0 flex w-(--sidebar-width) bg-transparent" />
|
||||
{name}
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
)
|
||||
})}
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
{tree.children.map((item) => {
|
||||
if (EXCLUDED_SECTIONS.includes(item.$id ?? "")) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<SidebarGroup key={item.$id}>
|
||||
<SidebarGroupLabel className="text-muted-foreground font-medium">
|
||||
{item.name}
|
||||
</SidebarGroupLabel>
|
||||
<SidebarGroupContent>
|
||||
{item.type === "folder" && (
|
||||
<SidebarMenu className="gap-0.5">
|
||||
{item.children.map((item) => {
|
||||
if (
|
||||
!showMcpDocs &&
|
||||
item.type === "page" &&
|
||||
item.url?.includes("/mcp")
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
item.type === "page" &&
|
||||
!EXCLUDED_PAGES.includes(item.url) && (
|
||||
<SidebarMenuItem key={item.url}>
|
||||
<SidebarMenuButton
|
||||
asChild
|
||||
isActive={item.url === pathname}
|
||||
className="data-[active=true]:bg-accent data-[active=true]:border-accent 3xl:fixed:w-full 3xl:fixed:max-w-48 relative h-[30px] w-fit overflow-visible border border-transparent text-[0.8rem] font-medium after:absolute after:inset-x-0 after:-inset-y-1 after:z-0 after:rounded-md"
|
||||
>
|
||||
<Link href={item.url}>
|
||||
<span className="absolute inset-0 flex w-(--sidebar-width) bg-transparent" />
|
||||
{item.name}
|
||||
{PAGES_NEW.includes(item.url) && (
|
||||
<span
|
||||
className="flex size-2 rounded-full bg-blue-500"
|
||||
title="New"
|
||||
/>
|
||||
)}
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
)
|
||||
)
|
||||
)
|
||||
})}
|
||||
</SidebarMenu>
|
||||
)}
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
))}
|
||||
})}
|
||||
</SidebarMenu>
|
||||
)}
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
)
|
||||
})}
|
||||
</SidebarContent>
|
||||
</Sidebar>
|
||||
)
|
||||
|
||||
@@ -4,6 +4,8 @@ import * as React from "react"
|
||||
import Link, { LinkProps } from "next/link"
|
||||
import { useRouter } from "next/navigation"
|
||||
|
||||
import { PAGES_NEW } from "@/lib/docs"
|
||||
import { showMcpDocs } from "@/lib/flags"
|
||||
import { source } from "@/lib/source"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
@@ -13,6 +15,30 @@ import {
|
||||
PopoverTrigger,
|
||||
} from "@/registry/new-york-v4/ui/popover"
|
||||
|
||||
const TOP_LEVEL_SECTIONS = [
|
||||
{ name: "Get Started", href: "/docs" },
|
||||
{
|
||||
name: "Components",
|
||||
href: "/docs/components",
|
||||
},
|
||||
{
|
||||
name: "Registry",
|
||||
href: "/docs/registry",
|
||||
},
|
||||
{
|
||||
name: "MCP Server",
|
||||
href: "/docs/mcp",
|
||||
},
|
||||
{
|
||||
name: "Forms",
|
||||
href: "/docs/forms",
|
||||
},
|
||||
{
|
||||
name: "Changelog",
|
||||
href: "/docs/changelog",
|
||||
},
|
||||
]
|
||||
|
||||
export function MobileNav({
|
||||
tree,
|
||||
items,
|
||||
@@ -79,6 +105,23 @@ export function MobileNav({
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="text-muted-foreground text-sm font-medium">
|
||||
Sections
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
{TOP_LEVEL_SECTIONS.map(({ name, href }) => {
|
||||
if (!showMcpDocs && href.includes("/mcp")) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<MobileLink key={name} href={href} onOpenChange={setOpen}>
|
||||
{name}
|
||||
</MobileLink>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-8">
|
||||
{tree?.children?.map((group, index) => {
|
||||
if (group.type === "folder") {
|
||||
@@ -90,13 +133,20 @@ export function MobileNav({
|
||||
<div className="flex flex-col gap-3">
|
||||
{group.children.map((item) => {
|
||||
if (item.type === "page") {
|
||||
if (!showMcpDocs && item.url.includes("/mcp")) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<MobileLink
|
||||
key={`${item.url}-${index}`}
|
||||
href={item.url}
|
||||
onOpenChange={setOpen}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
{item.name}
|
||||
{item.name}{" "}
|
||||
{PAGES_NEW.includes(item.url) && (
|
||||
<span className="flex size-2 rounded-full bg-blue-500" />
|
||||
)}
|
||||
</MobileLink>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const SHOW = false
|
||||
const SHOW = true
|
||||
|
||||
export function TailwindIndicator() {
|
||||
if (process.env.NODE_ENV === "production" || !SHOW) {
|
||||
|
||||
@@ -4,6 +4,627 @@ description: Latest updates and announcements.
|
||||
toc: false
|
||||
---
|
||||
|
||||
## October 2025 - New Components
|
||||
|
||||
For this round of components, I looked at what we build every day, the boring stuff we rebuild over and over, and made reusable abstractions you can actually use.
|
||||
|
||||
**These components work with every component library, Radix, Base UI, React Aria, you name it. Copy and paste to your projects.**
|
||||
|
||||
- [Spinner](#spinner): An indicator to show a loading state.
|
||||
- [Kbd](#kbd): Display a keyboard key or group of keys.
|
||||
- [Button Group](#button-group): A group of buttons for actions and split buttons.
|
||||
- [Input Group](#input-group): Input with icons, buttons, labels and more.
|
||||
- [Field](#field): One component. All your forms.
|
||||
- [Item](#item): Display lists of items, cards, and more.
|
||||
- [Empty](#empty): Use this one for empty states.
|
||||
|
||||
### Spinner
|
||||
|
||||
Okay let's start with the easiest ones: **Spinner** and **Kbd**. Pretty basic. We all know what they do.
|
||||
|
||||
Here's how you render a spinner:
|
||||
|
||||
```tsx
|
||||
import { Spinner } from "@/components/ui/spinner"
|
||||
```
|
||||
|
||||
```tsx
|
||||
<Spinner />
|
||||
```
|
||||
|
||||
Here's what it looks like:
|
||||
|
||||
<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] [&_pre]:!h-[250px]"
|
||||
/>
|
||||
|
||||
You can edit the code and replace it with your own spinner.
|
||||
|
||||
<ComponentPreview
|
||||
name="spinner-custom"
|
||||
className="[&_.preview]:h-[250px] [&_pre]:!h-[250px]"
|
||||
/>
|
||||
|
||||
### Kbd
|
||||
|
||||
Kbd is a component that renders a keyboard key.
|
||||
|
||||
```tsx
|
||||
import { Kbd, KbdGroup } from "@/components/ui/kbd"
|
||||
```
|
||||
|
||||
```tsx
|
||||
<Kbd>Ctrl</Kbd>
|
||||
```
|
||||
|
||||
Use `KbdGroup` to group keyboard keys together.
|
||||
|
||||
```tsx showLineNumbers
|
||||
<KbdGroup>
|
||||
<Kbd>Ctrl</Kbd>
|
||||
<Kbd>B</Kbd>
|
||||
</KbdGroup>
|
||||
```
|
||||
|
||||
<ComponentPreview
|
||||
name="kbd-demo"
|
||||
className="[&_.preview]:h-[250px] [&_pre]:!h-[250px]"
|
||||
/>
|
||||
|
||||
You can add it to buttons, tooltips, input groups, and more.
|
||||
|
||||
### Button Group
|
||||
|
||||
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] [&_pre]:!h-[250px]"
|
||||
/>
|
||||
|
||||
Here's the code:
|
||||
|
||||
```tsx
|
||||
import { ButtonGroup } from "@/components/ui/button-group"
|
||||
```
|
||||
|
||||
```tsx showLineNumbers
|
||||
<ButtonGroup>
|
||||
<Button>Button 1</Button>
|
||||
<Button>Button 2</Button>
|
||||
</ButtonGroup>
|
||||
```
|
||||
|
||||
You can nest button groups to create more complex layouts with spacing.
|
||||
|
||||
```tsx showLineNumbers
|
||||
<ButtonGroup>
|
||||
<ButtonGroup>
|
||||
<Button>Button 1</Button>
|
||||
<Button>Button 2</Button>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup>
|
||||
<Button>Button 3</Button>
|
||||
<Button>Button 4</Button>
|
||||
</ButtonGroup>
|
||||
</ButtonGroup>
|
||||
```
|
||||
|
||||
Use `ButtonGroupSeparator` to create split buttons. Classic dropdown pattern.
|
||||
|
||||
<ComponentPreview
|
||||
name="button-group-dropdown"
|
||||
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] [&_pre]:!h-[250px]"
|
||||
/>
|
||||
|
||||
```tsx showLineNumbers
|
||||
<ButtonGroup>
|
||||
<ButtonGroupText>Prefix</ButtonGroupText>
|
||||
<Input placeholder="Type something here..." />
|
||||
<Button>Button</Button>
|
||||
</ButtonGroup>
|
||||
```
|
||||
|
||||
### Input Group
|
||||
|
||||
Input Group lets you add icons, buttons, and more to your inputs. You know, all those little bits you always need around your inputs.
|
||||
|
||||
```tsx
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupInput,
|
||||
} from "@/components/ui/input-group"
|
||||
```
|
||||
|
||||
```tsx showLineNumbers
|
||||
<InputGroup>
|
||||
<InputGroupInput placeholder="Search..." />
|
||||
<InputGroupAddon>
|
||||
<SearchIcon />
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
```
|
||||
|
||||
Here's a preview with icons:
|
||||
|
||||
<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] [&_pre]:!h-[300px]"
|
||||
/>
|
||||
|
||||
Or text, labels, tooltips,...
|
||||
|
||||
<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] [&_pre]:!h-[450px]"
|
||||
/>
|
||||
|
||||
Oh here are some cool ones with spinners:
|
||||
|
||||
<ComponentPreview
|
||||
name="input-group-spinner"
|
||||
className="[&_.preview]:h-[350px] [&_pre]:!h-[350px]"
|
||||
/>
|
||||
|
||||
### Field
|
||||
|
||||
Introducing **Field**, a component for building really complex forms. The abstraction here is beautiful.
|
||||
|
||||
It took me a long time to get it right but I made it work with all your form libraries: Server Actions, React Hook Form, TanStack Form, Bring Your Own Form.
|
||||
|
||||
```tsx
|
||||
import {
|
||||
Field,
|
||||
FieldDescription,
|
||||
FieldError,
|
||||
FieldLabel,
|
||||
} from "@/components/ui/field"
|
||||
```
|
||||
|
||||
Here's a basic field with an input:
|
||||
|
||||
```tsx showLineNumbers
|
||||
<Field>
|
||||
<FieldLabel htmlFor="username">Username</FieldLabel>
|
||||
<Input id="username" placeholder="Max Leiter" />
|
||||
<FieldDescription>
|
||||
Choose a unique username for your account.
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
```
|
||||
|
||||
<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] [&_pre]:!h-[850px]"
|
||||
/>
|
||||
|
||||
Here are some checkbox fields:
|
||||
|
||||
<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.
|
||||
|
||||
```tsx showLineNumbers
|
||||
<FieldSet>
|
||||
<FieldLegend />
|
||||
<FieldGroup>
|
||||
<Field />
|
||||
<Field />
|
||||
</FieldGroup>
|
||||
</FieldSet>
|
||||
```
|
||||
|
||||
<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] [&_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] [&_pre]:!h-[600px]"
|
||||
/>
|
||||
|
||||
### Item
|
||||
|
||||
This one is a straightforward flex container that can house nearly any type of content.
|
||||
|
||||
I've built this so many times that I decided to create a component for it. Now I use it all the time. I use it to display lists of items, cards, and more.
|
||||
|
||||
```tsx
|
||||
import {
|
||||
Item,
|
||||
ItemContent,
|
||||
ItemDescription,
|
||||
ItemMedia,
|
||||
ItemTitle,
|
||||
} from "@/components/ui/item"
|
||||
```
|
||||
|
||||
Here's a basic item:
|
||||
|
||||
```tsx showLineNumbers
|
||||
<Item>
|
||||
<ItemMedia variant="icon">
|
||||
<HomeIcon />
|
||||
</ItemMedia>
|
||||
<ItemContent>
|
||||
<ItemTitle>Dashboard</ItemTitle>
|
||||
<ItemDescription>Overview of your account and activity.</ItemDescription>
|
||||
</ItemContent>
|
||||
</Item>
|
||||
```
|
||||
|
||||
<ComponentPreview
|
||||
name="item-demo"
|
||||
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 [&_pre]:!h-[300px]"
|
||||
/>
|
||||
|
||||
<ComponentPreview
|
||||
name="item-avatar"
|
||||
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 [&_pre]:!h-[500px]"
|
||||
/>
|
||||
|
||||
Need it as a link? Use the `asChild` prop:
|
||||
|
||||
```tsx showLineNumbers
|
||||
<Item asChild>
|
||||
<a href="/dashboard">
|
||||
<ItemMedia variant="icon">
|
||||
<HomeIcon />
|
||||
</ItemMedia>
|
||||
<ItemContent>
|
||||
<ItemTitle>Dashboard</ItemTitle>
|
||||
<ItemDescription>Overview of your account and activity.</ItemDescription>
|
||||
</ItemContent>
|
||||
</a>
|
||||
</Item>
|
||||
```
|
||||
|
||||
<ComponentPreview
|
||||
name="item-link"
|
||||
className="[&_.preview]:h-[400px] [&_.preview]:p-4 [&_pre]:!h-[400px]"
|
||||
/>
|
||||
|
||||
### Empty
|
||||
|
||||
Okay last one: **Empty**. Use this to display empty states in your app.
|
||||
|
||||
```tsx
|
||||
import {
|
||||
Empty,
|
||||
EmptyContent,
|
||||
EmptyDescription,
|
||||
EmptyMedia,
|
||||
EmptyTitle,
|
||||
} from "@/components/ui/empty"
|
||||
```
|
||||
|
||||
Here's how you use it:
|
||||
|
||||
```tsx showLineNumbers
|
||||
<Empty>
|
||||
<EmptyMedia variant="icon">
|
||||
<InboxIcon />
|
||||
</EmptyMedia>
|
||||
<EmptyTitle>No messages</EmptyTitle>
|
||||
<EmptyDescription>You don't have any messages yet.</EmptyDescription>
|
||||
<EmptyContent>
|
||||
<Button>Send a message</Button>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
```
|
||||
|
||||
<ComponentPreview
|
||||
name="empty-demo"
|
||||
className="[&_.preview]:h-[400px] [&_.preview]:p-4 [&_pre]:!h-[400px]"
|
||||
/>
|
||||
|
||||
You can use it with avatars:
|
||||
|
||||
<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] [&_pre]:!h-[450px]"
|
||||
/>
|
||||
|
||||
That's it. Seven new components. Works with all your libraries. Ready for your projects.
|
||||
|
||||
---
|
||||
|
||||
## September 2025 - Registry Index
|
||||
|
||||
We've created an index of open source registries that you can install items from.
|
||||
|
||||
You can search, view and add items from the registry index without configuring the `.components.json` file.
|
||||
|
||||
They'll be automatically added to your `components.json` file for you.
|
||||
|
||||
```bash
|
||||
npx shadcn add @ai-elements/prompt-input
|
||||
```
|
||||
|
||||
The full list of registries is available at [https://ui.shadcn.com/r/registries.json](https://ui.shadcn.com/r/registries.json).
|
||||
|
||||
To add a registry to the index, submit a PR to the `shadcn/ui` repository. See the [registry index documentation](/docs/registry/registry-index) for more details.
|
||||
|
||||
---
|
||||
|
||||
## August 2025 - shadcn CLI 3.0 and MCP Server
|
||||
|
||||
We just shipped shadcn CLI 3.0 with support for namespaced registries, advanced authentication, new commands and a completely rewritten registry engine.
|
||||
|
||||
### What's New
|
||||
|
||||
- [Namespaced Registries](#namespaced-registries) - Install components using `@registry/name` format.
|
||||
- [Private Registries](#private-registries) - Secure your registry with advanced authentication.
|
||||
- [Search & Discovery](#search--discovery) - New commands to find and view code before installing.
|
||||
- [MCP Server](#mcp-server) - MCP server for all registries.
|
||||
- [Faster Everything](#faster-everything) - Completely rewritten registry resolution.
|
||||
- [Improved Error Handling](#improved-error-handling) - Better error messages for users and LLMs.
|
||||
- [Upgrade Guide](#upgrade-guide) - Migration notes for existing users.
|
||||
|
||||
### Namespaced Registries
|
||||
|
||||
The biggest change in 3.0 is namespaced registries. You can now install components from registries: a community registry, your company's private registry or internal registry, using the `@registry/name` format.
|
||||
|
||||
This makes it easier to distribute code across teams and projects.
|
||||
|
||||
Configure registries in your `components.json`:
|
||||
|
||||
```json title="components.json"
|
||||
{
|
||||
"registries": {
|
||||
"@acme": "https://acme.com/r/{name}.json",
|
||||
"@internal": {
|
||||
"url": "https://registry.company.com/{name}",
|
||||
"headers": {
|
||||
"Authorization": "Bearer ${REGISTRY_TOKEN}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Then use the `@registry/name` format to install components:
|
||||
|
||||
```bash
|
||||
npx shadcn add @acme/button @internal/auth-system
|
||||
```
|
||||
|
||||
It's completely decentralized. There's no central registrar. Create any namespace you want and organize components however makes sense for your team.
|
||||
|
||||
```json title="components.json" showLineNumbers
|
||||
{
|
||||
"registries": {
|
||||
"@design": "https://registry.company.com/design/{name}.json",
|
||||
"@engineering": "https://registry.company.com/eng/{name}.json",
|
||||
"@marketing": "https://registry.company.com/marketing/{name}.json"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Components can even depend on resources from different registries. Everything gets resolved and installed automatically from the right sources.
|
||||
|
||||
```json title="registry-item.json" showLineNumbers
|
||||
{
|
||||
"name": "dashboard",
|
||||
"type": "registry:block",
|
||||
"registryDependencies": [
|
||||
"@shadcn/card", // From default registry
|
||||
"@v0/chart", // From v0 registry
|
||||
"@acme/data-table", // From acme registry
|
||||
"@lib/data-fetcher", // Utility library
|
||||
"@ai/analytics-prompt" // AI prompt resource
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Private Registries
|
||||
|
||||
Need to keep your components private? We've got you covered. Configure authentication with tokens, API keys, or custom headers:
|
||||
|
||||
```json title="components.json"
|
||||
{
|
||||
"registries": {
|
||||
"@private": {
|
||||
"url": "https://registry.company.com/{name}.json",
|
||||
"headers": {
|
||||
"Authorization": "Bearer ${REGISTRY_TOKEN}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Your private components stay private. Perfect for enterprise teams with proprietary UI libraries.
|
||||
|
||||
We support all major authentication methods: basic auth, bearer token, api key query params and custom headers.
|
||||
|
||||
See the [authentication docs](/docs/registry/authentication) for more details.
|
||||
|
||||
### Search & Discovery
|
||||
|
||||
Three new commands make it easy to find exactly what you need:
|
||||
|
||||
1. View items from the registry before installing
|
||||
|
||||
```bash
|
||||
npx shadcn view @acme/auth-system
|
||||
```
|
||||
|
||||
2. Search items from registries
|
||||
|
||||
```bash
|
||||
npx shadcn search @tweakcn -q "dark"
|
||||
```
|
||||
|
||||
3. List all items from a registry
|
||||
|
||||
```bash
|
||||
npx shadcn list @acme
|
||||
```
|
||||
|
||||
Preview components before installing them. Search across multiple registries. See the code and all dependencies upfront.
|
||||
|
||||
### MCP Server
|
||||
|
||||
<Image
|
||||
src="/images/mcp.jpeg"
|
||||
width="1432"
|
||||
height="1050"
|
||||
alt="Lift Mode"
|
||||
className="mt-6 w-full overflow-hidden rounded-lg border"
|
||||
/>
|
||||
|
||||
Back in April, we [introduced](https://x.com/shadcn/status/1917597228513853603) the first version of the MCP server. Since then, we've taken everything we learned and built a better MCP server.
|
||||
|
||||
Here's what's new:
|
||||
|
||||
- Works with all registries. Zero config
|
||||
- One command to add to your favorite MCP clients
|
||||
- We improved the underlying tools
|
||||
- Better integration with the CLI and registries
|
||||
- Support for multiple registries in the same project
|
||||
|
||||
Add the MCP server to your project:
|
||||
|
||||
```bash
|
||||
npx shadcn@latest mcp init
|
||||
```
|
||||
|
||||
See the [docs](/docs/mcp) for more details.
|
||||
|
||||
### Faster Everything
|
||||
|
||||
We completely rewrote the registry resolution engine from scratch. It's faster, smarter, and handles even the trickiest dependency trees.
|
||||
|
||||
- Up to 3x faster dependency resolution
|
||||
- Smarter file deduplication and merging
|
||||
- Better monorepo support out of the box
|
||||
- Updated `build` command for registry authors
|
||||
|
||||
### Improved Error Handling
|
||||
|
||||
Registry developers can now provide custom error messages to help guide users (and LLMs) when things go wrong. The CLI displays helpful, actionable errors for common issues:
|
||||
|
||||
```txt
|
||||
Unknown registry "@acme". Make sure it is defined in components.json as follows:
|
||||
{
|
||||
"registries": {
|
||||
"@acme": "[URL_TO_REGISTRY]"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Missing environment variables? The CLI tells you exactly what's needed:
|
||||
|
||||
```txt
|
||||
Registry "@private" requires the following environment variables:
|
||||
• REGISTRY_TOKEN
|
||||
|
||||
Set the required environment variables to your .env or .env.local file.
|
||||
```
|
||||
|
||||
Registry authors can provide custom error messages in their responses to help users and AI agents understand and fix issues quickly.
|
||||
|
||||
```txt
|
||||
Error:
|
||||
You are not authorized to access the item at http://example.com/r/component.
|
||||
|
||||
Message:
|
||||
[Unauthorized] Your API key has expired. Renew it at https://example.com/api/renew-key.
|
||||
```
|
||||
|
||||
### Upgrade Guide
|
||||
|
||||
Here's the best part: there are no breaking changes for users. Your existing `components.json` works exactly the same. All your installed components work exactly the same.
|
||||
|
||||
For developers, if you're using the programmatic APIs directly, we've deprecated a few functions in favor of better ones:
|
||||
|
||||
- `fetchRegistry` → `getRegistry`
|
||||
- `resolveRegistryTree` → `resolveRegistryItems`
|
||||
- Schema moved from `shadcn/registry` to `shadcn/schema` package
|
||||
|
||||
```diff
|
||||
- import { registryItemSchema } from "shadcn/registry"
|
||||
+ import { registryItemSchema } from "shadcn/schema"
|
||||
```
|
||||
|
||||
That's it. Seriously. Everything else just works.
|
||||
|
||||
---
|
||||
|
||||
## July 2025 - Universal Registry Items
|
||||
|
||||
We've added support for universal registry items. This allows you to create registry items that can be distributed to any project i.e. no framework, no components.json, no tailwind, no react required.
|
||||
@@ -12,6 +633,8 @@ This new registry item type unlocks a lot of new workflows. You can now distribu
|
||||
|
||||
See the [docs](/docs/registry/examples) for more details and examples.
|
||||
|
||||
---
|
||||
|
||||
## July 2025 - Local File Support
|
||||
|
||||
The shadcn CLI now supports local files. Initialize projects and add components, themes, hooks, utils and more from local JSON files.
|
||||
@@ -31,6 +654,8 @@ This feature enables powerful new workflows:
|
||||
- **Enhanced workflow for agents and MCP** - Generate and run registry items locally
|
||||
- **Private components** - Keep proprietary components local and private.
|
||||
|
||||
---
|
||||
|
||||
## June 2025 - `radix-ui`
|
||||
|
||||
We've added a new command to migrate to the new `radix-ui` package. This command will replace all `@radix-ui/react-*` imports with `radix-ui`.
|
||||
@@ -82,7 +707,7 @@ We're working on zero-config MCP support for shadcn/ui registry. One command `np
|
||||
className="mt-6 w-full overflow-hidden rounded-lg border"
|
||||
/>
|
||||
|
||||
Learn more in the thread here: https://x.com/shadcn/status/1917597228513853603
|
||||
Learn more in the [thread here](https://x.com/shadcn/status/1917597228513853603).
|
||||
|
||||
## March 2025 - shadcn 2.5.0
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ The `init` command installs dependencies, adds the `cn` util and configures CSS
|
||||
npx shadcn@latest init
|
||||
```
|
||||
|
||||
### Options
|
||||
**Options**
|
||||
|
||||
```bash
|
||||
Usage: shadcn init [options] [components...]
|
||||
@@ -34,9 +34,12 @@ Options:
|
||||
--no-src-dir do not use the src directory when creating a new project.
|
||||
--css-variables use css variables for theming. (default: true)
|
||||
--no-css-variables do not use css variables for theming.
|
||||
--no-base-style do not install the base shadcn style
|
||||
-h, --help display help for command
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## add
|
||||
|
||||
Use the `add` command to add components and dependencies to your project.
|
||||
@@ -45,7 +48,7 @@ Use the `add` command to add components and dependencies to your project.
|
||||
npx shadcn@latest add [component]
|
||||
```
|
||||
|
||||
### Options
|
||||
**Options**
|
||||
|
||||
```bash
|
||||
Usage: shadcn add [options] [components...]
|
||||
@@ -69,6 +72,126 @@ Options:
|
||||
-h, --help display help for command
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## view
|
||||
|
||||
Use the `view` command to view items from the registry before installing them.
|
||||
|
||||
```bash
|
||||
npx shadcn@latest view [item]
|
||||
```
|
||||
|
||||
You can view multiple items at once:
|
||||
|
||||
```bash
|
||||
npx shadcn@latest view button card dialog
|
||||
```
|
||||
|
||||
Or view items from namespaced registries:
|
||||
|
||||
```bash
|
||||
npx shadcn@latest view @acme/auth @v0/dashboard
|
||||
```
|
||||
|
||||
**Options**
|
||||
|
||||
```bash
|
||||
Usage: shadcn view [options] <items...>
|
||||
|
||||
view items from the registry
|
||||
|
||||
Arguments:
|
||||
items the item names or URLs to view
|
||||
|
||||
Options:
|
||||
-c, --cwd <cwd> the working directory. defaults to the current directory.
|
||||
-h, --help display help for command
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## search
|
||||
|
||||
Use the `search` command to search for items from registries.
|
||||
|
||||
```bash
|
||||
npx shadcn@latest search [registry]
|
||||
```
|
||||
|
||||
You can search with a query:
|
||||
|
||||
```bash
|
||||
npx shadcn@latest search @shadcn -q "button"
|
||||
```
|
||||
|
||||
Or search multiple registries at once:
|
||||
|
||||
```bash
|
||||
npx shadcn@latest search @shadcn @v0 @acme
|
||||
```
|
||||
|
||||
The `list` command is an alias for `search`:
|
||||
|
||||
```bash
|
||||
npx shadcn@latest list @acme
|
||||
```
|
||||
|
||||
**Options**
|
||||
|
||||
```bash
|
||||
Usage: shadcn search|list [options] <registries...>
|
||||
|
||||
search items from registries
|
||||
|
||||
Arguments:
|
||||
registries the registry names or urls to search items from. Names
|
||||
must be prefixed with @.
|
||||
|
||||
Options:
|
||||
-c, --cwd <cwd> the working directory. defaults to the current directory.
|
||||
-q, --query <query> query string
|
||||
-l, --limit <number> maximum number of items to display per registry (default: "100")
|
||||
-o, --offset <number> number of items to skip (default: "0")
|
||||
-h, --help display help for command
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## list
|
||||
|
||||
Use the `list` command to list all items from a registry.
|
||||
|
||||
```bash
|
||||
npx shadcn@latest list @acme
|
||||
```
|
||||
|
||||
**Options**
|
||||
|
||||
```bash
|
||||
Usage: shadcn list [options] <registries...>
|
||||
|
||||
list items from registries
|
||||
|
||||
Arguments:
|
||||
registries the registry names or urls to list items from. Names
|
||||
must be prefixed with @.
|
||||
```
|
||||
|
||||
**Options**
|
||||
|
||||
```bash
|
||||
Usage: shadcn list [options] <registries...>
|
||||
|
||||
list items from registries
|
||||
|
||||
Arguments:
|
||||
registries the registry names or urls to list items from. Names
|
||||
must be prefixed with @.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## build
|
||||
|
||||
Use the `build` command to generate the registry JSON files.
|
||||
@@ -79,7 +202,7 @@ npx shadcn@latest build
|
||||
|
||||
This command reads the `registry.json` file and generates the registry JSON files in the `public/r` directory.
|
||||
|
||||
### Options
|
||||
**Options**
|
||||
|
||||
```bash
|
||||
Usage: shadcn build [options] [registry]
|
||||
|
||||
@@ -210,3 +210,94 @@ Import alias for `hooks` such as `use-media-query` or `use-toast`.
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## registries
|
||||
|
||||
Configure multiple resource registries for your project. This allows you to install components, libraries, utilities, and other resources from various sources including private registries.
|
||||
|
||||
See the <Link href="/docs/registry/namespace">Namespaced Registries</Link> documentation for detailed information.
|
||||
|
||||
### Basic Configuration
|
||||
|
||||
Configure registries with URL templates:
|
||||
|
||||
```json title="components.json"
|
||||
{
|
||||
"registries": {
|
||||
"@v0": "https://v0.dev/chat/b/{name}",
|
||||
"@acme": "https://registry.acme.com/{name}.json",
|
||||
"@internal": "https://internal.company.com/{name}.json"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `{name}` placeholder is replaced with the resource name when installing.
|
||||
|
||||
### Advanced Configuration with Authentication
|
||||
|
||||
For private registries that require authentication:
|
||||
|
||||
```json title="components.json"
|
||||
{
|
||||
"registries": {
|
||||
"@private": {
|
||||
"url": "https://api.company.com/registry/{name}.json",
|
||||
"headers": {
|
||||
"Authorization": "Bearer ${REGISTRY_TOKEN}",
|
||||
"X-API-Key": "${API_KEY}"
|
||||
},
|
||||
"params": {
|
||||
"version": "latest"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Environment variables in the format `${VAR_NAME}` are automatically expanded from your environment.
|
||||
|
||||
### Using Namespaced Registries
|
||||
|
||||
Once configured, install resources using the namespace syntax:
|
||||
|
||||
```bash
|
||||
# Install from a configured registry
|
||||
npx shadcn@latest add @v0/dashboard
|
||||
|
||||
# Install from private registry
|
||||
npx shadcn@latest add @private/button
|
||||
|
||||
# Install multiple resources
|
||||
npx shadcn@latest add @acme/header @internal/auth-utils
|
||||
```
|
||||
|
||||
### Example: Multiple Registry Setup
|
||||
|
||||
```json title="components.json"
|
||||
{
|
||||
"registries": {
|
||||
"@shadcn": "https://ui.shadcn.com/r/{name}.json",
|
||||
"@company-ui": {
|
||||
"url": "https://registry.company.com/ui/{name}.json",
|
||||
"headers": {
|
||||
"Authorization": "Bearer ${COMPANY_TOKEN}"
|
||||
}
|
||||
},
|
||||
"@team": {
|
||||
"url": "https://team.company.com/{name}.json",
|
||||
"params": {
|
||||
"team": "frontend",
|
||||
"version": "${REGISTRY_VERSION}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This configuration allows you to:
|
||||
|
||||
- Install public components from shadcn/ui
|
||||
- Access private company UI components with authentication
|
||||
- Use team-specific resources with versioning
|
||||
|
||||
For more information about authentication, see the <Link href="/docs/registry/authentication">Authentication</Link> documentation.
|
||||
|
||||
@@ -11,6 +11,7 @@ description: Every component recreated in Figma. With customizable props, typogr
|
||||
## Paid
|
||||
|
||||
- [shadcn/ui kit](https://shadcndesign.com) by [ Matt Wierzbicki](https://x.com/matsugfx) - A premium, always up-to-date UI kit for Figma - shadcn/ui compatible and optimized for smooth design-to-dev handoff.
|
||||
- [Shadcraft UI Kit](https://shadcraft.com) - The most advanced shadcn-compatible kit with instant theming via [tweakcn](https://tweakcn.com), a pro library of components and templates, and complete coverage of shadcn components and blocks.
|
||||
|
||||
## Free
|
||||
|
||||
|
||||
324
apps/v4/content/docs/(root)/mcp.mdx
Normal file
324
apps/v4/content/docs/(root)/mcp.mdx
Normal file
@@ -0,0 +1,324 @@
|
||||
---
|
||||
title: MCP Server
|
||||
description: Use the shadcn MCP server to browse, search, and install components from registries.
|
||||
---
|
||||
|
||||
The shadcn MCP Server allows AI assistants to interact with items from registries. You can browse available components, search for specific ones, and install them directly into your project using natural language.
|
||||
|
||||
For example, you can ask an AI assistant to "Build a landing page using components from the acme registry" or "Find me a login form from the shadcn registry".
|
||||
|
||||
Registries are configured in your project's `components.json` file.
|
||||
|
||||
```json title="components.json" showLineNumbers
|
||||
{
|
||||
"registries": {
|
||||
"@acme": "https://acme.com/r/{name}.json"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
Select your MCP client and follow the instructions to configure the shadcn MCP server. If you'd like to do it manually, see the [Configuration](#configuration) section.
|
||||
|
||||
<Tabs defaultValue="claude">
|
||||
<TabsList>
|
||||
<TabsTrigger value="claude">Claude Code</TabsTrigger>
|
||||
<TabsTrigger value="cursor">Cursor</TabsTrigger>
|
||||
<TabsTrigger value="vscode">VS Code</TabsTrigger>
|
||||
<TabsTrigger value="codex">Codex</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="claude" className="mt-4">
|
||||
**Run the following command** in your project:
|
||||
```bash
|
||||
npx shadcn@latest mcp init --client claude
|
||||
```
|
||||
|
||||
**Restart Claude Code** and try the following prompts:
|
||||
- Show me all available components in the shadcn registry
|
||||
- Add the button, dialog and card components to my project
|
||||
- Create a contact form using components from the shadcn registry
|
||||
|
||||
**Note:** You can use `/mcp` command in Claude Code to debug the MCP server.
|
||||
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="cursor" className="mt-4">
|
||||
**Run the following command** in your project:
|
||||
```bash
|
||||
npx shadcn@latest mcp init --client cursor
|
||||
```
|
||||
|
||||
Open **Cursor Settings** and **Enable the MCP server** for shadcn. Then try the following prompts:
|
||||
- Show me all available components in the shadcn registry
|
||||
- Add the button, dialog and card components to my project
|
||||
- Create a contact form using components from the shadcn registry
|
||||
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="vscode" className="mt-4">
|
||||
**Run the following command** in your project:
|
||||
```bash
|
||||
npx shadcn@latest mcp init --client vscode
|
||||
```
|
||||
|
||||
Open `.vscode/mcp.json` and click **Start** next to the shadcn server. Then try the following prompts with GitHub Copilot:
|
||||
- Show me all available components in the shadcn registry
|
||||
- Add the button, dialog and card components to my project
|
||||
- Create a contact form using components from the shadcn registry
|
||||
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="codex" className="mt-4">
|
||||
<Callout className="mt-0">
|
||||
**Note:** The `shadcn` CLI cannot automatically update `~/.codex/config.toml`.
|
||||
You'll need to add the configuration manually for Codex.
|
||||
</Callout>
|
||||
|
||||
**Run the following command** in your project:
|
||||
```bash
|
||||
npx shadcn@latest mcp init --client codex
|
||||
```
|
||||
|
||||
**Then, add the following configuration** to `~/.codex/config.toml`:
|
||||
```toml
|
||||
[mcp_servers.shadcn]
|
||||
command = "npx"
|
||||
args = ["shadcn@latest", "mcp"]
|
||||
```
|
||||
|
||||
**Restart Codex** and try the following prompts:
|
||||
- Show me all available components in the shadcn registry
|
||||
- Add the button, dialog and card components to my project
|
||||
- Create a contact form using components from the shadcn registry
|
||||
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
---
|
||||
|
||||
## What is MCP?
|
||||
|
||||
[Model Context Protocol (MCP)](https://modelcontextprotocol.io) is an open protocol that enables AI assistants to securely connect to external data sources and tools. With the shadcn MCP server, your AI assistant gains direct access to:
|
||||
|
||||
- **Browse Components** - List all available components, blocks, and templates from any configured registry
|
||||
- **Search Across Registries** - Find specific components by name or functionality across multiple sources
|
||||
- **Install with Natural Language** - Add components using simple conversational prompts like "add a login form"
|
||||
- **Support for Multiple Registries** - Access public registries, private company libraries, and third-party sources
|
||||
|
||||
---
|
||||
|
||||
## How It Works
|
||||
|
||||
The MCP server acts as a bridge between your AI assistant, component registries and the shadcn CLI.
|
||||
|
||||
1. **Registry Connection** - MCP connects to configured registries (shadcn/ui, private registries, third-party sources)
|
||||
2. **Natural Language** - You describe what you need in plain English
|
||||
3. **AI Processing** - The assistant translates your request into registry commands
|
||||
4. **Component Delivery** - Resources are fetched and installed in your project
|
||||
|
||||
---
|
||||
|
||||
## Supported Registries
|
||||
|
||||
The shadcn MCP server works out of the box with any shadcn-compatible registry.
|
||||
|
||||
- **shadcn/ui Registry** - The default registry with all shadcn/ui components
|
||||
- **Third-Party Registries** - Any registry following the shadcn registry specification
|
||||
- **Private Registries** - Your company's internal component libraries
|
||||
- **Namespaced Registries** - Multiple registries configured with `@namespace` syntax
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
You can use any MCP client to interact with the shadcn MCP server. Here are the instructions for the most popular ones.
|
||||
|
||||
### Claude Code
|
||||
|
||||
To use the shadcn MCP server with Claude Code, add the following configuration to your project's `.mcp.json` file:
|
||||
|
||||
```json title=".mcp.json" showLineNumbers
|
||||
{
|
||||
"mcpServers": {
|
||||
"shadcn": {
|
||||
"command": "npx",
|
||||
"args": ["shadcn@latest", "mcp"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
After adding the configuration, restart Claude Code and run `/mcp` to see the shadcn MCP server in the list. If you see `Connected`, you're good to go.
|
||||
|
||||
See the [Claude Code MCP documentation](https://docs.anthropic.com/en/docs/claude-code/mcp) for more details.
|
||||
|
||||
### Cursor
|
||||
|
||||
To configure MCP in Cursor, add the shadcn server to your project's `.cursor/mcp.json` configuration file:
|
||||
|
||||
```json title=".cursor/mcp.json" showLineNumbers
|
||||
{
|
||||
"mcpServers": {
|
||||
"shadcn": {
|
||||
"command": "npx",
|
||||
"args": ["shadcn@latest", "mcp"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
After adding the configuration, enable the shadcn MCP server in Cursor Settings.
|
||||
|
||||
Once enabled, you should see a green dot next to the shadcn server in the MCP server list and a list of available tools.
|
||||
|
||||
See the [Cursor MCP documentation](https://docs.cursor.com/en/context/mcp#using-mcp-json) for more details.
|
||||
|
||||
### VS Code
|
||||
|
||||
To configure MCP in VS Code with GitHub Copilot, add the shadcn server to your project's `.vscode/mcp.json` configuration file:
|
||||
|
||||
```json title=".vscode/mcp.json" showLineNumbers
|
||||
{
|
||||
"mcpServers": {
|
||||
"shadcn": {
|
||||
"command": "npx",
|
||||
"args": ["shadcn@latest", "mcp"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
After adding the configuration, open `.vscode/mcp.json` and click **Start** next to the shadcn server.
|
||||
|
||||
See the [VS Code MCP documentation](https://code.visualstudio.com/docs/copilot/chat/mcp-servers) for more details.
|
||||
|
||||
### Codex
|
||||
|
||||
<Callout>
|
||||
**Note:** The `shadcn` CLI cannot automatically update `~/.codex/config.toml`.
|
||||
You'll need to add the configuration manually.
|
||||
</Callout>
|
||||
|
||||
To configure MCP in Codex, add the shadcn server to `~/.codex/config.toml`:
|
||||
|
||||
```toml title="~/.codex/config.toml" showLineNumbers
|
||||
[mcp_servers.shadcn]
|
||||
command = "npx"
|
||||
args = ["shadcn@latest", "mcp"]
|
||||
```
|
||||
|
||||
After adding the configuration, restart Codex to load the MCP server.
|
||||
|
||||
---
|
||||
|
||||
## Configuring Registries
|
||||
|
||||
The MCP server supports multiple registries through your project's `components.json` configuration. This allows you to access components from various sources including private registries and third-party providers.
|
||||
|
||||
Configure additional registries in your `components.json`:
|
||||
|
||||
```json title="components.json" showLineNumbers
|
||||
{
|
||||
"registries": {
|
||||
"@acme": "https://registry.acme.com/{name}.json",
|
||||
"@internal": {
|
||||
"url": "https://internal.company.com/{name}.json",
|
||||
"headers": {
|
||||
"Authorization": "Bearer ${REGISTRY_TOKEN}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<Callout>
|
||||
**Note:** No configuration is needed to access the standard shadcn/ui
|
||||
registry.
|
||||
</Callout>
|
||||
|
||||
---
|
||||
|
||||
## Authentication
|
||||
|
||||
For private registries requiring authentication, set environment variables in your `.env.local`:
|
||||
|
||||
```bash title=".env.local"
|
||||
REGISTRY_TOKEN=your_token_here
|
||||
API_KEY=your_api_key_here
|
||||
```
|
||||
|
||||
For more details on registry authentication, see the [Authentication documentation](/docs/registry/authentication).
|
||||
|
||||
---
|
||||
|
||||
## Example Prompts
|
||||
|
||||
Once the MCP server is configured, you can use natural language to interact with registries. Try one of the following prompts:
|
||||
|
||||
### Browse & Search
|
||||
|
||||
- Show me all available components in the shadcn registry
|
||||
- Find me a login form from the shadcn registry
|
||||
|
||||
### Install Items
|
||||
|
||||
- Add the button component to my project
|
||||
- Create a login form using shadcn components
|
||||
- Install the Cursor rules from the acme registry
|
||||
|
||||
### Work with Namespaces
|
||||
|
||||
- Show me components from acme registry
|
||||
- Install @internal/auth-form
|
||||
- Build me a landing page using hero, features and testimonials sections from the acme registry
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### MCP Not Responding
|
||||
|
||||
If the MCP server isn't responding to prompts:
|
||||
|
||||
1. **Check Configuration** - Verify the MCP server is properly configured and enabled in your MCP client
|
||||
2. **Restart MCP Client** - Restart your MCP client after configuration changes
|
||||
3. **Verify Installation** - Ensure `shadcn` is installed in your project
|
||||
4. **Check Network** - Confirm you can access the configured registries
|
||||
|
||||
### Registry Access Issues
|
||||
|
||||
If components aren't loading from registries:
|
||||
|
||||
1. **Check components.json** - Verify registry URLs are correct
|
||||
2. **Test Authentication** - Ensure environment variables are set for private registries
|
||||
3. **Verify Registry** - Confirm the registry is online and accessible
|
||||
4. **Check Namespace** - Ensure namespace syntax is correct (`@namespace/component`)
|
||||
|
||||
### Installation Failures
|
||||
|
||||
If components fail to install:
|
||||
|
||||
1. **Check Project Setup** - Ensure you have a valid `components.json` file
|
||||
2. **Verify Paths** - Confirm the target directories exist
|
||||
3. **Check Permissions** - Ensure write permissions for component directories
|
||||
4. **Review Dependencies** - Check that required dependencies are installed
|
||||
|
||||
### No Tools or Prompts
|
||||
|
||||
If you see the `No tools or prompts` message, try the following:
|
||||
|
||||
1. **Clear the npx cache** - Run `npx clear-npx-cache`
|
||||
2. **Re-enable the MCP server** - Try to re-enable the MCP server in your MCP client
|
||||
3. **Check Logs** - In Cursor, you can see the logs under View -> Output and select `MCP: project-*` in the dropdown.
|
||||
|
||||
---
|
||||
|
||||
## Learn More
|
||||
|
||||
- [Registry Documentation](/docs/registry) - Complete guide to shadcn registries
|
||||
- [Namespaces](/docs/registry/namespace) - Configure multiple registry sources
|
||||
- [Authentication](/docs/registry/authentication) - Secure your private registries
|
||||
- [MCP Specification](https://modelcontextprotocol.io) - Learn about Model Context Protocol
|
||||
@@ -14,6 +14,7 @@
|
||||
"blocks",
|
||||
"figma",
|
||||
"changelog",
|
||||
"[llms.txt](/llms.txt)",
|
||||
"legacy"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -170,7 +170,7 @@ To use CSS variables for theming set `tailwind.cssVariables` to `true` in your `
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/registry/new-york-v4/ui",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
},
|
||||
@@ -199,7 +199,7 @@ To use utility classes for theming set `tailwind.cssVariables` to `false` in you
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/registry/new-york-v4/ui",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
|
||||
...
|
||||
|
||||
|
||||
219
apps/v4/content/docs/components/button-group.mdx
Normal file
219
apps/v4/content/docs/components/button-group.mdx
Normal file
@@ -0,0 +1,219 @@
|
||||
---
|
||||
title: Button Group
|
||||
description: A container that groups related buttons together with consistent styling.
|
||||
component: true
|
||||
---
|
||||
|
||||
<ComponentPreview name="button-group-demo" />
|
||||
|
||||
## Installation
|
||||
|
||||
<CodeTabs>
|
||||
|
||||
<TabsList>
|
||||
<TabsTrigger value="cli">CLI</TabsTrigger>
|
||||
<TabsTrigger value="manual">Manual</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="cli">
|
||||
|
||||
```bash
|
||||
npx shadcn@latest add button-group
|
||||
```
|
||||
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="manual">
|
||||
|
||||
<Steps>
|
||||
|
||||
<Step>Install the following dependencies:</Step>
|
||||
|
||||
```bash
|
||||
npm install @radix-ui/react-slot
|
||||
```
|
||||
|
||||
<Step>Copy and paste the following code into your project.</Step>
|
||||
|
||||
<ComponentSource name="button-group" title="components/ui/button-group.tsx" />
|
||||
|
||||
<Step>Update the import paths to match your project setup.</Step>
|
||||
|
||||
</Steps>
|
||||
|
||||
</TabsContent>
|
||||
|
||||
</CodeTabs>
|
||||
|
||||
## Usage
|
||||
|
||||
```tsx
|
||||
import {
|
||||
ButtonGroup,
|
||||
ButtonGroupSeparator,
|
||||
ButtonGroupText,
|
||||
} from "@/components/ui/button-group"
|
||||
```
|
||||
|
||||
```tsx
|
||||
<ButtonGroup>
|
||||
<Button>Button 1</Button>
|
||||
<Button>Button 2</Button>
|
||||
</ButtonGroup>
|
||||
```
|
||||
|
||||
## Accessibility
|
||||
|
||||
- The `ButtonGroup` component has the `role` attribute set to `group`.
|
||||
- Use <Kbd>Tab</Kbd> to navigate between the buttons in the group.
|
||||
- Use `aria-label` or `aria-labelledby` to label the button group.
|
||||
|
||||
```tsx showLineNumbers
|
||||
<ButtonGroup aria-label="Button group">
|
||||
<Button>Button 1</Button>
|
||||
<Button>Button 2</Button>
|
||||
</ButtonGroup>
|
||||
```
|
||||
|
||||
## ButtonGroup vs ToggleGroup
|
||||
|
||||
- Use the `ButtonGroup` component when you want to group buttons that perform an action.
|
||||
- Use the `ToggleGroup` component when you want to group buttons that toggle a state.
|
||||
|
||||
## Examples
|
||||
|
||||
### Orientation
|
||||
|
||||
Set the `orientation` prop to change the button group layout.
|
||||
|
||||
<ComponentPreview name="button-group-orientation" />
|
||||
|
||||
### Size
|
||||
|
||||
Control the size of buttons using the `size` prop on individual buttons.
|
||||
|
||||
<ComponentPreview name="button-group-size" />
|
||||
|
||||
### Nested
|
||||
|
||||
Nest `<ButtonGroup>` components to create button groups with spacing.
|
||||
|
||||
<ComponentPreview name="button-group-nested" />
|
||||
|
||||
### Separator
|
||||
|
||||
The `ButtonGroupSeparator` component visually divides buttons within a group.
|
||||
|
||||
Buttons with variant `outline` do not need a separator since they have a border. For other variants, a separator is recommended to improve the visual hierarchy.
|
||||
|
||||
<ComponentPreview name="button-group-separator" />
|
||||
|
||||
### Split
|
||||
|
||||
Create a split button group by adding two buttons separated by a `ButtonGroupSeparator`.
|
||||
|
||||
<ComponentPreview name="button-group-split" />
|
||||
|
||||
### Input
|
||||
|
||||
Wrap an `Input` component with buttons.
|
||||
|
||||
<ComponentPreview name="button-group-input" />
|
||||
|
||||
### Input Group
|
||||
|
||||
Wrap an `InputGroup` component to create complex input layouts.
|
||||
|
||||
<ComponentPreview name="button-group-input-group" />
|
||||
|
||||
### Dropdown Menu
|
||||
|
||||
Create a split button group with a `DropdownMenu` component.
|
||||
|
||||
<ComponentPreview name="button-group-dropdown" />
|
||||
|
||||
### Select
|
||||
|
||||
Pair with a `Select` component.
|
||||
|
||||
<ComponentPreview name="button-group-select" />
|
||||
|
||||
### Popover
|
||||
|
||||
Use with a `Popover` component.
|
||||
|
||||
<ComponentPreview name="button-group-popover" />
|
||||
|
||||
## API Reference
|
||||
|
||||
### ButtonGroup
|
||||
|
||||
The `ButtonGroup` component is a container that groups related buttons together with consistent styling.
|
||||
|
||||
| Prop | Type | Default |
|
||||
| ------------- | ---------------------------- | -------------- |
|
||||
| `orientation` | `"horizontal" \| "vertical"` | `"horizontal"` |
|
||||
|
||||
```tsx
|
||||
<ButtonGroup>
|
||||
<Button>Button 1</Button>
|
||||
<Button>Button 2</Button>
|
||||
</ButtonGroup>
|
||||
```
|
||||
|
||||
Nest multiple button groups to create complex layouts with spacing. See the [nested](#nested) example for more details.
|
||||
|
||||
```tsx
|
||||
<ButtonGroup>
|
||||
<ButtonGroup />
|
||||
<ButtonGroup />
|
||||
</ButtonGroup>
|
||||
```
|
||||
|
||||
### ButtonGroupSeparator
|
||||
|
||||
The `ButtonGroupSeparator` component visually divides buttons within a group.
|
||||
|
||||
| Prop | Type | Default |
|
||||
| ------------- | ---------------------------- | ------------ |
|
||||
| `orientation` | `"horizontal" \| "vertical"` | `"vertical"` |
|
||||
|
||||
```tsx
|
||||
<ButtonGroup>
|
||||
<Button>Button 1</Button>
|
||||
<ButtonGroupSeparator />
|
||||
<Button>Button 2</Button>
|
||||
</ButtonGroup>
|
||||
```
|
||||
|
||||
### ButtonGroupText
|
||||
|
||||
Use this component to display text within a button group.
|
||||
|
||||
| Prop | Type | Default |
|
||||
| --------- | --------- | ------- |
|
||||
| `asChild` | `boolean` | `false` |
|
||||
|
||||
```tsx
|
||||
<ButtonGroup>
|
||||
<ButtonGroupText>Text</ButtonGroupText>
|
||||
<Button>Button</Button>
|
||||
</ButtonGroup>
|
||||
```
|
||||
|
||||
Use the `asChild` prop to render a custom component as the text, for example a label.
|
||||
|
||||
```tsx showLineNumbers
|
||||
import { ButtonGroupText } from "@/components/ui/button-group"
|
||||
import { Label } from "@/components/ui/label"
|
||||
|
||||
export function ButtonGroupTextDemo() {
|
||||
return (
|
||||
<ButtonGroup>
|
||||
<ButtonGroupText asChild>
|
||||
<Label htmlFor="name">Text</Label>
|
||||
</ButtonGroupText>
|
||||
<Input placeholder="Type something here..." id="name" />
|
||||
</ButtonGroup>
|
||||
)
|
||||
}
|
||||
```
|
||||
@@ -5,7 +5,23 @@ featured: true
|
||||
component: true
|
||||
---
|
||||
|
||||
<ComponentPreview name="button-demo" description="A button" />
|
||||
import { InfoIcon } from "lucide-react"
|
||||
|
||||
<Callout variant="info" icon={<InfoIcon />}>
|
||||
**Updated:** We have updated the button component to add new sizes: `icon-sm` and `icon-lg`. See the
|
||||
[changelog](/docs/components/button#changelog) for more details. Follow the
|
||||
instructions to update your project.
|
||||
|
||||
</Callout>
|
||||
|
||||
<ComponentPreview name="button-demo" description="A button" className="mb-4" />
|
||||
|
||||
```tsx showLineNumbers
|
||||
<Button variant="outline">Button</Button>
|
||||
<Button variant="outline" size="icon" aria-label="Submit">
|
||||
<ArrowUpIcon />
|
||||
</Button>
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -55,12 +71,214 @@ import { Button } from "@/components/ui/button"
|
||||
<Button variant="outline">Button</Button>
|
||||
```
|
||||
|
||||
## Link
|
||||
## 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
|
||||
|
||||
### Size
|
||||
|
||||
<ComponentPreview name="button-size" className="mb-4" />
|
||||
|
||||
```tsx
|
||||
// Small
|
||||
<Button size="sm" variant="outline">Small</Button>
|
||||
<Button size="icon-sm" aria-label="Submit" variant="outline">
|
||||
<ArrowUpRightIcon />
|
||||
</Button>
|
||||
|
||||
// Medium
|
||||
<Button variant="outline">Default</Button>
|
||||
<Button size="icon" aria-label="Submit" variant="outline">
|
||||
<ArrowUpRightIcon />
|
||||
</Button>
|
||||
|
||||
// Large
|
||||
<Button size="lg" variant="outline">Large</Button>
|
||||
<Button size="icon-lg" aria-label="Submit" variant="outline">
|
||||
<ArrowUpRightIcon />
|
||||
</Button>
|
||||
```
|
||||
|
||||
### Default
|
||||
|
||||
<ComponentPreview
|
||||
name="button-default"
|
||||
description="A primary button"
|
||||
className="mb-4"
|
||||
/>
|
||||
|
||||
```tsx
|
||||
<Button>Button</Button>
|
||||
```
|
||||
|
||||
### Outline
|
||||
|
||||
<ComponentPreview
|
||||
name="button-outline"
|
||||
description="A button using the outline variant."
|
||||
className="mb-4"
|
||||
/>
|
||||
|
||||
```tsx
|
||||
<Button variant="outline">Outline</Button>
|
||||
```
|
||||
|
||||
### Secondary
|
||||
|
||||
<ComponentPreview
|
||||
name="button-secondary"
|
||||
description="A secondary button"
|
||||
className="mb-4"
|
||||
/>
|
||||
|
||||
```tsx
|
||||
<Button variant="secondary">Secondary</Button>
|
||||
```
|
||||
|
||||
### Ghost
|
||||
|
||||
<ComponentPreview
|
||||
name="button-ghost"
|
||||
description="A button using the ghost variant"
|
||||
className="mb-4"
|
||||
/>
|
||||
|
||||
```tsx
|
||||
<Button variant="ghost">Ghost</Button>
|
||||
```
|
||||
|
||||
### Destructive
|
||||
|
||||
<ComponentPreview
|
||||
name="button-destructive"
|
||||
description="A destructive button"
|
||||
className="mb-4"
|
||||
/>
|
||||
|
||||
```tsx
|
||||
<Button variant="destructive">Destructive</Button>
|
||||
```
|
||||
|
||||
### Link
|
||||
|
||||
<ComponentPreview
|
||||
name="button-link"
|
||||
description="A button using the link variant."
|
||||
className="mb-4"
|
||||
/>
|
||||
|
||||
```tsx
|
||||
<Button variant="link">Link</Button>
|
||||
```
|
||||
|
||||
### Icon
|
||||
|
||||
<ComponentPreview
|
||||
name="button-icon"
|
||||
description="An icon button"
|
||||
className="mb-4"
|
||||
/>
|
||||
|
||||
```tsx showLineNumbers
|
||||
<Button variant="outline" size="icon" aria-label="Submit">
|
||||
<CircleFadingArrowUpIcon />
|
||||
</Button>
|
||||
```
|
||||
|
||||
### With Icon
|
||||
|
||||
The spacing between the icon and the text is automatically adjusted
|
||||
based on the size of the button. You do not need any margin on the icon.
|
||||
|
||||
<ComponentPreview
|
||||
name="button-with-icon"
|
||||
description="A button with an icon"
|
||||
className="mb-4"
|
||||
/>
|
||||
|
||||
```tsx
|
||||
<Button variant="outline" size="sm">
|
||||
<IconGitBranch /> New Branch
|
||||
</Button>
|
||||
```
|
||||
|
||||
### Rounded
|
||||
|
||||
Use the `rounded-full` class to make the button rounded.
|
||||
|
||||
<ComponentPreview name="button-rounded" className="mb-4" />
|
||||
|
||||
```tsx
|
||||
<Button variant="outline" size="icon" className="rounded-full">
|
||||
<ArrowUpRightIcon />
|
||||
</Button>
|
||||
```
|
||||
|
||||
### Spinner
|
||||
|
||||
<ComponentPreview
|
||||
name="button-loading"
|
||||
description="A button with a loading state."
|
||||
className="mb-4"
|
||||
/>
|
||||
|
||||
```tsx showLineNumbers
|
||||
<Button size="sm" variant="outline" disabled>
|
||||
<Spinner />
|
||||
Submit
|
||||
</Button>
|
||||
```
|
||||
|
||||
### Button Group
|
||||
|
||||
To create a button group, use the `ButtonGroup` component. See the [Button Group](/docs/components/button-group) documentation for more details.
|
||||
|
||||
<ComponentPreview name="button-group-demo" className="mb-4" />
|
||||
|
||||
```tsx showLineNumbers
|
||||
<ButtonGroup>
|
||||
<ButtonGroup>
|
||||
<Button variant="outline" size="icon" aria-label="Go Back">
|
||||
<ArrowLeftIcon />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup>
|
||||
<Button variant="outline">Archive</Button>
|
||||
<Button variant="outline">Report</Button>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup>
|
||||
<Button variant="outline">Snooze</Button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" size="icon" aria-label="More Options">
|
||||
<MoreHorizontalIcon />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent />
|
||||
</DropdownMenu>
|
||||
</ButtonGroup>
|
||||
</ButtonGroup>
|
||||
```
|
||||
|
||||
### Link
|
||||
|
||||
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"
|
||||
|
||||
@@ -73,55 +291,33 @@ export function LinkAsButton() {
|
||||
}
|
||||
```
|
||||
|
||||
## Examples
|
||||
## API Reference
|
||||
|
||||
### Default
|
||||
### Button
|
||||
|
||||
<ComponentPreview name="button-demo" description="A primary button" />
|
||||
The `Button` component is a wrapper around the `button` element that adds a variety of styles and functionality.
|
||||
|
||||
### Secondary
|
||||
| Prop | Type | Default |
|
||||
| --------- | ----------------------------------------------------------------------------- | ----------- |
|
||||
| `variant` | `"default" \| "outline" \| "ghost" \| "destructive" \| "secondary" \| "link"` | `"default"` |
|
||||
| `size` | `"default" \| "sm" \| "lg" \| "icon" \| "icon-sm" \| "icon-lg"` | `"default"` |
|
||||
| `asChild` | `boolean` | `false` |
|
||||
|
||||
<ComponentPreview name="button-secondary" description="A secondary button" />
|
||||
## Changelog
|
||||
|
||||
### Destructive
|
||||
### 2025-09-24 New sizes
|
||||
|
||||
<ComponentPreview
|
||||
name="button-destructive"
|
||||
description="A destructive button"
|
||||
/>
|
||||
We have added two new sizes to the button component: `icon-sm` and `icon-lg`. These sizes are used to create icon buttons. To add them, edit `button.tsx` and add the following code under `size` in `buttonVariants`:
|
||||
|
||||
### Outline
|
||||
|
||||
<ComponentPreview
|
||||
name="button-outline"
|
||||
description="A button using the outline variant."
|
||||
/>
|
||||
|
||||
### Ghost
|
||||
|
||||
<ComponentPreview
|
||||
name="button-ghost"
|
||||
description="A button using the ghost variant"
|
||||
/>
|
||||
|
||||
### Link
|
||||
|
||||
<ComponentPreview
|
||||
name="button-link"
|
||||
description="A button using the link variant."
|
||||
/>
|
||||
|
||||
### Icon
|
||||
|
||||
<ComponentPreview name="button-icon" description="An icon button" />
|
||||
|
||||
### With Icon
|
||||
|
||||
<ComponentPreview name="button-with-icon" description="A button with an icon" />
|
||||
|
||||
### Loading
|
||||
|
||||
<ComponentPreview
|
||||
name="button-loading"
|
||||
description="A button with a loading state."
|
||||
/>
|
||||
```tsx showLineNumbers title="components/ui/button.tsx"
|
||||
const buttonVariants = cva("...", {
|
||||
variants: {
|
||||
size: {
|
||||
// ...
|
||||
"icon-sm": "size-8",
|
||||
"icon-lg": "size-10",
|
||||
// ...
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
@@ -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" />
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user