Compare commits

..

2 Commits

Author SHA1 Message Date
shadcn
319f7f9419 chore: build external 2025-07-14 20:06:47 +04:00
shadcn
e6bc16461a feat: poc 2025-07-14 20:02:02 +04:00
891 changed files with 11001 additions and 117969 deletions

View File

@@ -7,5 +7,5 @@
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": ["www", "v4", "tests"]
"ignore": ["www", "v4"]
}

View File

@@ -1,5 +0,0 @@
---
"shadcn": patch
---
Fix support for universal registry items that only have dependencies without files

View File

@@ -1,5 +0,0 @@
---
"shadcn": patch
---
add support for color as var

View File

@@ -1,5 +0,0 @@
---
"shadcn": patch
---
fix adding registry item with CSS at-property

View File

@@ -1,9 +0,0 @@
{
"permissions": {
"allow": [
"Bash(npm test:*)",
"Bash(npm run typecheck:*)"
],
"deny": []
}
}

View File

@@ -1,75 +0,0 @@
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');
}
}

View File

@@ -41,7 +41,7 @@ jobs:
- name: Create Version PR or Publish to NPM
id: changesets
uses: changesets/action@v1
uses: changesets/action@v1.4.1
with:
commit: "chore(release): version packages"
title: "chore(release): version packages"

View File

@@ -8,9 +8,6 @@ jobs:
test:
runs-on: ubuntu-latest
name: pnpm test
env:
NEXT_PUBLIC_APP_URL: http://localhost:4000
NEXT_PUBLIC_V0_URL: https://v0.dev
steps:
- uses: actions/checkout@v3
with:
@@ -42,7 +39,4 @@ jobs:
- name: Install dependencies
run: pnpm install
- name: Build packages
run: pnpm build --filter=shadcn
- run: pnpm test

View File

@@ -1,54 +0,0 @@
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
View File

@@ -3,13 +3,15 @@
{ "pattern": "apps/*/" },
{ "pattern": "packages/*/" }
],
"tailwindCSS.classFunctions": ["cva", "cn"],
"tailwindCSS.experimental.classRegex": [
["cva\\(((?:[^()]|\\([^()]*\\))*)\\)", "[\"'`]?([^\"'`]+)[\"'`]?"],
["cn\\(((?:[^()]|\\([^()]*\\))*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"]
// "cva\\(([^)]*)\\)",
// "[\"'`]([^\"'`]*).*?[\"'`]"
],
"vitest.debugExclude": [
"<node_internals>/**",
"**/node_modules/**",
"**/fixtures/**"
],
"files.exclude": {
"apps/www": true
}
]
}

3
apps/v4/.gitignore vendored
View File

@@ -46,3 +46,6 @@ next-env.d.ts
.contentlayer
.content-collections
.source
# Generated data
.data/

View File

@@ -1,168 +0,0 @@
"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>
)
}

View File

@@ -1,120 +0,0 @@
"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>
)
}

View File

@@ -1,58 +0,0 @@
"use client"
import * as React from "react"
import { AudioLinesIcon, PlusIcon } from "lucide-react"
import { Button } from "@/registry/new-york-v4/ui/button"
import { ButtonGroup } from "@/registry/new-york-v4/ui/button-group"
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
} from "@/registry/new-york-v4/ui/input-group"
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/registry/new-york-v4/ui/tooltip"
export function ButtonGroupInputGroup() {
const [voiceEnabled, setVoiceEnabled] = React.useState(false)
return (
<ButtonGroup className="[--radius:9999rem]">
<ButtonGroup>
<Button variant="outline" size="icon" 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>
)
}

View File

@@ -1,32 +0,0 @@
"use client"
import { ArrowLeftIcon, ArrowRightIcon } from "lucide-react"
import { Button } from "@/registry/new-york-v4/ui/button"
import { ButtonGroup } from "@/registry/new-york-v4/ui/button-group"
export function ButtonGroupNested() {
return (
<ButtonGroup>
<ButtonGroup>
<Button variant="outline" size="sm">
1
</Button>
<Button variant="outline" size="sm">
2
</Button>
<Button variant="outline" size="sm">
3
</Button>
</ButtonGroup>
<ButtonGroup>
<Button variant="outline" size="icon-sm" aria-label="Previous">
<ArrowLeftIcon />
</Button>
<Button variant="outline" size="icon-sm" aria-label="Next">
<ArrowRightIcon />
</Button>
</ButtonGroup>
</ButtonGroup>
)
}

View File

@@ -1,45 +0,0 @@
import { BotIcon, ChevronDownIcon } from "lucide-react"
import { Button } from "@/registry/new-york-v4/ui/button"
import { ButtonGroup } from "@/registry/new-york-v4/ui/button-group"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/registry/new-york-v4/ui/popover"
import { Separator } from "@/registry/new-york-v4/ui/separator"
import { Textarea } from "@/registry/new-york-v4/ui/textarea"
export function ButtonGroupPopover() {
return (
<ButtonGroup>
<Button variant="outline" size="sm">
<BotIcon /> Copilot
</Button>
<Popover>
<PopoverTrigger asChild>
<Button variant="outline" 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>
)
}

View File

@@ -1,57 +0,0 @@
import { PlusIcon } from "lucide-react"
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@/registry/new-york-v4/ui/avatar"
import { Button } from "@/registry/new-york-v4/ui/button"
import {
Empty,
EmptyContent,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle,
} from "@/registry/new-york-v4/ui/empty"
export function EmptyAvatarGroup() {
return (
<Empty 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>
)
}

View File

@@ -1,43 +0,0 @@
import { SearchIcon } from "lucide-react"
import {
Empty,
EmptyContent,
EmptyDescription,
EmptyHeader,
EmptyTitle,
} from "@/registry/new-york-v4/ui/empty"
import {
InputGroup,
InputGroupAddon,
InputGroupInput,
} from "@/registry/new-york-v4/ui/input-group"
import { Kbd } from "@/registry/new-york-v4/ui/kbd"
export function EmptyInputGroup() {
return (
<Empty>
<EmptyHeader>
<EmptyTitle>404 - Not Found</EmptyTitle>
<EmptyDescription>
The page you&apos;re looking for doesn&apos;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>
)
}

View File

@@ -1,15 +0,0 @@
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>
)
}

View File

@@ -1,62 +0,0 @@
import {
Field,
FieldContent,
FieldDescription,
FieldGroup,
FieldLabel,
FieldSet,
FieldTitle,
} from "@/registry/new-york-v4/ui/field"
import {
RadioGroup,
RadioGroupItem,
} from "@/registry/new-york-v4/ui/radio-group"
export function FieldChoiceCard() {
return (
<div className="w-full max-w-md">
<FieldGroup>
<FieldSet>
<FieldLabel htmlFor="compute-environment-p8w">
Compute Environment
</FieldLabel>
<FieldDescription>
Select the compute environment for your cluster.
</FieldDescription>
<RadioGroup defaultValue="kubernetes">
<FieldLabel htmlFor="kubernetes-r2h">
<Field orientation="horizontal">
<RadioGroupItem
value="kubernetes"
id="kubernetes-r2h"
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>
)
}

View File

@@ -1,153 +0,0 @@
import { Button } from "@/registry/new-york-v4/ui/button"
import { Checkbox } from "@/registry/new-york-v4/ui/checkbox"
import {
Field,
FieldDescription,
FieldGroup,
FieldLabel,
FieldLegend,
FieldSeparator,
FieldSet,
} from "@/registry/new-york-v4/ui/field"
import { Input } from "@/registry/new-york-v4/ui/input"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/registry/new-york-v4/ui/select"
import { Textarea } from "@/registry/new-york-v4/ui/textarea"
export function FieldDemo() {
return (
<div className="w-full max-w-md 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>
)
}

View File

@@ -1,72 +0,0 @@
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>
)
}

View File

@@ -1,35 +0,0 @@
"use client"
import { useState } from "react"
import {
Field,
FieldDescription,
FieldTitle,
} from "@/registry/new-york-v4/ui/field"
import { Slider } from "@/registry/new-york-v4/ui/slider"
export function FieldSlider() {
const [value, setValue] = useState([200, 800])
return (
<div className="w-full max-w-md">
<Field>
<FieldTitle>Price Range</FieldTitle>
<FieldDescription>
Set your budget range ($
<span className="font-medium tabular-nums">{value[0]}</span> -{" "}
<span className="font-medium tabular-nums">{value[1]}</span>).
</FieldDescription>
<Slider
value={value}
onValueChange={setValue}
max={1000}
min={0}
step={10}
className="mt-2 w-full"
aria-label="Price Range"
/>
</Field>
</div>
)
}

View File

@@ -1,52 +0,0 @@
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>
)
}

View File

@@ -1,68 +0,0 @@
"use client"
import * as React from "react"
import { IconInfoCircle, IconStar } from "@tabler/icons-react"
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
} from "@/registry/new-york-v4/ui/input-group"
import { 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>
)
}

View File

@@ -1,102 +0,0 @@
import { IconCheck, IconInfoCircle, IconPlus } from "@tabler/icons-react"
import { ArrowUpIcon, Search } from "lucide-react"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/registry/new-york-v4/ui/dropdown-menu"
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
InputGroupText,
InputGroupTextarea,
} from "@/registry/new-york-v4/ui/input-group"
import { Separator } from "@/registry/new-york-v4/ui/separator"
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/registry/new-york-v4/ui/tooltip"
export function InputGroupDemo() {
return (
<div className="grid w-full max-w-sm gap-6">
<InputGroup>
<InputGroupInput placeholder="Search..." />
<InputGroupAddon>
<Search />
</InputGroupAddon>
<InputGroupAddon align="inline-end">12 results</InputGroupAddon>
</InputGroup>
<InputGroup>
<InputGroupInput placeholder="example.com" className="!pl-1" />
<InputGroupAddon>
<InputGroupText>https://</InputGroupText>
</InputGroupAddon>
<InputGroupAddon align="inline-end">
<Tooltip>
<TooltipTrigger asChild>
<InputGroupButton
className="rounded-full"
size="icon-xs"
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>
)
}

View File

@@ -1,46 +0,0 @@
import {
IconBrandJavascript,
IconCopy,
IconCornerDownLeft,
IconRefresh,
} from "@tabler/icons-react"
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupText,
InputGroupTextarea,
} from "@/registry/new-york-v4/ui/input-group"
export function InputGroupTextareaExample() {
return (
<div className="grid w-full max-w-md gap-4">
<InputGroup>
<InputGroupTextarea
id="textarea-code-32"
placeholder="console.log('Hello, world!');"
className="min-h-[180px]"
/>
<InputGroupAddon align="block-end" className="border-t">
<InputGroupText>Line 1, Column 1</InputGroupText>
<InputGroupButton size="sm" className="ml-auto" variant="default">
Run <IconCornerDownLeft />
</InputGroupButton>
</InputGroupAddon>
<InputGroupAddon align="block-start" className="border-b">
<InputGroupText className="font-mono font-medium">
<IconBrandJavascript />
script.js
</InputGroupText>
<InputGroupButton className="ml-auto">
<IconRefresh />
</InputGroupButton>
<InputGroupButton variant="ghost">
<IconCopy />
</InputGroupButton>
</InputGroupAddon>
</InputGroup>
</div>
)
}

View File

@@ -1,78 +0,0 @@
import { Plus } from "lucide-react"
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@/registry/new-york-v4/ui/avatar"
import { Button } from "@/registry/new-york-v4/ui/button"
import {
Item,
ItemActions,
ItemContent,
ItemDescription,
ItemMedia,
ItemTitle,
} from "@/registry/new-york-v4/ui/item"
export function ItemAvatar() {
return (
<div className="flex w-full max-w-lg flex-col gap-6">
<Item variant="outline" className="hidden">
<ItemMedia>
<Avatar className="size-10">
<AvatarImage src="https://github.com/maxleiter.png" />
<AvatarFallback>LR</AvatarFallback>
</Avatar>
</ItemMedia>
<ItemContent>
<ItemTitle>Max Leiter</ItemTitle>
<ItemDescription>Last seen 5 months ago</ItemDescription>
</ItemContent>
<ItemActions>
<Button
size="icon-sm"
variant="outline"
className="rounded-full"
aria-label="Invite"
>
<Plus />
</Button>
</ItemActions>
</Item>
<Item variant="outline">
<ItemMedia>
<div className="*:data-[slot=avatar]:ring-background flex -space-x-2 *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:grayscale">
<Avatar className="hidden sm:flex">
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
<AvatarFallback>CN</AvatarFallback>
</Avatar>
<Avatar className="hidden sm:flex">
<AvatarImage
src="https://github.com/maxleiter.png"
alt="@maxleiter"
/>
<AvatarFallback>LR</AvatarFallback>
</Avatar>
<Avatar>
<AvatarImage
src="https://github.com/evilrabbit.png"
alt="@evilrabbit"
/>
<AvatarFallback>ER</AvatarFallback>
</Avatar>
</div>
</ItemMedia>
<ItemContent>
<ItemTitle>No Team Members</ItemTitle>
<ItemDescription>Invite your team to collaborate.</ItemDescription>
</ItemContent>
<ItemActions>
<Button size="sm" variant="outline">
Invite
</Button>
</ItemActions>
</Item>
</div>
)
}

View File

@@ -1,42 +0,0 @@
import { BadgeCheckIcon, ChevronRightIcon } from "lucide-react"
import { Button } from "@/registry/new-york-v4/ui/button"
import {
Item,
ItemActions,
ItemContent,
ItemDescription,
ItemMedia,
ItemTitle,
} from "@/registry/new-york-v4/ui/item"
export function ItemDemo() {
return (
<div className="flex w-full max-w-md flex-col gap-6">
<Item variant="outline">
<ItemContent>
<ItemTitle>Two-factor authentication</ItemTitle>
<ItemDescription className="text-pretty 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>
)
}

View File

@@ -1,456 +0,0 @@
"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&apos;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>
)
}

View File

@@ -1,21 +0,0 @@
import { Badge } from "@/registry/new-york-v4/ui/badge"
import { Spinner } from "@/registry/new-york-v4/ui/spinner"
export function SpinnerBadge() {
return (
<div className="flex items-center gap-2 [--radius:1.2rem]">
<Badge>
<Spinner />
Syncing
</Badge>
<Badge variant="secondary">
<Spinner />
Updating
</Badge>
<Badge variant="outline">
<Spinner />
Loading
</Badge>
</div>
)
}

View File

@@ -1,31 +0,0 @@
import { Button } from "@/registry/new-york-v4/ui/button"
import {
Empty,
EmptyContent,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle,
} from "@/registry/new-york-v4/ui/empty"
import { Spinner } from "@/registry/new-york-v4/ui/spinner"
export function SpinnerEmpty() {
return (
<Empty className="w-full 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>
)
}

View File

@@ -3,6 +3,7 @@ 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,
@@ -14,11 +15,9 @@ 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 title = "Build your Component Library"
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."
"A set of beautifully-designed, accessible components and a code distribution platform. Works with your favorite frameworks. Open Source. Open Code."
export const dynamic = "force-static"
export const revalidate = false
@@ -52,14 +51,14 @@ export default function IndexPage() {
<div className="flex flex-1 flex-col">
<PageHeader>
<Announcement />
<PageHeaderHeading className="max-w-4xl">{title}</PageHeaderHeading>
<PageHeaderHeading>{title}</PageHeaderHeading>
<PageHeaderDescription>{description}</PageHeaderDescription>
<PageActions>
<Button asChild size="sm">
<Link href="/docs/installation">Get Started</Link>
</Button>
<Button asChild size="sm" variant="ghost">
<Link href="/docs/components">View Components</Link>
<Link href="/blocks">Browse Blocks</Link>
</Button>
</PageActions>
</PageHeader>
@@ -88,7 +87,7 @@ export default function IndexPage() {
/>
</section>
<section className="theme-container hidden md:block">
<RootComponents />
<CardsDemo />
</section>
</div>
</div>

View File

@@ -10,7 +10,6 @@ import { findNeighbour } from "fumadocs-core/server"
import { source } from "@/lib/source"
import { absoluteUrl } from "@/lib/utils"
import { DocsCopyPage } from "@/components/docs-copy-page"
import { DocsTableOfContents } from "@/components/docs-toc"
import { OpenInV0Cta } from "@/components/open-in-v0-cta"
import { Badge } from "@/registry/new-york-v4/ui/badge"
@@ -103,17 +102,12 @@ export default async function Page(props: {
<h1 className="scroll-m-20 text-4xl font-semibold tracking-tight sm:text-3xl xl:text-4xl">
{doc.title}
</h1>
<div className="docs-nav bg-background/80 border-border/50 fixed inset-x-0 bottom-0 isolate z-50 flex items-center gap-2 border-t px-6 py-4 backdrop-blur-sm sm:static sm:z-0 sm:border-t-0 sm:bg-transparent sm:px-0 sm:pt-1.5 sm:backdrop-blur-none">
<DocsCopyPage
// @ts-expect-error - revisit fumadocs types.
page={doc.content}
url={absoluteUrl(page.url)}
/>
<div className="flex items-center gap-2 pt-1.5">
{neighbours.previous && (
<Button
variant="secondary"
size="icon"
className="extend-touch-target ml-auto size-8 shadow-none md:size-7"
className="extend-touch-target size-8 shadow-none md:size-7"
asChild
>
<Link href={neighbours.previous.url}>
@@ -144,19 +138,19 @@ export default async function Page(props: {
)}
</div>
{links ? (
<div className="flex items-center gap-2 pt-4">
<div className="flex items-center space-x-2 pt-4">
{links?.doc && (
<Badge asChild variant="secondary" className="rounded-full">
<a href={links.doc} target="_blank" rel="noreferrer">
<Badge asChild variant="secondary">
<Link href={links.doc} target="_blank" rel="noreferrer">
Docs <IconArrowUpRight />
</a>
</Link>
</Badge>
)}
{links?.api && (
<Badge asChild variant="secondary" className="rounded-full">
<a href={links.api} target="_blank" rel="noreferrer">
<Badge asChild variant="secondary">
<Link href={links.api} target="_blank" rel="noreferrer">
API Reference <IconArrowUpRight />
</a>
</Link>
</Badge>
)}
</div>
@@ -166,7 +160,7 @@ export default async function Page(props: {
<MDX components={mdxComponents} />
</div>
</div>
<div className="mx-auto hidden h-16 w-full max-w-2xl items-center gap-2 px-4 sm:flex md:px-0">
<div className="mx-auto flex h-16 w-full max-w-2xl items-center gap-2 px-4 md:px-0">
{neighbours.previous && (
<Button
variant="secondary"
@@ -193,7 +187,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(--footer-height)+2rem)] 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(--header-height)-var(--footer-height))] 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 ? (

View File

@@ -5,14 +5,8 @@ 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 { Spinner } from "@/registry/new-york-v4/ui/spinner"
import { Label } from "@/registry/new-york-v4/ui/label"
export function UserAuthForm({
className,
@@ -32,11 +26,11 @@ export function UserAuthForm({
return (
<div className={cn("grid gap-6", className)} {...props}>
<form onSubmit={onSubmit}>
<FieldGroup>
<Field>
<FieldLabel className="sr-only" htmlFor="email">
<div className="grid gap-2">
<div className="grid gap-1">
<Label className="sr-only" htmlFor="email">
Email
</FieldLabel>
</Label>
<Input
id="email"
placeholder="name@example.com"
@@ -46,18 +40,31 @@ export function UserAuthForm({
autoCorrect="off"
disabled={isLoading}
/>
</Field>
<Field>
<Button disabled={isLoading}>
{isLoading && <Spinner />}
Sign In with Email
</Button>
</Field>
</FieldGroup>
</div>
<Button disabled={isLoading}>
{isLoading && (
<Icons.spinner className="mr-2 h-4 w-4 animate-spin" />
)}
Sign In with Email
</Button>
</div>
</form>
<FieldSeparator>Or continue with</FieldSeparator>
<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>
<Button variant="outline" type="button" disabled={isLoading}>
{isLoading ? <Spinner /> : <Icons.gitHub className="mr-2 h-4 w-4" />}{" "}
{isLoading ? (
<Icons.spinner className="mr-2 h-4 w-4 animate-spin" />
) : (
<Icons.gitHub className="mr-2 h-4 w-4" />
)}{" "}
GitHub
</Button>
</div>

View File

@@ -4,7 +4,6 @@ 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 = {
@@ -79,11 +78,23 @@ export default function AuthenticationPage() {
</p>
</div>
<UserAuthForm />
<FieldDescription className="px-6 text-center">
<p className="text-muted-foreground px-8 text-center text-sm">
By clicking continue, you agree to our{" "}
<Link href="/terms">Terms of Service</Link> and{" "}
<Link href="/privacy">Privacy Policy</Link>.
</FieldDescription>
<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>
</div>
</div>
</div>

View File

@@ -142,7 +142,13 @@ const chartConfig = {
export function ChartAreaInteractive() {
const isMobile = useIsMobile()
const [timeRange, setTimeRange] = React.useState("7d")
const [timeRange, setTimeRange] = React.useState("90d")
React.useEffect(() => {
if (isMobile) {
setTimeRange("7d")
}
}, [isMobile])
const filteredData = chartData.filter((item) => {
const date = new Date(item.date)

View File

@@ -75,7 +75,7 @@ export function DataTable<TData, TValue>({
return (
<div className="flex flex-col gap-4">
<DataTableToolbar table={table} />
<div className="overflow-hidden rounded-md border">
<div className="rounded-md border">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (

View File

@@ -1,32 +0,0 @@
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
export async function GET(
_req: NextRequest,
{ params }: { params: Promise<{ slug: string[] }> }
) {
const slug = (await params).slug
const page = source.getPage(slug)
if (!page) {
notFound()
}
// @ts-expect-error - revisit fumadocs types.
const processedContent = processMdxForLLMs(page.data.content)
return new NextResponse(processedContent, {
headers: {
"Content-Type": "text/markdown; charset=utf-8",
},
})
}
export function generateStaticParams() {
return source.generateParams()
}

View File

@@ -1,168 +0,0 @@
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>
)
}

View File

@@ -1,463 +0,0 @@
"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&apos;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&apos;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>
)
}

View File

@@ -1,137 +0,0 @@
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>
)
}

View File

@@ -1,464 +0,0 @@
"use client"
import { useMemo, useState } from "react"
import {
IconApps,
IconArrowUp,
IconAt,
IconBook,
IconBrandAbstract,
IconBrandOpenai,
IconBrandZeit,
IconCircleDashedPlus,
IconPaperclip,
IconPlus,
IconWorld,
IconX,
} from "@tabler/icons-react"
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@/registry/new-york-v4/ui/avatar"
import { Badge } from "@/registry/new-york-v4/ui/badge"
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/registry/new-york-v4/ui/command"
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from "@/registry/new-york-v4/ui/dropdown-menu"
import { Field, FieldLabel } from "@/registry/new-york-v4/ui/field"
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupTextarea,
} from "@/registry/new-york-v4/ui/input-group"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/registry/new-york-v4/ui/popover"
import { Switch } from "@/registry/new-york-v4/ui/switch"
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/registry/new-york-v4/ui/tooltip"
const SAMPLE_DATA = {
mentionable: [
{
type: "page",
title: "Meeting Notes",
image: "📝",
},
{
type: "page",
title: "Project Dashboard",
image: "📊",
},
{
type: "page",
title: "Ideas & Brainstorming",
image: "💡",
},
{
type: "page",
title: "Calendar & Events",
image: "📅",
},
{
type: "page",
title: "Documentation",
image: "📚",
},
{
type: "page",
title: "Goals & Objectives",
image: "🎯",
},
{
type: "page",
title: "Budget Planning",
image: "💰",
},
{
type: "page",
title: "Team Directory",
image: "👥",
},
{
type: "page",
title: "Technical Specs",
image: "🔧",
},
{
type: "page",
title: "Analytics Report",
image: "📈",
},
{
type: "user",
title: "shadcn",
image: "https://github.com/shadcn.png",
workspace: "Workspace",
},
{
type: "user",
title: "maxleiter",
image: "https://github.com/maxleiter.png",
workspace: "Cursor",
},
{
type: "user",
title: "evilrabbit",
image: "https://github.com/evilrabbit.png",
workspace: "Vercel",
},
],
models: [
{
name: "Auto",
icon: IconBrandZeit,
},
{
name: "Claude Sonnet 4",
icon: IconBrandAbstract,
badge: "Beta",
},
{
name: "GPT-5",
icon: IconBrandOpenai,
badge: "Beta",
},
],
}
function MentionableIcon({
item,
}: {
item: (typeof SAMPLE_DATA.mentionable)[0]
}) {
return item.type === "page" ? (
<span className="flex size-4 items-center justify-center">
{item.image}
</span>
) : (
<Avatar className="size-4">
<AvatarImage src={item.image} />
<AvatarFallback>{item.title[0]}</AvatarFallback>
</Avatar>
)
}
export function NotionPromptForm() {
const [mentions, setMentions] = useState<string[]>([])
const [mentionPopoverOpen, setMentionPopoverOpen] = useState(false)
const [modelPopoverOpen, setModelPopoverOpen] = useState(false)
const [selectedModel, setSelectedModel] = useState<
(typeof SAMPLE_DATA.models)[0]
>(SAMPLE_DATA.models[0])
const [scopeMenuOpen, setScopeMenuOpen] = useState(false)
const grouped = useMemo(() => {
return SAMPLE_DATA.mentionable.reduce(
(acc, item) => {
const isAvailable = !mentions.includes(item.title)
if (isAvailable) {
if (!acc[item.type]) {
acc[item.type] = []
}
acc[item.type].push(item)
}
return acc
},
{} as Record<string, typeof SAMPLE_DATA.mentionable>
)
}, [mentions])
const hasMentions = mentions.length > 0
return (
<form className="[--radius:1.2rem]">
<Field>
<FieldLabel htmlFor="notion-prompt" className="sr-only">
Prompt
</FieldLabel>
<InputGroup 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&apos;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>
)
}

View File

@@ -1,31 +0,0 @@
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>
)
}

View File

@@ -1,193 +0,0 @@
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>
)
}

View File

@@ -1,121 +0,0 @@
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>
)
}

View File

@@ -1,55 +0,0 @@
"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,
}
}

View File

@@ -1,391 +0,0 @@
"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&apos;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>
</>
)
}

View File

@@ -1,9 +0,0 @@
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>
)
}

View File

@@ -1,490 +0,0 @@
"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&apos;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>
</>
)
}

View File

@@ -1,9 +0,0 @@
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>
)
}

View File

@@ -1,97 +0,0 @@
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"
),
})

View File

@@ -1,532 +0,0 @@
/* 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&apos;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>
</>
)
}

View File

@@ -1,9 +0,0 @@
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>
)
}

View File

@@ -1,54 +0,0 @@
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>
)
}

View File

@@ -1,429 +0,0 @@
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

View File

@@ -1,43 +0,0 @@
"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>
)
}

View File

@@ -2,7 +2,6 @@
import * as React from "react"
import Link from "next/link"
import { usePathname } from "next/navigation"
import {
AudioWaveform,
BookOpen,
@@ -10,7 +9,7 @@ import {
ChevronRightIcon,
Command,
GalleryVerticalEnd,
SearchIcon,
Search,
Settings2,
SquareTerminal,
} from "lucide-react"
@@ -23,11 +22,6 @@ 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,
@@ -37,6 +31,7 @@ import {
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarInput,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
@@ -45,7 +40,6 @@ import {
SidebarMenuSubItem,
SidebarRail,
} from "@/registry/new-york-v4/ui/sidebar"
import { componentRegistry } from "@/app/(internal)/sink/component-registry"
// This is sample data.
const data = {
@@ -169,9 +163,8 @@ const data = {
}
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
const pathname = usePathname()
return (
<Sidebar side="left" collapsible="icon" {...props}>
<Sidebar collapsible="icon" {...props}>
<SidebarHeader>
<TeamSwitcher teams={data.teams} />
<SidebarGroup className="py-0 group-data-[collapsible=icon]:hidden">
@@ -180,17 +173,12 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
<Label htmlFor="search" className="sr-only">
Search
</Label>
<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>
<SidebarInput
id="search"
placeholder="Search the docs..."
className="pl-8"
/>
<Search className="pointer-events-none absolute top-1/2 left-2 size-4 -translate-y-1/2 opacity-50 select-none" />
</form>
</SidebarGroupContent>
</SidebarGroup>
@@ -233,58 +221,17 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
</SidebarMenu>
</SidebarGroup>
<SidebarGroup className="group-data-[collapsible=icon]:hidden">
<SidebarGroupLabel>Components</SidebarGroupLabel>
<SidebarMenu>
{["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>
)
})}
{data.components.map((item) => (
<SidebarMenuItem key={item.name}>
<SidebarMenuButton asChild>
<Link href={`/sink#${item.name}`}>
<span>{getComponentName(item.name)}</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroup>
</SidebarContent>
@@ -295,3 +242,8 @@ 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())
}

View File

@@ -31,10 +31,7 @@ export function AvatarDemo() {
<AvatarFallback>CN</AvatarFallback>
</Avatar>
<Avatar>
<AvatarImage
src="https://github.com/maxleiter.png"
alt="@maxleiter"
/>
<AvatarImage src="https://github.com/leerob.png" alt="@leerob" />
<AvatarFallback>LR</AvatarFallback>
</Avatar>
<Avatar>
@@ -51,10 +48,7 @@ export function AvatarDemo() {
<AvatarFallback>CN</AvatarFallback>
</Avatar>
<Avatar>
<AvatarImage
src="https://github.com/maxleiter.png"
alt="@maxleiter"
/>
<AvatarImage src="https://github.com/leerob.png" alt="@leerob" />
<AvatarFallback>LR</AvatarFallback>
</Avatar>
<Avatar>
@@ -71,10 +65,7 @@ export function AvatarDemo() {
<AvatarFallback>CN</AvatarFallback>
</Avatar>
<Avatar>
<AvatarImage
src="https://github.com/maxleiter.png"
alt="@maxleiter"
/>
<AvatarImage src="https://github.com/leerob.png" alt="@leerob" />
<AvatarFallback>LR</AvatarFallback>
</Avatar>
<Avatar>

View File

@@ -1,581 +0,0 @@
"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>
)
}

View File

@@ -98,10 +98,7 @@ export function CardDemo() {
<AvatarFallback>CN</AvatarFallback>
</Avatar>
<Avatar>
<AvatarImage
src="https://github.com/maxleiter.png"
alt="@maxleiter"
/>
<AvatarImage src="https://github.com/leerob.png" alt="@leerob" />
<AvatarFallback>LR</AvatarFallback>
</Avatar>
<Avatar>

View File

@@ -62,7 +62,7 @@ const users = [
},
{
id: "2",
username: "maxleiter",
username: "leerob",
},
{
id: "3",

View File

@@ -274,10 +274,7 @@ function DropdownMenuAvatarOnly() {
className="size-8 rounded-full border-none p-0"
>
<Avatar>
<AvatarImage
src="https://github.com/maxleiter.png"
alt="maxleiter"
/>
<AvatarImage src="https://github.com/leerob.png" alt="leerob" />
<AvatarFallback className="rounded-lg">LR</AvatarFallback>
</Avatar>
</Button>
@@ -289,16 +286,13 @@ 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/maxleiter.png"
alt="maxleiter"
/>
<AvatarImage src="https://github.com/leerob.png" alt="leerob" />
<AvatarFallback className="rounded-lg">LR</AvatarFallback>
</Avatar>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-semibold">maxleiter</span>
<span className="truncate font-semibold">leerob</span>
<span className="text-muted-foreground truncate text-xs">
maxleiter@example.com
leerob@example.com
</span>
</div>
</div>

View File

@@ -1,250 +0,0 @@
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&apos;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&apos;re looking for doesn&apos;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&apos;re looking for doesn&apos;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&apos;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&apos;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>
)
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,663 +0,0 @@
"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>
)
}

View File

@@ -1,392 +0,0 @@
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>
)
}

View File

@@ -1,103 +0,0 @@
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>
)
}

View File

@@ -1,106 +0,0 @@
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&apos;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>
)
}

View File

@@ -1,147 +0,0 @@
"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>
)
}

View File

@@ -1,66 +0,0 @@
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>
)
}

View File

@@ -1,7 +1,62 @@
import { Metadata } from "next"
import { cookies } from "next/headers"
import { componentRegistry } from "@/app/(internal)/sink/component-registry"
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 { 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
@@ -11,25 +66,164 @@ export const metadata: Metadata = {
description: "A page with all components for testing purposes.",
}
export default function SinkPage() {
export default async function SinkPage() {
const cookieStore = await cookies()
const defaultOpen = cookieStore.get("sidebar_state")?.value === "true"
return (
<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>
<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>
)
}

View File

@@ -1,7 +1,7 @@
import * as React from "react"
import { Metadata } from "next"
import { notFound } from "next/navigation"
import { registryItemSchema } from "shadcn/schema"
import { registryItemSchema } from "shadcn/registry"
import { z } from "zod"
import { siteConfig } from "@/lib/config"

View File

@@ -0,0 +1,150 @@
import { promises as fs } from "fs"
import path from "path"
import type { Orama } from "@orama/orama"
import { create, load, search } from "@orama/orama"
import { registryItemSchema } from "shadcn/registry"
import { z } from "zod"
type RegistryItem = z.infer<typeof registryItemSchema>
const searchSchema = {
name: "string",
description: "string",
type: "string",
author: "string",
url: "string",
registryName: "string",
} as const
let searchDb: Orama<typeof searchSchema> | null = null
async function getSearchDb() {
if (searchDb) return searchDb
try {
const indexPath = path.join(
process.cwd(),
".data",
"external-registries-index.json"
)
const indexData = await fs.readFile(indexPath, "utf-8")
const savedDb = JSON.parse(indexData)
searchDb = await create({
schema: searchSchema,
})
await load(searchDb, savedDb)
return searchDb
} catch (error) {
console.error("Failed to load search index:", error)
return null
}
}
export async function GET(request: Request) {
try {
const { searchParams } = new URL(request.url)
const query = searchParams.get("q") || ""
const limit = parseInt(searchParams.get("limit") || "20")
const offset = parseInt(searchParams.get("offset") || "0")
if (!query) {
// Return all items if no query
const registryPath = path.join(
process.cwd(),
".data",
"external-registries.json"
)
try {
const data = await fs.readFile(registryPath, "utf-8")
const registry = JSON.parse(data)
const items = registry.items.slice(offset, offset + limit)
return Response.json(
{
items,
total: registry.items.length,
hasMore: offset + limit < registry.items.length,
},
{
headers: {
"Cache-Control":
"public, s-maxage=3600, stale-while-revalidate=86400",
},
}
)
} catch {
return Response.json({
items: [],
total: 0,
hasMore: false,
})
}
}
// Search using Orama
const db = await getSearchDb()
if (!db) {
// No search index - return empty results
return Response.json({
items: [],
total: 0,
hasMore: false,
})
}
const results = await search(db, {
term: query,
limit,
offset,
})
console.log(`Search query: "${query}", found ${results.count} results`)
// Load the full registry to get complete item data
const registryPath = path.join(
process.cwd(),
".data",
"external-registries.json"
)
try {
const data = await fs.readFile(registryPath, "utf-8")
const registry = JSON.parse(data)
// Map search results to full items
const items = results.hits
.map((hit) => {
return registry.items.find(
(item: RegistryItem) => item.name === hit.document.name
)
})
.filter(Boolean)
return Response.json(
{
items,
total: results.count,
hasMore: offset + limit < results.count,
},
{
headers: {
"Cache-Control":
"public, s-maxage=3600, stale-while-revalidate=86400",
},
}
)
} catch {
return Response.json({
items: [],
total: 0,
hasMore: false,
})
}
} catch (error) {
console.error("Search API error:", error)
return Response.json({ error: "Internal server error" }, { status: 500 })
}
}

View File

@@ -1,4 +1,5 @@
import type { Metadata } from "next"
import { NuqsAdapter } from "nuqs/adapters/next/app"
import { META_THEME_COLORS, siteConfig } from "@/lib/config"
import { fontVariables } from "@/lib/fonts"
@@ -84,20 +85,22 @@ export default function RootLayout({
</head>
<body
className={cn(
"text-foreground group/body theme-blue overscroll-none font-sans antialiased [--footer-height:calc(var(--spacing)*14)] [--header-height:calc(var(--spacing)*14)] xl:[--footer-height:calc(var(--spacing)*24)]",
"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)]",
fontVariables
)}
>
<ThemeProvider>
<LayoutProvider>
<ActiveThemeProvider initialTheme="blue">
{children}
<TailwindIndicator />
<Toaster position="top-center" />
<Analytics />
</ActiveThemeProvider>
</LayoutProvider>
</ThemeProvider>
<NuqsAdapter>
<ThemeProvider>
<LayoutProvider>
<ActiveThemeProvider>
{children}
<TailwindIndicator />
<Toaster position="top-center" />
<Analytics />
</ActiveThemeProvider>
</LayoutProvider>
</ThemeProvider>
</NuqsAdapter>
</body>
</html>
)

View File

@@ -8,7 +8,7 @@ import {
useState,
} from "react"
const DEFAULT_THEME = "blue"
const DEFAULT_THEME = "default"
type ThemeContextType = {
activeTheme: string

View File

@@ -6,9 +6,8 @@ import { Badge } from "@/registry/new-york-v4/ui/badge"
export function Announcement() {
return (
<Badge asChild variant="secondary" className="rounded-full">
<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 href="/docs/components/calendar">
New Calendar Component <ArrowRightIcon />
</Link>
</Badge>
)

View File

@@ -1,5 +1,5 @@
import * as React from "react"
import { registryItemFileSchema } from "shadcn/schema"
import { registryItemFileSchema } from "shadcn/registry"
import { z } from "zod"
import { highlightCode } from "@/lib/highlight-code"

View File

@@ -17,7 +17,7 @@ import {
Terminal,
} from "lucide-react"
import { ImperativePanelHandle } from "react-resizable-panels"
import { registryItemFileSchema, registryItemSchema } from "shadcn/schema"
import { registryItemFileSchema, registryItemSchema } from "shadcn/registry"
import { z } from "zod"
import { trackEvent } from "@/lib/events"

View File

@@ -10,17 +10,12 @@ export function Callout({
children,
icon,
className,
variant = "default",
...props
}: React.ComponentProps<typeof Alert> & {
icon?: React.ReactNode
variant?: "default" | "info" | "warning"
}) {
}: React.ComponentProps<typeof Alert> & { icon?: React.ReactNode }) {
return (
<Alert
data-variant={variant}
className={cn(
"bg-background text-foreground mt-6 w-auto border md:-mx-1",
"bg-surface text-surface-foreground mt-6 w-auto border-none md:-mx-1",
className
)}
{...props}

View File

@@ -32,12 +32,7 @@ import {
DialogHeader,
DialogTitle,
} from "@/registry/new-york-v4/ui/dialog"
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
} from "@/registry/new-york-v4/ui/input-group"
import { Input } from "@/registry/new-york-v4/ui/input"
import {
Tooltip,
TooltipContent,
@@ -164,25 +159,23 @@ export function CardsChat() {
}}
className="relative w-full"
>
<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>
<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>
</form>
</CardFooter>
</Card>

View File

@@ -5,15 +5,11 @@ import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/registry/new-york-v4/ui/card"
import {
Field,
FieldContent,
FieldDescription,
FieldLabel,
} from "@/registry/new-york-v4/ui/field"
import { Label } from "@/registry/new-york-v4/ui/label"
import { Switch } from "@/registry/new-york-v4/ui/switch"
export function CardsCookieSettings() {
@@ -24,20 +20,32 @@ export function CardsCookieSettings() {
<CardDescription>Manage your cookie settings here.</CardDescription>
</CardHeader>
<CardContent className="grid gap-6">
<Field orientation="horizontal">
<FieldContent>
<FieldLabel htmlFor="necessary">Strictly Necessary</FieldLabel>
<FieldDescription>
<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">
These cookies are essential in order to use the website and use
its features.
</FieldDescription>
</FieldContent>
</span>
</Label>
<Switch id="necessary" defaultChecked aria-label="Necessary" />
</Field>
<Field>
<Button variant="outline">Save preferences</Button>
</Field>
</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>
</CardContent>
<CardFooter>
<Button variant="outline" className="w-full">
Save preferences
</Button>
</CardFooter>
</Card>
)
}

View File

@@ -5,16 +5,12 @@ 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 (
@@ -25,48 +21,53 @@ export function CardsCreateAccount() {
Enter your email below to create your account
</CardDescription>
</CardHeader>
<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 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>
<CardFooter>
<Button className="w-full">Create account</Button>
</CardFooter>
</Card>
)
}

View File

@@ -5,21 +5,13 @@ 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,
@@ -30,7 +22,7 @@ const plans = [
{
id: "starter",
name: "Starter Plan",
description: "For small businesses.",
description: "Perfect for small businesses.",
price: "$10",
},
{
@@ -45,96 +37,91 @@ 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>
<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>
<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>
</CardContent>
<CardFooter className="flex justify-between">
<Button variant="outline" size="sm">
Cancel
</Button>
<Button size="sm">Upgrade Plan</Button>
</CardFooter>
</Card>
)
}

View File

@@ -209,7 +209,7 @@ export function CardsPayments() {
</CardAction>
</CardHeader>
<CardContent className="flex flex-col gap-4">
<div className="overflow-hidden rounded-md border">
<div className="rounded-md border">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (

View File

@@ -7,11 +7,12 @@ 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,
@@ -32,69 +33,65 @@ export function CardsReportIssue() {
What area are you having problems with?
</CardDescription>
</CardHeader>
<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 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>
<CardFooter className="justify-end gap-2">
<Button variant="ghost" size="sm">
Cancel
</Button>
<Button size="sm">Submit</Button>
</CardFooter>
</Card>
)
}

View File

@@ -14,14 +14,6 @@ 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,
@@ -81,35 +73,42 @@ export function CardsShare() {
<Separator className="my-4" />
<div className="flex flex-col gap-4">
<div className="text-sm font-medium">People with access</div>
<ItemGroup>
<div className="grid gap-6">
{people.map((person) => (
<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
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>
))}
</ItemGroup>
</div>
</div>
</CardContent>
</Card>

View File

@@ -23,13 +23,6 @@ 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,
@@ -78,58 +71,63 @@ const roles = [
export function CardsTeamMembers() {
return (
<Card className="gap-4">
<Card>
<CardHeader>
<CardTitle>Team Members</CardTitle>
<CardDescription>
Invite your team members to collaborate.
</CardDescription>
</CardHeader>
<CardContent>
<CardContent className="grid gap-6">
{teamMembers.map((member) => (
<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>
<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>
))}
</CardContent>
</Card>

View File

@@ -1,5 +1,5 @@
import * as React from "react"
import { registryItemSchema } from "shadcn/schema"
import { registryItemSchema } from "shadcn/registry"
import { z } from "zod"
import { highlightCode } from "@/lib/highlight-code"

View File

@@ -7,7 +7,6 @@ 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"
@@ -31,7 +30,6 @@ 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({
@@ -145,7 +143,7 @@ export function CommandMenu({
<Button
variant="secondary"
className={cn(
"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"
"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"
)}
onClick={() => setOpen(true)}
{...props}
@@ -153,10 +151,8 @@ 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">
<KbdGroup>
<Kbd className="border">{isMac ? "⌘" : "Ctrl"}</Kbd>
<Kbd className="border">K</Kbd>
</KbdGroup>
<CommandMenuKbd>{isMac ? "⌘" : "Ctrl"}</CommandMenuKbd>
<CommandMenuKbd className="aspect-square">K</CommandMenuKbd>
</div>
</Button>
</DialogTrigger>
@@ -218,10 +214,6 @@ 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}

View File

@@ -9,14 +9,12 @@ 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
}) {
@@ -53,8 +51,7 @@ export function ComponentPreviewTabs({
</Tabs>
<div
data-tab={tab}
data-chrome-less-on-mobile={chromeLessOnMobile}
className="data-[tab=code]:border-code relative rounded-lg border data-[chrome-less-on-mobile=true]:border-0 sm:data-[chrome-less-on-mobile=true]:border md:-mx-1"
className="data-[tab=code]:border-code relative rounded-lg border md:-mx-1"
>
<div
data-slot="preview"
@@ -64,8 +61,7 @@ export function ComponentPreviewTabs({
<div
data-align={align}
className={cn(
"preview flex w-full justify-center data-[align=center]:items-center data-[align=end]:items-end data-[align=start]:items-start",
chromeLessOnMobile ? "sm:p-10" : "h-[450px] p-10"
"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"
)}
>
{component}

View File

@@ -10,7 +10,6 @@ export function ComponentPreview({
className,
align = "center",
hideCode = false,
chromeLessOnMobile = false,
...props
}: React.ComponentProps<"div"> & {
name: string
@@ -18,13 +17,12 @@ 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 mt-6 text-sm">
<p className="text-muted-foreground text-sm">
Component{" "}
<code className="bg-muted relative rounded px-[0.3rem] py-[0.2rem] font-mono text-sm">
{name}
@@ -65,7 +63,6 @@ export function ComponentPreview({
hideCode={hideCode}
component={<Component />}
source={<ComponentSource name={name} collapsible={false} />}
chromeLessOnMobile={chromeLessOnMobile}
{...props}
/>
)

View File

@@ -43,14 +43,6 @@ 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)

View File

@@ -0,0 +1,236 @@
"use client"
import * as React from "react"
import { ArrowUpRightIcon, Loader2Icon, SearchIcon } from "lucide-react"
import { useQueryState } from "nuqs"
import { registryItemSchema } from "shadcn/registry"
import { z } from "zod"
import { cn } from "@/lib/utils"
import { Button } from "@/registry/new-york-v4/ui/button"
import { Input } from "@/registry/new-york-v4/ui/input"
import { Skeleton } from "@/registry/new-york-v4/ui/skeleton"
export function ComponentsCommunitySearch({
className,
}: React.ComponentProps<"div">) {
return (
<div className={cn("grid gap-4", className)}>
<ComponentsCommunitySearchForm />
<ComponentsCommunitySearchResults />
</div>
)
}
interface SearchResponse {
items: z.infer<typeof registryItemSchema>[]
total: number
hasMore: boolean
}
function ComponentsCommunitySearchForm() {
const [search, setSearch] = useQueryState("q", {
defaultValue: "",
throttleMs: 150,
})
const [isSearching, setIsSearching] = React.useState(false)
const inputRef = React.useRef<HTMLInputElement>(null)
React.useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "/" && !e.ctrlKey && !e.metaKey) {
const activeElement = document.activeElement
const isInputFocused =
activeElement instanceof HTMLInputElement ||
activeElement instanceof HTMLTextAreaElement
if (!isInputFocused) {
e.preventDefault()
inputRef.current?.focus()
}
}
}
document.addEventListener("keydown", handleKeyDown)
return () => document.removeEventListener("keydown", handleKeyDown)
}, [])
return (
<div className="relative">
<SearchIcon className="text-muted-foreground absolute top-1/2 left-3 size-4 -translate-y-1/2" />
<Input
ref={inputRef}
placeholder="Search components..."
value={search}
onChange={(e) => {
setSearch(e.target.value)
setIsSearching(true)
setTimeout(() => setIsSearching(false), 300)
}}
className="pr-9 pl-9 shadow-none"
/>
{isSearching && (
<Loader2Icon className="text-muted-foreground absolute top-1/2 right-3 size-4 -translate-y-1/2 animate-spin" />
)}
</div>
)
}
function ComponentsCommunityResults({
items,
}: {
items: z.infer<typeof registryItemSchema>[]
}) {
if (items.length === 0) {
return (
<div className="p-6 text-center text-sm">
<p className="text-muted-foreground text-balance">
No components found for this search. Try a different search term.
</p>
</div>
)
}
return (
<div className="grid gap-1">
{items.map((item, index) => (
<a
key={`${item.type}-${item.name}-${index}`}
href={`${item.meta?.url ?? ""}?utm_source=shadcn-ui&utm_medium=referral&utm_campaign=components-community`}
target="_blank"
rel="noopener noreferrer"
className="group hover:bg-muted focus-visible:border-ring focus-visible:ring-ring/50 flex h-16 flex-col gap-1 rounded-md p-3 transition-colors outline-none focus-visible:ring-[3px]"
title={`${item.name} - ${item.description}`}
>
<div className="flex items-center gap-1 leading-none font-medium underline-offset-4">
{item.name}{" "}
{item.meta?.registryName && (
<div className="text-muted-foreground ml-auto flex items-center gap-1 text-xs opacity-0 transition-opacity group-hover:opacity-100 group-focus-visible:opacity-100">
{item.meta.registryName}
<ArrowUpRightIcon className="size-3" />
</div>
)}
</div>
{item.description && (
<div className="text-muted-foreground line-clamp-1 max-w-[80%] text-sm">
{item.description}
</div>
)}
</a>
))}
</div>
)
}
function ComponentsCommunitySkeleton() {
return (
<div className="grid gap-1">
{Array.from({ length: 20 }).map((_, i) => (
<div key={i} className="flex h-16 flex-col gap-1 rounded-md p-3">
<Skeleton className="h-4 w-32 rounded-md" />
<Skeleton className="h-4 w-full max-w-[90%] rounded-md" />
</div>
))}
</div>
)
}
function ComponentsCommunitySearchResults() {
const [search] = useQueryState("q", { defaultValue: "" })
const [items, setItems] = React.useState<
z.infer<typeof registryItemSchema>[]
>([])
const [loading, setLoading] = React.useState(true)
const [hasMore, setHasMore] = React.useState(false)
const [offset, setOffset] = React.useState(0)
const abortControllerRef = React.useRef<AbortController | null>(null)
const searchCacheRef = React.useRef<Map<string, SearchResponse>>(new Map())
const performSearch = React.useCallback(
async (query: string, currentOffset = 0) => {
const cacheKey = `${query}:${currentOffset}`
const cached = searchCacheRef.current.get(cacheKey)
if (cached && currentOffset === 0) {
setItems(cached.items)
setHasMore(cached.hasMore)
setOffset(0)
return
}
if (abortControllerRef.current) {
abortControllerRef.current.abort()
}
const controller = new AbortController()
abortControllerRef.current = controller
setLoading(true)
try {
const params = new URLSearchParams({
q: query,
limit: "20",
offset: currentOffset.toString(),
})
const response = await fetch(`/api/search/community?${params}`, {
signal: controller.signal,
})
if (!response.ok) {
throw new Error("Search failed")
}
const data: SearchResponse = await response.json()
searchCacheRef.current.set(cacheKey, data)
if (currentOffset === 0) {
setItems(data.items)
} else {
setItems((prev) => [...prev, ...data.items])
}
setHasMore(data.hasMore)
setOffset(currentOffset)
} catch (error) {
if (error instanceof Error && error.name !== "AbortError") {
console.error("Search error:", error)
}
} finally {
setLoading(false)
}
},
[]
)
React.useEffect(() => {
performSearch(search, 0)
}, [search, performSearch])
const loadMore = React.useCallback(() => {
if (!loading && hasMore) {
performSearch(search, offset + 20)
}
}, [search, offset, loading, hasMore, performSearch])
if (loading && items.length === 0) {
return <ComponentsCommunitySkeleton />
}
return (
<>
<ComponentsCommunityResults items={items} />
{hasMore && (
<div className="flex justify-center pt-4">
<Button
onClick={loadMore}
disabled={loading}
variant="secondary"
size="sm"
>
{loading ? "Loading..." : "Load more"}
</Button>
</div>
)}
</>
)
}

View File

@@ -1,6 +1,5 @@
import Link from "next/link"
import { PAGES_NEW } from "@/lib/docs"
import { source } from "@/lib/source"
export function ComponentsList() {
@@ -22,15 +21,9 @@ export function ComponentsList() {
<Link
key={component.$id}
href={component.url}
className="inline-flex items-center gap-2 text-lg font-medium underline-offset-4 hover:underline md:text-base"
className="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>

View File

@@ -1,156 +1,33 @@
"use client"
import { IconCheck, IconChevronDown, IconCopy } from "@tabler/icons-react"
import { IconCheck, IconCopy } from "@tabler/icons-react"
import { useCopyToClipboard } from "@/hooks/use-copy-to-clipboard"
import { Button } from "@/registry/new-york-v4/ui/button"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/registry/new-york-v4/ui/dropdown-menu"
import {
Popover,
PopoverAnchor,
PopoverContent,
PopoverTrigger,
} from "@/registry/new-york-v4/ui/popover"
import { Separator } from "@/registry/new-york-v4/ui/separator"
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/registry/new-york-v4/ui/tooltip"
function getPromptUrl(baseURL: string, url: string) {
return `${baseURL}?q=${encodeURIComponent(
`Im looking at this shadcn/ui documentation: ${url}.
Help me understand how to use it. Be ready to explain concepts, give examples, or help debug based on it.
`
)}`
}
const menuItems = {
markdown: (url: string) => (
<a href={`${url}.md`} target="_blank" rel="noopener noreferrer">
<svg strokeLinejoin="round" viewBox="0 0 22 16">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M19.5 2.25H2.5C1.80964 2.25 1.25 2.80964 1.25 3.5V12.5C1.25 13.1904 1.80964 13.75 2.5 13.75H19.5C20.1904 13.75 20.75 13.1904 20.75 12.5V3.5C20.75 2.80964 20.1904 2.25 19.5 2.25ZM2.5 1C1.11929 1 0 2.11929 0 3.5V12.5C0 13.8807 1.11929 15 2.5 15H19.5C20.8807 15 22 13.8807 22 12.5V3.5C22 2.11929 20.8807 1 19.5 1H2.5ZM3 4.5H4H4.25H4.6899L4.98715 4.82428L7 7.02011L9.01285 4.82428L9.3101 4.5H9.75H10H11V5.5V11.5H9V7.79807L7.73715 9.17572L7 9.97989L6.26285 9.17572L5 7.79807V11.5H3V5.5V4.5ZM15 8V4.5H17V8H19.5L17 10.5L16 11.5L15 10.5L12.5 8H15Z"
fill="currentColor"
/>
</svg>
View as Markdown
</a>
),
v0: (url: string) => (
<a
href={getPromptUrl("https://v0.dev", url)}
target="_blank"
rel="noopener noreferrer"
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
viewBox="0 0 147 70"
className="size-4.5 -translate-x-px"
>
<path d="M56 50.203V14h14v46.156C70 65.593 65.593 70 60.156 70c-2.596 0-5.158-1-7-2.843L0 14h19.797L56 50.203ZM147 56h-14V23.953L100.953 56H133v14H96.687C85.814 70 77 61.186 77 50.312V14h14v32.156L123.156 14H91V0h36.312C138.186 0 147 8.814 147 19.688V56Z" />
</svg>
<span className="-translate-x-[2px]">Open in v0</span>
</a>
),
chatgpt: (url: string) => (
<a
href={getPromptUrl("https://chatgpt.com", url)}
target="_blank"
rel="noopener noreferrer"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path
d="M22.282 9.821a5.985 5.985 0 0 0-.516-4.91 6.046 6.046 0 0 0-6.51-2.9A6.065 6.065 0 0 0 4.981 4.18a5.985 5.985 0 0 0-3.998 2.9 6.046 6.046 0 0 0 .743 7.097 5.98 5.98 0 0 0 .51 4.911 6.051 6.051 0 0 0 6.515 2.9A5.985 5.985 0 0 0 13.26 24a6.056 6.056 0 0 0 5.772-4.206 5.99 5.99 0 0 0 3.997-2.9 6.056 6.056 0 0 0-.747-7.073zM13.26 22.43a4.476 4.476 0 0 1-2.876-1.04l.141-.081 4.779-2.758a.795.795 0 0 0 .392-.681v-6.737l2.02 1.168a.071.071 0 0 1 .038.052v5.583a4.504 4.504 0 0 1-4.494 4.494zM3.6 18.304a4.47 4.47 0 0 1-.535-3.014l.142.085 4.783 2.759a.771.771 0 0 0 .78 0l5.843-3.369v2.332a.08.08 0 0 1-.033.062L9.74 19.95a4.5 4.5 0 0 1-6.14-1.646zM2.34 7.896a4.485 4.485 0 0 1 2.366-1.973V11.6a.766.766 0 0 0 .388.676l5.815 3.355-2.02 1.168a.076.076 0 0 1-.071 0l-4.83-2.786A4.504 4.504 0 0 1 2.34 7.872zm16.597 3.855-5.833-3.387L15.119 7.2a.076.076 0 0 1 .071 0l4.83 2.791a4.494 4.494 0 0 1-.676 8.105v-5.678a.79.79 0 0 0-.407-.667zm2.01-3.023-.141-.085-4.774-2.782a.776.776 0 0 0-.785 0L9.409 9.23V6.897a.066.066 0 0 1 .028-.061l4.83-2.787a4.5 4.5 0 0 1 6.68 4.66zm-12.64 4.135-2.02-1.164a.08.08 0 0 1-.038-.057V6.075a4.5 4.5 0 0 1 7.375-3.453l-.142.08-4.778 2.758a.795.795 0 0 0-.393.681zm1.097-2.365 2.602-1.5 2.607 1.5v2.999l-2.597 1.5-2.607-1.5Z"
fill="currentColor"
/>
</svg>
Open in ChatGPT
</a>
),
claude: (url: string) => (
<a
href={getPromptUrl("https://claude.ai/new", url)}
target="_blank"
rel="noopener noreferrer"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path
d="m4.714 15.956 4.718-2.648.079-.23-.08-.128h-.23l-.79-.048-2.695-.073-2.337-.097-2.265-.122-.57-.121-.535-.704.055-.353.48-.321.685.06 1.518.104 2.277.157 1.651.098 2.447.255h.389l.054-.158-.133-.097-.103-.098-2.356-1.596-2.55-1.688-1.336-.972-.722-.491L2 6.223l-.158-1.008.655-.722.88.06.225.061.893.686 1.906 1.476 2.49 1.833.364.304.146-.104.018-.072-.164-.274-1.354-2.446-1.445-2.49-.644-1.032-.17-.619a2.972 2.972 0 0 1-.103-.729L6.287.133 6.7 0l.995.134.42.364.619 1.415L9.735 4.14l1.555 3.03.455.898.243.832.09.255h.159V9.01l.127-1.706.237-2.095.23-2.695.08-.76.376-.91.747-.492.583.28.48.685-.067.444-.286 1.851-.558 2.903-.365 1.942h.213l.243-.242.983-1.306 1.652-2.064.728-.82.85-.904.547-.431h1.032l.759 1.129-.34 1.166-1.063 1.347-.88 1.142-1.263 1.7-.79 1.36.074.11.188-.02 2.853-.606 1.542-.28 1.84-.315.832.388.09.395-.327.807-1.967.486-2.307.462-3.436.813-.043.03.049.061 1.548.146.662.036h1.62l3.018.225.79.522.473.638-.08.485-1.213.62-1.64-.389-3.825-.91-1.31-.329h-.183v.11l1.093 1.068 2.003 1.81 2.508 2.33.127.578-.321.455-.34-.049-2.204-1.657-.85-.747-1.925-1.62h-.127v.17l.443.649 2.343 3.521.122 1.08-.17.353-.607.213-.668-.122-1.372-1.924-1.415-2.168-1.141-1.943-.14.08-.674 7.254-.316.37-.728.28-.607-.461-.322-.747.322-1.476.388-1.924.316-1.53.285-1.9.17-.632-.012-.042-.14.018-1.432 1.967-2.18 2.945-1.724 1.845-.413.164-.716-.37.066-.662.401-.589 2.386-3.036 1.439-1.882.929-1.086-.006-.158h-.055L4.138 18.56l-1.13.146-.485-.456.06-.746.231-.243 1.907-1.312Z"
fill="currentColor"
/>
</svg>
Open in Claude
</a>
),
}
export function DocsCopyPage({ page, url }: { page: string; url: string }) {
export function DocsCopyPage({ page }: { page: string }) {
const { copyToClipboard, isCopied } = useCopyToClipboard()
const trigger = (
<Button
variant="secondary"
size="sm"
className="peer -ml-0.5 size-8 shadow-none md:size-7 md:text-[0.8rem]"
>
<IconChevronDown className="rotate-180 sm:rotate-0" />
</Button>
)
return (
<Popover>
<div className="bg-secondary group/buttons relative flex rounded-lg *:[[data-slot=button]]:focus-visible:relative *:[[data-slot=button]]:focus-visible:z-10">
<PopoverAnchor />
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="secondary"
variant="outline"
size="sm"
className="h-8 shadow-none md:h-7 md:text-[0.8rem]"
className="h-8 pl-1.5 md:h-7 [&>svg]:size-3.5"
onClick={() => copyToClipboard(page)}
>
{isCopied ? <IconCheck /> : <IconCopy />}
Copy Page
{isCopied ? <IconCheck /> : <IconCopy />} Copy Page
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild className="hidden sm:flex">
{trigger}
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="shadow-none">
{Object.entries(menuItems).map(([key, value]) => (
<DropdownMenuItem key={key} asChild>
{value(url)}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
<Separator
orientation="vertical"
className="!bg-foreground/10 absolute top-0 right-8 z-0 !h-8 peer-focus-visible:opacity-0 sm:right-7 sm:!h-7"
/>
<PopoverTrigger asChild className="flex sm:hidden">
{trigger}
</PopoverTrigger>
<PopoverContent
className="bg-background/70 dark:bg-background/60 w-52 !origin-center rounded-lg p-1 shadow-sm backdrop-blur-sm"
align="start"
>
{Object.entries(menuItems).map(([key, value]) => (
<Button
variant="ghost"
size="lg"
asChild
key={key}
className="*:[svg]:text-muted-foreground w-full justify-start text-base font-normal"
>
{value(url)}
</Button>
))}
</PopoverContent>
</div>
</Popover>
</TooltipTrigger>
<TooltipContent>
<p>Copy as Markdown</p>
</TooltipContent>
</Tooltip>
)
}

View File

@@ -3,8 +3,6 @@
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,
@@ -17,32 +15,6 @@ 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
@@ -51,96 +23,40 @@ export function DocsSidebar({
return (
<Sidebar
className="sticky top-[calc(var(--header-height)+1px)] z-30 hidden h-[calc(100svh-var(--footer-height)+2rem)] bg-transparent lg:flex"
className="sticky top-[calc(var(--header-height)+1px)] z-30 hidden h-[calc(100svh-var(--header-height)-var(--footer-height))] bg-transparent lg:flex"
collapsible="none"
{...props}
>
<SidebarContent className="no-scrollbar overflow-x-hidden px-2 pb-12">
<SidebarContent className="no-scrollbar px-2 pb-12">
<div className="h-(--top-spacing) shrink-0" />
<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>
)
{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>
)
})}
</SidebarMenu>
)}
</SidebarGroupContent>
</SidebarGroup>
)
})}
)
})}
</SidebarMenu>
)}
</SidebarGroupContent>
</SidebarGroup>
))}
</SidebarContent>
</Sidebar>
)

View File

@@ -4,8 +4,6 @@ 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"
@@ -15,30 +13,6 @@ 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,
@@ -105,23 +79,6 @@ 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") {
@@ -133,20 +90,13 @@ 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}{" "}
{PAGES_NEW.includes(item.url) && (
<span className="flex size-2 rounded-full bg-blue-500" />
)}
{item.name}
</MobileLink>
)
}

View File

@@ -3,7 +3,7 @@ import { Icons } from "@/components/icons"
import { Button } from "@/registry/new-york-v4/ui/button"
// v0 uses the default style.
const V0_STYLE = "new-york-v4"
const V0_STYLE = "default"
export function OpenInV0Button({
name,

View File

@@ -2,10 +2,10 @@ import { siteConfig } from "@/lib/config"
export function SiteFooter() {
return (
<footer className="group-has-[.section-soft]/body:bg-surface/40 3xl:fixed:bg-transparent group-has-[.docs-nav]/body:pb-20 group-has-[.docs-nav]/body:sm:pb-0 dark:bg-transparent">
<footer className="group-has-[.section-soft]/body:bg-surface/40 3xl:fixed:bg-transparent dark:bg-transparent">
<div className="container-wrapper px-4 xl:px-6">
<div className="flex h-(--footer-height) items-center justify-between">
<div className="text-muted-foreground w-full px-1 text-center text-xs leading-loose sm:text-sm">
<div className="text-muted-foreground w-full text-center text-xs leading-loose sm:text-sm">
Built by{" "}
<a
href={siteConfig.links.twitter}

Some files were not shown because too many files have changed in this diff Show More