Compare commits

..

2 Commits

Author SHA1 Message Date
shadcn
cbf7b3708a chore: minor refactor and error message 2026-01-06 13:23:13 +04:00
yeasin2002
9da12c13f9 fix(cli): #9160 updated CLI name validation 2025-12-20 17:36:15 +06:00
2963 changed files with 16029 additions and 121464 deletions

View File

@@ -0,0 +1,5 @@
---
"shadcn": patch
---
validate app name on create

View File

@@ -0,0 +1,63 @@
name: Add registry to directory
description: Add your registry to the directory
title: "[Registry Directory]: "
labels: ["registry", "directory"]
assignees: []
body:
- type: input
id: name
attributes:
label: Name
description: The name of your registry. This is also the namespace.
placeholder: e.g., "@acme"
validations:
required: true
- type: input
id: url
attributes:
label: URL
description: The URL to your registry index. Use {name} placeholder.
placeholder: https://ui.acme.com/r/{name}.json
validations:
required: true
- type: input
id: homepage
attributes:
label: Homepage
description: The URL to your registry homepage. This is where users can browse your registry.
placeholder: https://ui.acme.com
validations:
required: true
- type: textarea
id: description
attributes:
label: Description
description: Briefly describe what is your registry and what type of components or code it distributes.
placeholder:
validations:
required: true
- type: textarea
id: logo
attributes:
label: Logo
description: Add your SVG logo here.
placeholder:
validations:
required: true
- type: checkboxes
id: requirements
attributes:
label: Checklist
description: Verify that your registry meets the following requirements.
options:
- label: The registry must be open source and publicly accessible.
- label: The registry must be a valid JSON file that conforms to the [registry schema](https://ui.shadcn.com/docs/registry/registry-json) specification.
- label: The `files` array, if present on your registry items, must NOT include a `content` property.
- label: I've attached a square SVG logo to this issue
validations:
required: true

View File

@@ -1,12 +1,12 @@
// ORIGINALLY FROM CLOUDFLARE WRANGLER: // ORIGINALLY FROM CLOUDFLARE WRANGLER:
// https://github.com/cloudflare/wrangler2/blob/main/.github/changeset-version.js // https://github.com/cloudflare/wrangler2/blob/main/.github/changeset-version.js
import { execSync } from "child_process" import { exec } from "child_process"
// This script is used by the `release.yml` workflow to update the version of the packages being released. // This script is used by the `release.yml` workflow to update the version of the packages being released.
// The standard step is only to run `changeset version` but this does not update the pnpm-lock.yaml file. // The standard step is only to run `changeset version` but this does not update the package-lock.json file.
// So we also run `pnpm install`, which does this update. // So we also run `npm install`, which does this update.
// This is a workaround until this is handled automatically by `changeset version`. // This is a workaround until this is handled automatically by `changeset version`.
// See https://github.com/changesets/changesets/issues/421. // See https://github.com/changesets/changesets/issues/421.
execSync("npx changeset version", { stdio: "inherit" }) exec("npx changeset version")
execSync("pnpm install --lockfile-only", { stdio: "inherit" }) exec("npm install")

View File

@@ -1,8 +1,10 @@
"use client" "use client"
import * as React from "react" import * as React from "react"
import { Button } from "@/examples/radix/ui/button" import { IconMinus, IconPlus } from "@tabler/icons-react"
import { ButtonGroup } from "@/examples/radix/ui/button-group"
import { Button } from "@/registry/new-york-v4/ui/button"
import { ButtonGroup } from "@/registry/new-york-v4/ui/button-group"
import { import {
Field, Field,
FieldContent, FieldContent,
@@ -13,11 +15,13 @@ import {
FieldSeparator, FieldSeparator,
FieldSet, FieldSet,
FieldTitle, FieldTitle,
} from "@/examples/radix/ui/field" } from "@/registry/new-york-v4/ui/field"
import { Input } from "@/examples/radix/ui/input" import { Input } from "@/registry/new-york-v4/ui/input"
import { RadioGroup, RadioGroupItem } from "@/examples/radix/ui/radio-group" import {
import { Switch } from "@/examples/radix/ui/switch" RadioGroup,
import { IconMinus, IconPlus } from "@tabler/icons-react" RadioGroupItem,
} from "@/registry/new-york-v4/ui/radio-group"
import { Switch } from "@/registry/new-york-v4/ui/switch"
export function AppearanceSettings() { export function AppearanceSettings() {
const [gpuCount, setGpuCount] = React.useState(8) const [gpuCount, setGpuCount] = React.useState(8)
@@ -93,7 +97,7 @@ export function AppearanceSettings() {
value={gpuCount} value={gpuCount}
onChange={handleGpuInputChange} onChange={handleGpuInputChange}
size={3} size={3}
className="h-7 !w-14 font-mono" className="h-8 !w-14 font-mono"
maxLength={3} maxLength={3}
/> />
<Button <Button

View File

@@ -1,8 +1,20 @@
"use client" "use client"
import * as React from "react" import * as React from "react"
import { Button } from "@/examples/radix/ui/button" import {
import { ButtonGroup } from "@/examples/radix/ui/button-group" ArchiveIcon,
ArrowLeftIcon,
CalendarPlusIcon,
ClockIcon,
ListFilterIcon,
MailCheckIcon,
MoreHorizontalIcon,
TagIcon,
Trash2Icon,
} from "lucide-react"
import { Button } from "@/registry/new-york-v4/ui/button"
import { ButtonGroup } from "@/registry/new-york-v4/ui/button-group"
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
@@ -15,18 +27,7 @@ import {
DropdownMenuSubContent, DropdownMenuSubContent,
DropdownMenuSubTrigger, DropdownMenuSubTrigger,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/examples/radix/ui/dropdown-menu" } from "@/registry/new-york-v4/ui/dropdown-menu"
import {
ArchiveIcon,
ArrowLeftIcon,
CalendarPlusIcon,
ClockIcon,
ListFilterIcon,
MailCheckIcon,
MoreHorizontalIcon,
TagIcon,
Trash2Icon,
} from "lucide-react"
export function ButtonGroupDemo() { export function ButtonGroupDemo() {
const [label, setLabel] = React.useState("personal") const [label, setLabel] = React.useState("personal")
@@ -56,7 +57,7 @@ export function ButtonGroupDemo() {
<MoreHorizontalIcon /> <MoreHorizontalIcon />
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-48"> <DropdownMenuContent align="end" className="w-48 [--radius:1rem]">
<DropdownMenuGroup> <DropdownMenuGroup>
<DropdownMenuItem> <DropdownMenuItem>
<MailCheckIcon /> <MailCheckIcon />

View File

@@ -1,20 +1,21 @@
"use client" "use client"
import * as React from "react" import * as React from "react"
import { Button } from "@/examples/radix/ui/button" import { AudioLinesIcon, PlusIcon } from "lucide-react"
import { ButtonGroup } from "@/examples/radix/ui/button-group"
import { Button } from "@/registry/new-york-v4/ui/button"
import { ButtonGroup } from "@/registry/new-york-v4/ui/button-group"
import { import {
InputGroup, InputGroup,
InputGroupAddon, InputGroupAddon,
InputGroupButton, InputGroupButton,
InputGroupInput, InputGroupInput,
} from "@/examples/radix/ui/input-group" } from "@/registry/new-york-v4/ui/input-group"
import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
TooltipTrigger, TooltipTrigger,
} from "@/examples/radix/ui/tooltip" } from "@/registry/new-york-v4/ui/tooltip"
import { AudioLinesIcon, PlusIcon } from "lucide-react"
export function ButtonGroupInputGroup() { export function ButtonGroupInputGroup() {
const [voiceEnabled, setVoiceEnabled] = React.useState(false) const [voiceEnabled, setVoiceEnabled] = React.useState(false)

View File

@@ -1,9 +1,10 @@
"use client" "use client"
import { Button } from "@/examples/radix/ui/button"
import { ButtonGroup } from "@/examples/radix/ui/button-group"
import { ArrowLeftIcon, ArrowRightIcon } from "lucide-react" 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() { export function ButtonGroupNested() {
return ( return (
<ButtonGroup> <ButtonGroup>

View File

@@ -1,13 +1,14 @@
import { Button } from "@/examples/radix/ui/button" import { BotIcon, ChevronDownIcon } from "lucide-react"
import { ButtonGroup } from "@/examples/radix/ui/button-group"
import { Button } from "@/registry/new-york-v4/ui/button"
import { ButtonGroup } from "@/registry/new-york-v4/ui/button-group"
import { import {
Popover, Popover,
PopoverContent, PopoverContent,
PopoverTrigger, PopoverTrigger,
} from "@/examples/radix/ui/popover" } from "@/registry/new-york-v4/ui/popover"
import { Separator } from "@/examples/radix/ui/separator" import { Separator } from "@/registry/new-york-v4/ui/separator"
import { Textarea } from "@/examples/radix/ui/textarea" import { Textarea } from "@/registry/new-york-v4/ui/textarea"
import { BotIcon, ChevronDownIcon } from "lucide-react"
export function ButtonGroupPopover() { export function ButtonGroupPopover() {
return ( return (

View File

@@ -1,10 +1,11 @@
import { PlusIcon } from "lucide-react"
import { import {
Avatar, Avatar,
AvatarFallback, AvatarFallback,
AvatarGroup,
AvatarImage, AvatarImage,
} from "@/examples/radix/ui/avatar" } from "@/registry/new-york-v4/ui/avatar"
import { Button } from "@/examples/radix/ui/button" import { Button } from "@/registry/new-york-v4/ui/button"
import { import {
Empty, Empty,
EmptyContent, EmptyContent,
@@ -12,15 +13,14 @@ import {
EmptyHeader, EmptyHeader,
EmptyMedia, EmptyMedia,
EmptyTitle, EmptyTitle,
} from "@/examples/radix/ui/empty" } from "@/registry/new-york-v4/ui/empty"
import { PlusIcon } from "lucide-react"
export function EmptyAvatarGroup() { export function EmptyAvatarGroup() {
return ( return (
<Empty className="flex-none border py-10"> <Empty className="flex-none border">
<EmptyHeader> <EmptyHeader>
<EmptyMedia> <EmptyMedia>
<AvatarGroup className="grayscale"> <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> <Avatar>
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" /> <AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
<AvatarFallback>CN</AvatarFallback> <AvatarFallback>CN</AvatarFallback>
@@ -39,7 +39,7 @@ export function EmptyAvatarGroup() {
/> />
<AvatarFallback>ER</AvatarFallback> <AvatarFallback>ER</AvatarFallback>
</Avatar> </Avatar>
</AvatarGroup> </div>
</EmptyMedia> </EmptyMedia>
<EmptyTitle>No Team Members</EmptyTitle> <EmptyTitle>No Team Members</EmptyTitle>
<EmptyDescription> <EmptyDescription>

View File

@@ -1,5 +1,5 @@
import { Checkbox } from "@/examples/radix/ui/checkbox" import { Checkbox } from "@/registry/new-york-v4/ui/checkbox"
import { Field, FieldLabel } from "@/examples/radix/ui/field" import { Field, FieldLabel } from "@/registry/new-york-v4/ui/field"
export function FieldCheckbox() { export function FieldCheckbox() {
return ( return (

View File

@@ -1,5 +1,5 @@
import { Button } from "@/examples/radix/ui/button" import { Button } from "@/registry/new-york-v4/ui/button"
import { Checkbox } from "@/examples/radix/ui/checkbox" import { Checkbox } from "@/registry/new-york-v4/ui/checkbox"
import { import {
Field, Field,
FieldDescription, FieldDescription,
@@ -8,17 +8,16 @@ import {
FieldLegend, FieldLegend,
FieldSeparator, FieldSeparator,
FieldSet, FieldSet,
} from "@/examples/radix/ui/field" } from "@/registry/new-york-v4/ui/field"
import { Input } from "@/examples/radix/ui/input" import { Input } from "@/registry/new-york-v4/ui/input"
import { import {
Select, Select,
SelectContent, SelectContent,
SelectGroup,
SelectItem, SelectItem,
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/examples/radix/ui/select" } from "@/registry/new-york-v4/ui/select"
import { Textarea } from "@/examples/radix/ui/textarea" import { Textarea } from "@/registry/new-york-v4/ui/textarea"
export function FieldDemo() { export function FieldDemo() {
return ( return (
@@ -70,20 +69,18 @@ export function FieldDemo() {
<SelectValue placeholder="MM" /> <SelectValue placeholder="MM" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectGroup> <SelectItem value="01">01</SelectItem>
<SelectItem value="01">01</SelectItem> <SelectItem value="02">02</SelectItem>
<SelectItem value="02">02</SelectItem> <SelectItem value="03">03</SelectItem>
<SelectItem value="03">03</SelectItem> <SelectItem value="04">04</SelectItem>
<SelectItem value="04">04</SelectItem> <SelectItem value="05">05</SelectItem>
<SelectItem value="05">05</SelectItem> <SelectItem value="06">06</SelectItem>
<SelectItem value="06">06</SelectItem> <SelectItem value="07">07</SelectItem>
<SelectItem value="07">07</SelectItem> <SelectItem value="08">08</SelectItem>
<SelectItem value="08">08</SelectItem> <SelectItem value="09">09</SelectItem>
<SelectItem value="09">09</SelectItem> <SelectItem value="10">10</SelectItem>
<SelectItem value="10">10</SelectItem> <SelectItem value="11">11</SelectItem>
<SelectItem value="11">11</SelectItem> <SelectItem value="12">12</SelectItem>
<SelectItem value="12">12</SelectItem>
</SelectGroup>
</SelectContent> </SelectContent>
</Select> </Select>
</Field> </Field>
@@ -96,14 +93,12 @@ export function FieldDemo() {
<SelectValue placeholder="YYYY" /> <SelectValue placeholder="YYYY" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectGroup> <SelectItem value="2024">2024</SelectItem>
<SelectItem value="2024">2024</SelectItem> <SelectItem value="2025">2025</SelectItem>
<SelectItem value="2025">2025</SelectItem> <SelectItem value="2026">2026</SelectItem>
<SelectItem value="2026">2026</SelectItem> <SelectItem value="2027">2027</SelectItem>
<SelectItem value="2027">2027</SelectItem> <SelectItem value="2028">2028</SelectItem>
<SelectItem value="2028">2028</SelectItem> <SelectItem value="2029">2029</SelectItem>
<SelectItem value="2029">2029</SelectItem>
</SelectGroup>
</SelectContent> </SelectContent>
</Select> </Select>
</Field> </Field>

View File

@@ -1,5 +1,5 @@
import { Card, CardContent } from "@/examples/radix/ui/card" import { Card, CardContent } from "@/registry/new-york-v4/ui/card"
import { Checkbox } from "@/examples/radix/ui/checkbox" import { Checkbox } from "@/registry/new-york-v4/ui/checkbox"
import { import {
Field, Field,
FieldDescription, FieldDescription,
@@ -8,7 +8,7 @@ import {
FieldLegend, FieldLegend,
FieldSet, FieldSet,
FieldTitle, FieldTitle,
} from "@/examples/radix/ui/field" } from "@/registry/new-york-v4/ui/field"
const options = [ const options = [
{ {
@@ -50,7 +50,7 @@ export function FieldHear() {
> >
<Field <Field
orientation="horizontal" 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!" 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 <Checkbox
value={option.value} value={option.value}

View File

@@ -1,8 +1,13 @@
"use client" "use client"
import { useState } from "react" import { useState } from "react"
import { Field, FieldDescription, FieldTitle } from "@/examples/radix/ui/field"
import { Slider } from "@/examples/radix/ui/slider" import {
Field,
FieldDescription,
FieldTitle,
} from "@/registry/new-york-v4/ui/field"
import { Slider } from "@/registry/new-york-v4/ui/slider"
export function FieldSlider() { export function FieldSlider() {
const [value, setValue] = useState([200, 800]) const [value, setValue] = useState([200, 800])

View File

@@ -1,4 +1,4 @@
import { FieldSeparator } from "@/examples/radix/ui/field" import { FieldSeparator } from "@/registry/new-york-v4/ui/field"
import { AppearanceSettings } from "./appearance-settings" import { AppearanceSettings } from "./appearance-settings"
import { ButtonGroupDemo } from "./button-group-demo" import { ButtonGroupDemo } from "./button-group-demo"

View File

@@ -1,19 +1,20 @@
"use client" "use client"
import * as React from "react" import * as React from "react"
import { IconInfoCircle, IconStar } from "@tabler/icons-react"
import { import {
InputGroup, InputGroup,
InputGroupAddon, InputGroupAddon,
InputGroupButton, InputGroupButton,
InputGroupInput, InputGroupInput,
} from "@/examples/radix/ui/input-group" } from "@/registry/new-york-v4/ui/input-group"
import { Label } from "@/examples/radix/ui/label" import { Label } from "@/registry/new-york-v4/ui/label"
import { import {
Popover, Popover,
PopoverContent, PopoverContent,
PopoverTrigger, PopoverTrigger,
} from "@/examples/radix/ui/popover" } from "@/registry/new-york-v4/ui/popover"
import { IconInfoCircle, IconStar } from "@tabler/icons-react"
export function InputGroupButtonExample() { export function InputGroupButtonExample() {
const [isFavorite, setIsFavorite] = React.useState(false) const [isFavorite, setIsFavorite] = React.useState(false)

View File

@@ -1,9 +1,12 @@
import { IconCheck, IconInfoCircle, IconPlus } from "@tabler/icons-react"
import { ArrowUpIcon, Search } from "lucide-react"
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/examples/radix/ui/dropdown-menu" } from "@/registry/new-york-v4/ui/dropdown-menu"
import { import {
InputGroup, InputGroup,
InputGroupAddon, InputGroupAddon,
@@ -11,15 +14,13 @@ import {
InputGroupInput, InputGroupInput,
InputGroupText, InputGroupText,
InputGroupTextarea, InputGroupTextarea,
} from "@/examples/radix/ui/input-group" } from "@/registry/new-york-v4/ui/input-group"
import { Separator } from "@/examples/radix/ui/separator" import { Separator } from "@/registry/new-york-v4/ui/separator"
import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
TooltipTrigger, TooltipTrigger,
} from "@/examples/radix/ui/tooltip" } from "@/registry/new-york-v4/ui/tooltip"
import { IconCheck, IconInfoCircle, IconPlus } from "@tabler/icons-react"
import { ArrowUpIcon, Search } from "lucide-react"
export function InputGroupDemo() { export function InputGroupDemo() {
return ( return (
@@ -66,7 +67,11 @@ export function InputGroupDemo() {
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<InputGroupButton variant="ghost">Auto</InputGroupButton> <InputGroupButton variant="ghost">Auto</InputGroupButton>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent side="top" align="start"> <DropdownMenuContent
side="top"
align="start"
className="[--radius:0.95rem]"
>
<DropdownMenuItem>Auto</DropdownMenuItem> <DropdownMenuItem>Auto</DropdownMenuItem>
<DropdownMenuItem>Agent</DropdownMenuItem> <DropdownMenuItem>Agent</DropdownMenuItem>
<DropdownMenuItem>Manual</DropdownMenuItem> <DropdownMenuItem>Manual</DropdownMenuItem>

View File

@@ -1,4 +1,6 @@
import { Button } from "@/examples/radix/ui/button" import { BadgeCheckIcon, ChevronRightIcon } from "lucide-react"
import { Button } from "@/registry/new-york-v4/ui/button"
import { import {
Item, Item,
ItemActions, ItemActions,
@@ -6,8 +8,7 @@ import {
ItemDescription, ItemDescription,
ItemMedia, ItemMedia,
ItemTitle, ItemTitle,
} from "@/examples/radix/ui/item" } from "@/registry/new-york-v4/ui/item"
import { BadgeCheckIcon, ChevronRightIcon } from "lucide-react"
export function ItemDemo() { export function ItemDemo() {
return ( return (

View File

@@ -1,8 +1,24 @@
"use client" "use client"
import { useMemo, useState } from "react" import { useMemo, useState } from "react"
import { Avatar, AvatarFallback, AvatarImage } from "@/examples/radix/ui/avatar" import {
import { Badge } from "@/examples/radix/ui/badge" 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 { import {
Command, Command,
CommandEmpty, CommandEmpty,
@@ -10,7 +26,7 @@ import {
CommandInput, CommandInput,
CommandItem, CommandItem,
CommandList, CommandList,
} from "@/examples/radix/ui/command" } from "@/registry/new-york-v4/ui/command"
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuCheckboxItem, DropdownMenuCheckboxItem,
@@ -23,36 +39,25 @@ import {
DropdownMenuSubContent, DropdownMenuSubContent,
DropdownMenuSubTrigger, DropdownMenuSubTrigger,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/examples/radix/ui/dropdown-menu" } from "@/registry/new-york-v4/ui/dropdown-menu"
import { Field, FieldLabel } from "@/examples/radix/ui/field" import { Field, FieldLabel } from "@/registry/new-york-v4/ui/field"
import { import {
InputGroup, InputGroup,
InputGroupAddon, InputGroupAddon,
InputGroupButton, InputGroupButton,
InputGroupTextarea, InputGroupTextarea,
} from "@/examples/radix/ui/input-group" } from "@/registry/new-york-v4/ui/input-group"
import { import {
Popover, Popover,
PopoverContent, PopoverContent,
PopoverTrigger, PopoverTrigger,
} from "@/examples/radix/ui/popover" } from "@/registry/new-york-v4/ui/popover"
import { Switch } from "@/examples/radix/ui/switch" import { Switch } from "@/registry/new-york-v4/ui/switch"
import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
TooltipTrigger, TooltipTrigger,
} from "@/examples/radix/ui/tooltip" } from "@/registry/new-york-v4/ui/tooltip"
import {
IconApps,
IconArrowUp,
IconAt,
IconBook,
IconCircleDashedPlus,
IconPaperclip,
IconPlus,
IconWorld,
IconX,
} from "@tabler/icons-react"
const SAMPLE_DATA = { const SAMPLE_DATA = {
mentionable: [ mentionable: [
@@ -185,7 +190,7 @@ export function NotionPromptForm() {
const hasMentions = mentions.length > 0 const hasMentions = mentions.length > 0
return ( return (
<form> <form className="[--radius:1.2rem]">
<Field> <Field>
<FieldLabel htmlFor="notion-prompt" className="sr-only"> <FieldLabel htmlFor="notion-prompt" className="sr-only">
Prompt Prompt
@@ -217,7 +222,7 @@ export function NotionPromptForm() {
</TooltipTrigger> </TooltipTrigger>
<TooltipContent>Mention a person, page, or date</TooltipContent> <TooltipContent>Mention a person, page, or date</TooltipContent>
</Tooltip> </Tooltip>
<PopoverContent className="p-0" align="start"> <PopoverContent className="p-0 [--radius:1.2rem]" align="start">
<Command> <Command>
<CommandInput placeholder="Search pages..." /> <CommandInput placeholder="Search pages..." />
<CommandList> <CommandList>
@@ -301,8 +306,12 @@ export function NotionPromptForm() {
</TooltipTrigger> </TooltipTrigger>
<TooltipContent>Select AI model</TooltipContent> <TooltipContent>Select AI model</TooltipContent>
</Tooltip> </Tooltip>
<DropdownMenuContent side="top" align="start" className="w-48"> <DropdownMenuContent
<DropdownMenuGroup className="w-48"> side="top"
align="start"
className="[--radius:1rem]"
>
<DropdownMenuGroup className="w-42">
<DropdownMenuLabel className="text-muted-foreground text-xs"> <DropdownMenuLabel className="text-muted-foreground text-xs">
Select Agent Mode Select Agent Mode
</DropdownMenuLabel> </DropdownMenuLabel>
@@ -337,7 +346,11 @@ export function NotionPromptForm() {
<IconWorld /> All Sources <IconWorld /> All Sources
</InputGroupButton> </InputGroupButton>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent side="top" align="end" className="w-72"> <DropdownMenuContent
side="top"
align="end"
className="[--radius:1rem]"
>
<DropdownMenuGroup> <DropdownMenuGroup>
<DropdownMenuItem <DropdownMenuItem
asChild asChild

View File

@@ -1,5 +1,5 @@
import { Badge } from "@/examples/radix/ui/badge" import { Badge } from "@/registry/new-york-v4/ui/badge"
import { Spinner } from "@/examples/radix/ui/spinner" import { Spinner } from "@/registry/new-york-v4/ui/spinner"
export function SpinnerBadge() { export function SpinnerBadge() {
return ( return (

View File

@@ -1,4 +1,4 @@
import { Button } from "@/examples/radix/ui/button" import { Button } from "@/registry/new-york-v4/ui/button"
import { import {
Empty, Empty,
EmptyContent, EmptyContent,
@@ -6,8 +6,8 @@ import {
EmptyHeader, EmptyHeader,
EmptyMedia, EmptyMedia,
EmptyTitle, EmptyTitle,
} from "@/examples/radix/ui/empty" } from "@/registry/new-york-v4/ui/empty"
import { Spinner } from "@/examples/radix/ui/spinner" import { Spinner } from "@/registry/new-york-v4/ui/spinner"
export function SpinnerEmpty() { export function SpinnerEmpty() {
return ( return (

View File

@@ -1,6 +1,8 @@
import { type Metadata } from "next" import { type Metadata } from "next"
import Image from "next/image" import Image from "next/image"
import Link from "next/link" import Link from "next/link"
import { PlusSignIcon } from "@hugeicons/core-free-icons"
import { HugeiconsIcon } from "@hugeicons/react"
import { Announcement } from "@/components/announcement" import { Announcement } from "@/components/announcement"
import { ExamplesNav } from "@/components/examples-nav" import { ExamplesNav } from "@/components/examples-nav"
@@ -56,7 +58,10 @@ export default function IndexPage() {
<PageHeaderDescription>{description}</PageHeaderDescription> <PageHeaderDescription>{description}</PageHeaderDescription>
<PageActions> <PageActions>
<Button asChild size="sm" className="h-[31px] rounded-lg"> <Button asChild size="sm" className="h-[31px] rounded-lg">
<Link href="/docs/installation">Get Started</Link> <Link href="/create">
<HugeiconsIcon icon={PlusSignIcon} />
New Project
</Link>
</Button> </Button>
<Button asChild size="sm" variant="ghost" className="rounded-lg"> <Button asChild size="sm" variant="ghost" className="rounded-lg">
<Link href="/docs/components">View Components</Link> <Link href="/docs/components">View Components</Link>

View File

@@ -2,11 +2,7 @@ import * as React from "react"
import { notFound } from "next/navigation" import { notFound } from "next/navigation"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { import { ChartDisplay } from "@/components/chart-display"
ChartDisplay,
getCachedRegistryItem,
getChartHighlightedCode,
} from "@/components/chart-display"
import { getActiveStyle } from "@/registry/_legacy-styles" import { getActiveStyle } from "@/registry/_legacy-styles"
import { charts } from "@/app/(app)/charts/charts" import { charts } from "@/app/(app)/charts/charts"
@@ -48,26 +44,6 @@ export default async function ChartPage({ params }: ChartPageProps) {
const chartList = charts[chartType] const chartList = charts[chartType]
const activeStyle = await getActiveStyle() const activeStyle = await getActiveStyle()
// Prefetch all chart data in parallel for better performance.
// Charts are rendered via iframes, so we only need the metadata and highlighted code.
const chartDataPromises = chartList.map(async (chart) => {
const registryItem = await getCachedRegistryItem(chart.id, activeStyle.name)
if (!registryItem) return null
const highlightedCode = await getChartHighlightedCode(
registryItem.files?.[0]?.content ?? ""
)
if (!highlightedCode) return null
return {
...registryItem,
highlightedCode,
fullWidth: chart.fullWidth,
}
})
const prefetchedCharts = await Promise.all(chartDataPromises)
return ( return (
<div className="grid flex-1 gap-12 lg:gap-24"> <div className="grid flex-1 gap-12 lg:gap-24">
<h2 className="sr-only"> <h2 className="sr-only">
@@ -75,14 +51,16 @@ export default async function ChartPage({ params }: ChartPageProps) {
</h2> </h2>
<div className="grid flex-1 scroll-mt-20 items-stretch gap-10 md:grid-cols-2 md:gap-6 lg:grid-cols-3 xl:gap-10"> <div className="grid flex-1 scroll-mt-20 items-stretch gap-10 md:grid-cols-2 md:gap-6 lg:grid-cols-3 xl:gap-10">
{Array.from({ length: 12 }).map((_, index) => { {Array.from({ length: 12 }).map((_, index) => {
const chart = prefetchedCharts[index] const chart = chartList[index]
return chart ? ( return chart ? (
<ChartDisplay <ChartDisplay
key={chart.name} key={chart.id}
chart={chart} name={chart.id}
style={activeStyle.name} styleName={activeStyle.name}
className={cn(chart.fullWidth && "md:col-span-2 lg:col-span-3")} className={cn(chart.fullWidth && "md:col-span-2 lg:col-span-3")}
/> >
<chart.component />
</ChartDisplay>
) : ( ) : (
<div <div
key={`empty-${index}`} key={`empty-${index}`}

View File

@@ -63,8 +63,9 @@ export default function ChartsLayout({
</PageHeader> </PageHeader>
<PageNav id="charts"> <PageNav id="charts">
<ChartsNav /> <ChartsNav />
<ThemeSelector className="mr-4 hidden md:flex" />
</PageNav> </PageNav>
<div className="container-wrapper flex-1"> <div className="container-wrapper section-soft flex-1">
<div className="container pb-6"> <div className="container pb-6">
<section className="theme-container">{children}</section> <section className="theme-container">{children}</section>
</div> </div>

View File

@@ -1,15 +1,21 @@
import Link from "next/link" import Link from "next/link"
import { notFound } from "next/navigation" import { notFound } from "next/navigation"
import { mdxComponents } from "@/mdx-components" import { mdxComponents } from "@/mdx-components"
import { IconArrowLeft, IconArrowRight } from "@tabler/icons-react" import {
IconArrowLeft,
IconArrowRight,
IconArrowUpRight,
} from "@tabler/icons-react"
import fm from "front-matter"
import { findNeighbour } from "fumadocs-core/page-tree" import { findNeighbour } from "fumadocs-core/page-tree"
import z from "zod"
import { source } from "@/lib/source" import { source } from "@/lib/source"
import { absoluteUrl } from "@/lib/utils" import { absoluteUrl } from "@/lib/utils"
import { DocsBaseSwitcher } from "@/components/docs-base-switcher"
import { DocsCopyPage } from "@/components/docs-copy-page" import { DocsCopyPage } from "@/components/docs-copy-page"
import { DocsTableOfContents } from "@/components/docs-toc" import { DocsTableOfContents } from "@/components/docs-toc"
import { OpenInV0Cta } from "@/components/open-in-v0-cta" import { OpenInV0Cta } from "@/components/open-in-v0-cta"
import { Badge } from "@/registry/new-york-v4/ui/badge"
import { Button } from "@/registry/new-york-v4/ui/button" import { Button } from "@/registry/new-york-v4/ui/button"
export const revalidate = false export const revalidate = false
@@ -79,116 +85,127 @@ export default async function Page(props: {
const doc = page.data const doc = page.data
const MDX = doc.body const MDX = doc.body
const isChangelog = params.slug?.[0] === "changelog" const neighbours = findNeighbour(source.pageTree, page.url)
const neighbours = isChangelog
? { previous: null, next: null }
: findNeighbour(source.pageTree, page.url)
const raw = await page.data.getText("raw") const raw = await page.data.getText("raw")
const { attributes } = fm(raw)
const { links } = z
.object({
links: z
.object({
doc: z.string().optional(),
api: z.string().optional(),
})
.optional(),
})
.parse(attributes)
return ( return (
<div <div className="flex items-stretch text-[1.05rem] sm:text-[15px] xl:w-full">
data-slot="docs"
className="flex scroll-mt-24 items-stretch pb-8 text-[1.05rem] sm:text-[15px] xl:w-full"
>
<div className="flex min-w-0 flex-1 flex-col"> <div className="flex min-w-0 flex-1 flex-col">
<div className="h-(--top-spacing) shrink-0" /> <div className="h-(--top-spacing) shrink-0" />
<div className="mx-auto flex w-full max-w-[40rem] min-w-0 flex-1 flex-col gap-6 px-4 py-6 text-neutral-800 md:px-0 lg:py-8 dark:text-neutral-300"> <div className="mx-auto flex w-full max-w-2xl min-w-0 flex-1 flex-col gap-8 px-4 py-6 text-neutral-800 md:px-0 lg:py-8 dark:text-neutral-300">
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<div className="flex items-center justify-between md:items-start"> <div className="flex items-start justify-between">
<h1 className="scroll-m-24 text-3xl font-semibold tracking-tight sm:text-3xl"> <h1 className="scroll-m-20 text-4xl font-semibold tracking-tight sm:text-3xl xl:text-4xl">
{doc.title} {doc.title}
</h1> </h1>
<div className="docs-nav flex items-center gap-2"> <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">
<div className="hidden sm:block"> <DocsCopyPage page={raw} url={absoluteUrl(page.url)} />
<DocsCopyPage page={raw} url={absoluteUrl(page.url)} /> {neighbours.previous && (
</div> <Button
<div className="ml-auto flex gap-2"> variant="secondary"
{neighbours.previous && ( size="icon"
<Button className="extend-touch-target ml-auto size-8 shadow-none md:size-7"
variant="secondary" asChild
size="icon" >
className="extend-touch-target size-8 shadow-none md:size-7" <Link href={neighbours.previous.url}>
asChild <IconArrowLeft />
> <span className="sr-only">Previous</span>
<Link href={neighbours.previous.url}> </Link>
<IconArrowLeft /> </Button>
<span className="sr-only">Previous</span> )}
</Link> {neighbours.next && (
</Button> <Button
)} variant="secondary"
{neighbours.next && ( size="icon"
<Button className="extend-touch-target size-8 shadow-none md:size-7"
variant="secondary" asChild
size="icon" >
className="extend-touch-target size-8 shadow-none md:size-7" <Link href={neighbours.next.url}>
asChild <span className="sr-only">Next</span>
> <IconArrowRight />
<Link href={neighbours.next.url}> </Link>
<span className="sr-only">Next</span> </Button>
<IconArrowRight /> )}
</Link>
</Button>
)}
</div>
</div> </div>
</div> </div>
{doc.description && ( {doc.description && (
<p className="text-muted-foreground text-[1.05rem] sm:text-base sm:text-balance md:max-w-[80%]"> <p className="text-muted-foreground text-[1.05rem] text-balance sm:text-base">
{doc.description} {doc.description}
</p> </p>
)} )}
</div> </div>
{links ? (
<div className="flex items-center gap-2 pt-4">
{links?.doc && (
<Badge asChild variant="secondary" className="rounded-full">
<a href={links.doc} target="_blank" rel="noreferrer">
Docs <IconArrowUpRight />
</a>
</Badge>
)}
{links?.api && (
<Badge asChild variant="secondary" className="rounded-full">
<a href={links.api} target="_blank" rel="noreferrer">
API Reference <IconArrowUpRight />
</a>
</Badge>
)}
</div>
) : null}
</div> </div>
<div className="w-full flex-1 pb-16 *:data-[slot=alert]:first:mt-0 sm:pb-0"> <div className="w-full flex-1 *:data-[slot=alert]:first:mt-0">
{params.slug &&
params.slug[0] === "components" &&
params.slug[1] &&
params.slug[2] && (
<DocsBaseSwitcher
base={params.slug[1]}
component={params.slug[2]}
className="mb-4"
/>
)}
<MDX components={mdxComponents} /> <MDX components={mdxComponents} />
</div> </div>
<div className="hidden h-16 w-full items-center gap-2 px-4 sm:flex sm:px-0"> </div>
{neighbours.previous && ( <div className="mx-auto hidden h-16 w-full max-w-2xl items-center gap-2 px-4 sm:flex md:px-0">
<Button {neighbours.previous && (
variant="secondary" <Button
size="sm" variant="secondary"
asChild size="sm"
className="shadow-none" asChild
> className="shadow-none"
<Link href={neighbours.previous.url}> >
<IconArrowLeft /> {neighbours.previous.name} <Link href={neighbours.previous.url}>
</Link> <IconArrowLeft /> {neighbours.previous.name}
</Button> </Link>
)} </Button>
{neighbours.next && ( )}
<Button {neighbours.next && (
variant="secondary" <Button
size="sm" variant="secondary"
className="ml-auto shadow-none" size="sm"
asChild className="ml-auto shadow-none"
> asChild
<Link href={neighbours.next.url}> >
{neighbours.next.name} <IconArrowRight /> <Link href={neighbours.next.url}>
</Link> {neighbours.next.name} <IconArrowRight />
</Button> </Link>
)} </Button>
</div> )}
</div> </div>
</div> </div>
<div className="sticky top-[calc(var(--header-height)+1px)] z-30 ml-auto hidden h-[90svh] w-(--sidebar-width) flex-col gap-4 overflow-hidden overscroll-none pb-8 xl:flex"> <div className="sticky top-[calc(var(--header-height)+1px)] z-30 ml-auto hidden h-[calc(100svh-var(--footer-height)+2rem)] w-72 flex-col gap-4 overflow-hidden overscroll-none pb-8 xl:flex">
<div className="h-(--top-spacing) shrink-0"></div> <div className="h-(--top-spacing) shrink-0" />
{doc.toc?.length ? ( {doc.toc?.length ? (
<div className="no-scrollbar flex flex-col gap-8 overflow-y-auto px-8"> <div className="no-scrollbar overflow-y-auto px-8">
<DocsTableOfContents toc={doc.toc} /> <DocsTableOfContents toc={doc.toc} />
<div className="h-12" />
</div> </div>
) : null} ) : null}
<div className="hidden flex-1 flex-col gap-6 px-6 xl:flex"> <div className="flex flex-1 flex-col gap-12 px-6">
<OpenInV0Cta /> <OpenInV0Cta />
</div> </div>
</div> </div>

View File

@@ -1,144 +0,0 @@
import Link from "next/link"
import { Button } from "@/examples/radix/ui/button"
import { mdxComponents } from "@/mdx-components"
import { IconRss } from "@tabler/icons-react"
import { getChangelogPages, type ChangelogPageData } from "@/lib/changelog"
import { absoluteUrl } from "@/lib/utils"
import { OpenInV0Cta } from "@/components/open-in-v0-cta"
export const revalidate = false
export const dynamic = "force-static"
export function generateMetadata() {
return {
title: "Changelog",
description: "Latest updates and announcements.",
openGraph: {
title: "Changelog",
description: "Latest updates and announcements.",
type: "article",
url: absoluteUrl("/docs/changelog"),
images: [
{
url: `/og?title=${encodeURIComponent(
"Changelog"
)}&description=${encodeURIComponent(
"Latest updates and announcements."
)}`,
},
],
},
}
}
export default function ChangelogPage() {
const pages = getChangelogPages()
const latestPages = pages.slice(0, 5)
const olderPages = pages.slice(5)
return (
<div
data-slot="docs"
className="flex scroll-mt-24 items-stretch pb-8 text-[1.05rem] sm:text-[15px] xl:w-full"
>
<div className="flex min-w-0 flex-1 flex-col">
<div className="h-(--top-spacing) shrink-0" />
<div className="mx-auto flex w-full max-w-[40rem] min-w-0 flex-1 flex-col gap-6 px-4 py-6 text-neutral-800 md:px-0 lg:py-8 dark:text-neutral-300">
<div className="flex flex-col gap-2">
<div className="flex items-center justify-between">
<h1 className="scroll-m-24 text-4xl font-semibold tracking-tight sm:text-3xl">
Changelog
</h1>
<Button variant="secondary" size="sm" asChild>
<a href="/rss.xml" target="_blank" rel="noopener noreferrer">
<IconRss />
RSS
</a>
</Button>
</div>
<p className="text-muted-foreground text-[1.05rem] sm:text-base sm:text-balance md:max-w-[80%]">
Latest updates and announcements.
</p>
</div>
<div className="w-full flex-1 pb-16 sm:pb-0">
{latestPages.map((page) => {
const data = page.data as ChangelogPageData
const MDX = page.data.body
return (
<article key={page.url} className="mb-12 border-b pb-12">
<h2 className="font-heading text-xl font-semibold tracking-tight">
{data.title}
</h2>
<div className="prose-changelog mt-6 *:first:mt-0">
<MDX components={mdxComponents} />
</div>
</article>
)
})}
{olderPages.length > 0 && (
<div id="more-updates" className="mb-24 scroll-mt-24">
<h2 className="font-heading mb-6 text-xl font-semibold tracking-tight">
More Updates
</h2>
<div className="grid auto-rows-fr gap-3 sm:grid-cols-2">
{olderPages.map((page) => {
const data = page.data as ChangelogPageData
const [date, ...titleParts] = data.title.split(" - ")
const title = titleParts.join(" - ")
return (
<Link
key={page.url}
href={page.url}
className="bg-surface text-surface-foreground hover:bg-surface/80 flex w-full flex-col rounded-xl px-4 py-3 transition-colors"
>
<span className="text-muted-foreground text-xs">
{date}
</span>
<span className="text-sm font-medium">{title}</span>
</Link>
)
})}
</div>
</div>
)}
</div>
</div>
</div>
<div className="sticky top-[calc(var(--header-height)+1px)] z-30 ml-auto hidden h-[90svh] w-72 flex-col gap-4 overflow-hidden overscroll-none pb-8 lg:flex">
<div className="h-(--top-spacing) shrink-0"></div>
<div className="no-scrollbar flex flex-col gap-8 overflow-y-auto px-8">
<div className="flex flex-col gap-2 p-4 pt-0 text-sm">
<p className="text-muted-foreground bg-background sticky top-0 h-6 text-xs font-medium">
On This Page
</p>
{latestPages.map((page) => {
const data = page.data as ChangelogPageData
return (
<Link
key={page.url}
href={page.url}
className="text-muted-foreground hover:text-foreground text-[0.8rem] no-underline transition-colors"
>
{data.title}
</Link>
)
})}
{olderPages.length > 0 && (
<a
href="#more-updates"
className="text-muted-foreground hover:text-foreground text-[0.8rem] no-underline transition-colors"
>
More Updates
</a>
)}
</div>
</div>
<div className="hidden flex-1 flex-col gap-6 px-6 xl:flex">
<OpenInV0Cta />
</div>
</div>
</div>
)
}

View File

@@ -9,14 +9,7 @@ export default function DocsLayout({
}) { }) {
return ( return (
<div className="container-wrapper flex flex-1 flex-col px-2"> <div className="container-wrapper flex flex-1 flex-col px-2">
<SidebarProvider <SidebarProvider className="3xl:fixed:container 3xl:fixed:px-3 min-h-min flex-1 items-start px-0 [--sidebar-width:220px] [--top-spacing:0] lg:grid lg:grid-cols-[var(--sidebar-width)_minmax(0,1fr)] lg:[--sidebar-width:240px] lg:[--top-spacing:calc(var(--spacing)*4)]">
className="3xl:fixed:container 3xl:fixed:px-3 min-h-min flex-1 items-start px-0 [--top-spacing:0] lg:grid lg:grid-cols-[var(--sidebar-width)_minmax(0,1fr)] lg:[--top-spacing:calc(var(--spacing)*4)]"
style={
{
"--sidebar-width": "calc(var(--spacing) * 72)",
} as React.CSSProperties
}
>
<DocsSidebar tree={source.pageTree} /> <DocsSidebar tree={source.pageTree} />
<div className="h-full w-full">{children}</div> <div className="h-full w-full">{children}</div>
</SidebarProvider> </SidebarProvider>

View File

@@ -70,7 +70,7 @@ export default function ExamplesLayout({
</PageNav> </PageNav>
<div className="container-wrapper section-soft flex flex-1 flex-col pb-6"> <div className="container-wrapper section-soft flex flex-1 flex-col pb-6">
<div className="theme-container container flex flex-1 scroll-mt-20 flex-col"> <div className="theme-container container flex flex-1 scroll-mt-20 flex-col">
<div className="bg-background flex flex-col overflow-hidden rounded-lg border bg-clip-padding has-[[data-slot=rtl-components]]:overflow-visible has-[[data-slot=rtl-components]]:border-0 has-[[data-slot=rtl-components]]:bg-transparent md:flex-1 xl:rounded-xl"> <div className="bg-background flex flex-col overflow-hidden rounded-lg border bg-clip-padding md:flex-1 xl:rounded-xl">
{children} {children}
</div> </div>
</div> </div>

View File

@@ -1,7 +1,7 @@
"use client" "use client"
import * as React from "react" import * as React from "react"
import type { Slider as SliderPrimitive } from "radix-ui" import { type SliderProps } from "@radix-ui/react-slider"
import { import {
HoverCard, HoverCard,
@@ -12,9 +12,7 @@ import { Label } from "@/registry/new-york-v4/ui/label"
import { Slider } from "@/registry/new-york-v4/ui/slider" import { Slider } from "@/registry/new-york-v4/ui/slider"
interface MaxLengthSelectorProps { interface MaxLengthSelectorProps {
defaultValue: React.ComponentProps< defaultValue: SliderProps["defaultValue"]
typeof SliderPrimitive.Root
>["defaultValue"]
} }
export function MaxLengthSelector({ defaultValue }: MaxLengthSelectorProps) { export function MaxLengthSelector({ defaultValue }: MaxLengthSelectorProps) {

View File

@@ -1,8 +1,8 @@
"use client" "use client"
import * as React from "react" import * as React from "react"
import { type PopoverProps } from "@radix-ui/react-popover"
import { Check, ChevronsUpDown } from "lucide-react" import { Check, ChevronsUpDown } from "lucide-react"
import type { Popover as PopoverPrimitive } from "radix-ui"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { useMutationObserver } from "@/hooks/use-mutation-observer" import { useMutationObserver } from "@/hooks/use-mutation-observer"
@@ -29,8 +29,7 @@ import {
import { type Model, type ModelType } from "../data/models" import { type Model, type ModelType } from "../data/models"
interface ModelSelectorProps interface ModelSelectorProps extends PopoverProps {
extends React.ComponentProps<typeof PopoverPrimitive.Root> {
types: readonly ModelType[] types: readonly ModelType[]
models: Model[] models: Model[]
} }

View File

@@ -1,6 +1,7 @@
"use client" "use client"
import * as React from "react" import * as React from "react"
import { Dialog } from "@radix-ui/react-dialog"
import { MoreHorizontal } from "lucide-react" import { MoreHorizontal } from "lucide-react"
import { toast } from "sonner" import { toast } from "sonner"
@@ -15,7 +16,6 @@ import {
} from "@/registry/new-york-v4/ui/alert-dialog" } from "@/registry/new-york-v4/ui/alert-dialog"
import { Button } from "@/registry/new-york-v4/ui/button" import { Button } from "@/registry/new-york-v4/ui/button"
import { import {
Dialog,
DialogClose, DialogClose,
DialogContent, DialogContent,
DialogDescription, DialogDescription,

View File

@@ -1,8 +1,8 @@
"use client" "use client"
import * as React from "react" import * as React from "react"
import { type PopoverProps } from "@radix-ui/react-popover"
import { Check, ChevronsUpDown } from "lucide-react" import { Check, ChevronsUpDown } from "lucide-react"
import type { Popover as PopoverPrimitive } from "radix-ui"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { Button } from "@/registry/new-york-v4/ui/button" import { Button } from "@/registry/new-york-v4/ui/button"
@@ -23,8 +23,7 @@ import {
import { type Preset } from "../data/presets" import { type Preset } from "../data/presets"
interface PresetSelectorProps interface PresetSelectorProps extends PopoverProps {
extends React.ComponentProps<typeof PopoverPrimitive.Root> {
presets: Preset[] presets: Preset[]
} }

View File

@@ -1,7 +1,7 @@
"use client" "use client"
import * as React from "react" import * as React from "react"
import type { Slider as SliderPrimitive } from "radix-ui" import { type SliderProps } from "@radix-ui/react-slider"
import { import {
HoverCard, HoverCard,
@@ -12,9 +12,7 @@ import { Label } from "@/registry/new-york-v4/ui/label"
import { Slider } from "@/registry/new-york-v4/ui/slider" import { Slider } from "@/registry/new-york-v4/ui/slider"
interface TemperatureSelectorProps { interface TemperatureSelectorProps {
defaultValue: React.ComponentProps< defaultValue: SliderProps["defaultValue"]
typeof SliderPrimitive.Root
>["defaultValue"]
} }
export function TemperatureSelector({ export function TemperatureSelector({

View File

@@ -1,7 +1,7 @@
"use client" "use client"
import * as React from "react" import * as React from "react"
import type { Slider as SliderPrimitive } from "radix-ui" import { type SliderProps } from "@radix-ui/react-slider"
import { import {
HoverCard, HoverCard,
@@ -12,9 +12,7 @@ import { Label } from "@/registry/new-york-v4/ui/label"
import { Slider } from "@/registry/new-york-v4/ui/slider" import { Slider } from "@/registry/new-york-v4/ui/slider"
interface TopPSelectorProps { interface TopPSelectorProps {
defaultValue: React.ComponentProps< defaultValue: SliderProps["defaultValue"]
typeof SliderPrimitive.Root
>["defaultValue"]
} }
export function TopPSelector({ defaultValue }: TopPSelectorProps) { export function TopPSelector({ defaultValue }: TopPSelectorProps) {

View File

@@ -1,170 +0,0 @@
"use client"
import * as React from "react"
import { Button } from "@/examples/base/ui-rtl/button"
import { ButtonGroup } from "@/examples/base/ui-rtl/button-group"
import {
Field,
FieldContent,
FieldDescription,
FieldGroup,
FieldLabel,
FieldLegend,
FieldSeparator,
FieldSet,
FieldTitle,
} from "@/examples/base/ui-rtl/field"
import { Input } from "@/examples/base/ui-rtl/input"
import { RadioGroup, RadioGroupItem } from "@/examples/base/ui-rtl/radio-group"
import { Switch } from "@/examples/base/ui-rtl/switch"
import { IconMinus, IconPlus } from "@tabler/icons-react"
import { useLanguageContext } from "@/components/language-selector"
const translations = {
ar: {
dir: "rtl" as const,
computeEnvironment: "بيئة الحوسبة",
computeDescription: "اختر بيئة الحوسبة لمجموعتك.",
kubernetes: "كوبرنيتس",
kubernetesDescription:
"تشغيل أحمال عمل GPU على مجموعة مُهيأة بـ K8s. هذا هو الافتراضي.",
virtualMachine: "جهاز افتراضي",
vmDescription: "الوصول إلى مجموعة VM مُهيأة لتشغيل أحمال العمل. (قريبًا)",
numberOfGpus: "عدد وحدات GPU",
gpuDescription: "يمكنك إضافة المزيد لاحقًا.",
decrement: "إنقاص",
increment: "زيادة",
wallpaperTinting: "تلوين الخلفية",
wallpaperDescription: "السماح بتلوين الخلفية.",
},
he: {
dir: "rtl" as const,
computeEnvironment: "סביבת מחשוב",
computeDescription: "בחר את סביבת המחשוב לאשכול שלך.",
kubernetes: "קוברנטיס",
kubernetesDescription:
"הפעל עומסי עבודה של GPU באשכול מוגדר K8s. זו ברירת המחדל.",
virtualMachine: "מכונה וירטואלית",
vmDescription: "גש לאשכול VM מוגדר להפעלת עומסי עבודה. (בקרוב)",
numberOfGpus: "מספר GPUs",
gpuDescription: "תוכל להוסיף עוד מאוחר יותר.",
decrement: "הפחת",
increment: "הגדל",
wallpaperTinting: "צביעת טפט",
wallpaperDescription: "אפשר לטפט להיצבע.",
},
}
export function AppearanceSettings() {
const context = useLanguageContext()
const lang = context?.language === "he" ? "he" : "ar"
const t = translations[lang]
const [gpuCount, setGpuCount] = React.useState(8)
const handleGpuAdjustment = React.useCallback((adjustment: number) => {
setGpuCount((prevCount) =>
Math.max(1, Math.min(99, prevCount + adjustment))
)
}, [])
const handleGpuInputChange = React.useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const value = parseInt(e.target.value, 10)
if (!isNaN(value) && value >= 1 && value <= 99) {
setGpuCount(value)
}
},
[]
)
return (
<div dir={t.dir}>
<FieldSet>
<FieldGroup>
<FieldSet>
<FieldLegend>{t.computeEnvironment}</FieldLegend>
<FieldDescription>{t.computeDescription}</FieldDescription>
<RadioGroup defaultValue="kubernetes">
<FieldLabel htmlFor="rtl-kubernetes">
<Field orientation="horizontal">
<FieldContent>
<FieldTitle>{t.kubernetes}</FieldTitle>
<FieldDescription>
{t.kubernetesDescription}
</FieldDescription>
</FieldContent>
<RadioGroupItem
value="kubernetes"
id="rtl-kubernetes"
aria-label={t.kubernetes}
/>
</Field>
</FieldLabel>
<FieldLabel htmlFor="rtl-vm">
<Field orientation="horizontal">
<FieldContent>
<FieldTitle>{t.virtualMachine}</FieldTitle>
<FieldDescription>{t.vmDescription}</FieldDescription>
</FieldContent>
<RadioGroupItem
value="vm"
id="rtl-vm"
aria-label={t.virtualMachine}
/>
</Field>
</FieldLabel>
</RadioGroup>
</FieldSet>
<FieldSeparator />
<Field orientation="horizontal">
<FieldContent>
<FieldLabel htmlFor="rtl-gpu-count">{t.numberOfGpus}</FieldLabel>
<FieldDescription>{t.gpuDescription}</FieldDescription>
</FieldContent>
<ButtonGroup>
<Input
id="rtl-gpu-count"
value={gpuCount}
onChange={handleGpuInputChange}
size={3}
className="h-7 !w-14 font-mono"
maxLength={3}
/>
<Button
variant="outline"
size="icon-sm"
type="button"
aria-label={t.decrement}
onClick={() => handleGpuAdjustment(-1)}
disabled={gpuCount <= 1}
>
<IconMinus />
</Button>
<Button
variant="outline"
size="icon-sm"
type="button"
aria-label={t.increment}
onClick={() => handleGpuAdjustment(1)}
disabled={gpuCount >= 99}
>
<IconPlus />
</Button>
</ButtonGroup>
</Field>
<FieldSeparator />
<Field orientation="horizontal">
<FieldContent>
<FieldLabel htmlFor="rtl-tinting">
{t.wallpaperTinting}
</FieldLabel>
<FieldDescription>{t.wallpaperDescription}</FieldDescription>
</FieldContent>
<Switch id="rtl-tinting" defaultChecked />
</Field>
</FieldGroup>
</FieldSet>
</div>
)
}

View File

@@ -1,179 +0,0 @@
"use client"
import * as React from "react"
import { Button } from "@/examples/base/ui-rtl/button"
import { ButtonGroup } from "@/examples/base/ui-rtl/button-group"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuPortal,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from "@/examples/base/ui-rtl/dropdown-menu"
import {
ArchiveIcon,
ArrowLeftIcon,
CalendarPlusIcon,
ClockIcon,
ListFilterIcon,
MailCheckIcon,
MoreHorizontalIcon,
TagIcon,
Trash2Icon,
} from "lucide-react"
import { useLanguageContext } from "@/components/language-selector"
const translations = {
ar: {
dir: "rtl" as const,
goBack: "رجوع",
archive: "أرشفة",
report: "إبلاغ",
snooze: "تأجيل",
moreOptions: "خيارات أخرى",
markAsRead: "تحديد كمقروء",
addToCalendar: "إضافة إلى التقويم",
addToList: "إضافة إلى القائمة",
labelAs: "تصنيف كـ...",
personal: "شخصي",
work: "عمل",
other: "أخرى",
trash: "حذف",
},
he: {
dir: "rtl" as const,
goBack: "חזור",
archive: "ארכיון",
report: "דווח",
snooze: "נודניק",
moreOptions: "אפשרויות נוספות",
markAsRead: "סמן כנקרא",
addToCalendar: "הוסף ליומן",
addToList: "הוסף לרשימה",
labelAs: "תייג כ...",
personal: "אישי",
work: "עבודה",
other: "אחר",
trash: "מחק",
},
}
export function ButtonGroupDemo() {
const context = useLanguageContext()
const lang = context?.language === "he" ? "he" : "ar"
const t = translations[lang]
const [label, setLabel] = React.useState("personal")
return (
<div dir={t.dir}>
<ButtonGroup>
<ButtonGroup className="hidden sm:flex">
<Button variant="outline" size="icon-sm" aria-label={t.goBack}>
<ArrowLeftIcon className="rtl:rotate-180" />
</Button>
</ButtonGroup>
<ButtonGroup>
<Button variant="outline" size="sm">
{t.archive}
</Button>
<Button variant="outline" size="sm">
{t.report}
</Button>
</ButtonGroup>
<ButtonGroup>
<Button variant="outline" size="sm">
{t.snooze}
</Button>
<DropdownMenu>
<DropdownMenuTrigger
render={
<Button
variant="outline"
size="icon-sm"
aria-label={t.moreOptions}
/>
}
>
<MoreHorizontalIcon />
</DropdownMenuTrigger>
<DropdownMenuContent
align="start"
dir={t.dir}
data-lang={lang}
className="w-44"
>
<DropdownMenuGroup>
<DropdownMenuItem>
<MailCheckIcon />
{t.markAsRead}
</DropdownMenuItem>
<DropdownMenuItem>
<ArchiveIcon />
{t.archive}
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
<ClockIcon />
{t.snooze}
</DropdownMenuItem>
<DropdownMenuItem>
<CalendarPlusIcon />
{t.addToCalendar}
</DropdownMenuItem>
<DropdownMenuItem>
<ListFilterIcon />
{t.addToList}
</DropdownMenuItem>
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<TagIcon />
{t.labelAs}
</DropdownMenuSubTrigger>
<DropdownMenuPortal>
<DropdownMenuSubContent
side="left"
dir={t.dir}
data-lang={lang}
>
<DropdownMenuRadioGroup
value={label}
onValueChange={setLabel}
>
<DropdownMenuRadioItem value="personal">
{t.personal}
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="work">
{t.work}
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="other">
{t.other}
</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
</DropdownMenuSubContent>
</DropdownMenuPortal>
</DropdownMenuSub>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem variant="destructive">
<Trash2Icon />
{t.trash}
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
</ButtonGroup>
</ButtonGroup>
</div>
)
}

View File

@@ -1,82 +0,0 @@
"use client"
import * as React from "react"
import { Button } from "@/examples/base/ui-rtl/button"
import { ButtonGroup } from "@/examples/base/ui-rtl/button-group"
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
} from "@/examples/base/ui-rtl/input-group"
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/examples/base/ui-rtl/tooltip"
import { AudioLinesIcon, PlusIcon } from "lucide-react"
import { useLanguageContext } from "@/components/language-selector"
const translations = {
ar: {
dir: "rtl" as const,
add: "إضافة",
voicePlaceholder: "سجل وأرسل صوتًا...",
messagePlaceholder: "أرسل رسالة...",
voiceMode: "الوضع الصوتي",
},
he: {
dir: "rtl" as const,
add: "הוסף",
voicePlaceholder: "הקלט ושלח אודיו...",
messagePlaceholder: "שלח הודעה...",
voiceMode: "מצב קולי",
},
}
export function ButtonGroupInputGroup() {
const context = useLanguageContext()
const lang = context?.language === "he" ? "he" : "ar"
const t = translations[lang]
const [voiceEnabled, setVoiceEnabled] = React.useState(false)
return (
<ButtonGroup dir={t.dir}>
<ButtonGroup>
<Button variant="outline" size="icon" aria-label={t.add}>
<PlusIcon />
</Button>
</ButtonGroup>
<ButtonGroup className="flex-1">
<InputGroup>
<InputGroupInput
placeholder={
voiceEnabled ? t.voicePlaceholder : t.messagePlaceholder
}
disabled={voiceEnabled}
/>
<InputGroupAddon align="inline-end">
<Tooltip>
<TooltipTrigger
render={
<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={t.voiceMode}
/>
}
>
<AudioLinesIcon />
</TooltipTrigger>
<TooltipContent>{t.voiceMode}</TooltipContent>
</Tooltip>
</InputGroupAddon>
</InputGroup>
</ButtonGroup>
</ButtonGroup>
)
}

View File

@@ -1,56 +0,0 @@
"use client"
import { Button } from "@/examples/base/ui-rtl/button"
import { ButtonGroup } from "@/examples/base/ui-rtl/button-group"
import { ArrowLeftIcon, ArrowRightIcon } from "lucide-react"
import { useLanguageContext } from "@/components/language-selector"
const translations = {
ar: {
dir: "rtl" as const,
locale: "ar-SA",
previous: "السابق",
next: "التالي",
},
he: {
dir: "rtl" as const,
locale: "he-IL",
previous: "הקודם",
next: "הבא",
},
}
function formatNumber(value: number, locale: string) {
return new Intl.NumberFormat(locale).format(value)
}
export function ButtonGroupNested() {
const context = useLanguageContext()
const lang = context?.language === "he" ? "he" : "ar"
const t = translations[lang]
return (
<ButtonGroup dir={t.dir}>
<ButtonGroup>
<Button variant="outline" size="sm">
{formatNumber(1, t.locale)}
</Button>
<Button variant="outline" size="sm">
{formatNumber(2, t.locale)}
</Button>
<Button variant="outline" size="sm">
{formatNumber(3, t.locale)}
</Button>
</ButtonGroup>
<ButtonGroup>
<Button variant="outline" size="icon-sm" aria-label={t.previous}>
<ArrowLeftIcon className="rtl:rotate-180" />
</Button>
<Button variant="outline" size="icon-sm" aria-label={t.next}>
<ArrowRightIcon className="rtl:rotate-180" />
</Button>
</ButtonGroup>
</ButtonGroup>
)
}

View File

@@ -1,83 +0,0 @@
"use client"
import { Button } from "@/examples/base/ui-rtl/button"
import { ButtonGroup } from "@/examples/base/ui-rtl/button-group"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/examples/base/ui-rtl/popover"
import { Separator } from "@/examples/base/ui-rtl/separator"
import { Textarea } from "@/examples/base/ui-rtl/textarea"
import { BotIcon, ChevronDownIcon } from "lucide-react"
import { useLanguageContext } from "@/components/language-selector"
const translations = {
ar: {
dir: "rtl" as const,
copilot: "المساعد",
openPopover: "فتح القائمة",
agentTasks: "مهام الوكيل",
placeholder: "صف مهمتك بلغة طبيعية.",
startTask: "ابدأ مهمة جديدة مع المساعد",
description:
"صف مهمتك بلغة طبيعية. سيعمل المساعد في الخلفية ويفتح طلب سحب لمراجعتك.",
},
he: {
dir: "rtl" as const,
copilot: "עוזר",
openPopover: "פתח תפריט",
agentTasks: "משימות סוכן",
placeholder: "תאר את המשימה שלך בשפה טבעית.",
startTask: "התחל משימה חדשה עם העוזר",
description:
"תאר את המשימה שלך בשפה טבעית. העוזר יעבוד ברקע ויפתח בקשת משיכה לבדיקתך.",
},
}
export function ButtonGroupPopover() {
const context = useLanguageContext()
const lang = context?.language === "he" ? "he" : "ar"
const t = translations[lang]
return (
<ButtonGroup dir={t.dir}>
<Button variant="outline" size="sm">
<BotIcon /> {t.copilot}
</Button>
<Popover>
<PopoverTrigger
render={
<Button
variant="outline"
size="icon-sm"
aria-label={t.openPopover}
/>
}
>
<ChevronDownIcon />
</PopoverTrigger>
<PopoverContent
align="start"
dir={t.dir}
data-lang={lang}
className="p-0"
>
<div className="px-4 py-3">
<div className="text-sm font-medium">{t.agentTasks}</div>
</div>
<Separator />
<div className="p-4 text-sm *:[p:not(:last-child)]:mb-2">
<Textarea
placeholder={t.placeholder}
className="mb-4 resize-none"
/>
<p className="font-medium">{t.startTask}</p>
<p className="text-muted-foreground">{t.description}</p>
</div>
</PopoverContent>
</Popover>
</ButtonGroup>
)
}

View File

@@ -1,78 +0,0 @@
"use client"
import {
Avatar,
AvatarFallback,
AvatarGroup,
AvatarImage,
} from "@/examples/base/ui-rtl/avatar"
import { Button } from "@/examples/base/ui-rtl/button"
import {
Empty,
EmptyContent,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle,
} from "@/examples/base/ui-rtl/empty"
import { PlusIcon } from "lucide-react"
import { useLanguageContext } from "@/components/language-selector"
const translations = {
ar: {
dir: "rtl" as const,
title: "لا يوجد أعضاء في الفريق",
description: "قم بدعوة فريقك للتعاون في هذا المشروع.",
invite: "دعوة أعضاء",
},
he: {
dir: "rtl" as const,
title: "אין חברי צוות",
description: "הזמן את הצוות שלך לשתף פעולה בפרויקט זה.",
invite: "הזמן חברים",
},
}
export function EmptyAvatarGroup() {
const context = useLanguageContext()
const lang = context?.language === "he" ? "he" : "ar"
const t = translations[lang]
return (
<Empty className="flex-none border py-10" dir={t.dir}>
<EmptyHeader>
<EmptyMedia>
<AvatarGroup className="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>
</AvatarGroup>
</EmptyMedia>
<EmptyTitle>{t.title}</EmptyTitle>
<EmptyDescription>{t.description}</EmptyDescription>
</EmptyHeader>
<EmptyContent>
<Button size="sm">
<PlusIcon />
{t.invite}
</Button>
</EmptyContent>
</Empty>
)
}

View File

@@ -1,36 +0,0 @@
"use client"
import { Checkbox } from "@/examples/base/ui-rtl/checkbox"
import { Field, FieldLabel } from "@/examples/base/ui-rtl/field"
import { useLanguageContext } from "@/components/language-selector"
const translations = {
ar: {
dir: "rtl" as const,
terms: "أوافق على الشروط والأحكام",
},
he: {
dir: "rtl" as const,
terms: "אני מסכים לתנאים וההגבלות",
},
}
export function FieldCheckbox() {
const context = useLanguageContext()
const lang = context?.language === "he" ? "he" : "ar"
const { dir, terms } = translations[lang]
return (
<div dir={dir}>
<FieldLabel htmlFor="checkbox-demo-rtl">
<Field orientation="horizontal">
<Checkbox id="checkbox-demo-rtl" defaultChecked />
<FieldLabel htmlFor="checkbox-demo-rtl" className="line-clamp-1">
{terms}
</FieldLabel>
</Field>
</FieldLabel>
</div>
)
}

View File

@@ -1,217 +0,0 @@
"use client"
import { Button } from "@/examples/base/ui-rtl/button"
import { Checkbox } from "@/examples/base/ui-rtl/checkbox"
import {
Field,
FieldDescription,
FieldGroup,
FieldLabel,
FieldLegend,
FieldSeparator,
FieldSet,
} from "@/examples/base/ui-rtl/field"
import { Input } from "@/examples/base/ui-rtl/input"
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/examples/base/ui-rtl/select"
import { Textarea } from "@/examples/base/ui-rtl/textarea"
import { useLanguageContext } from "@/components/language-selector"
const translations = {
ar: {
dir: "rtl" as const,
locale: "ar-SA",
paymentMethod: "طريقة الدفع",
secureEncrypted: "جميع المعاملات آمنة ومشفرة",
nameOnCard: "الاسم على البطاقة",
namePlaceholder: "أحمد محمد",
cardNumber: "رقم البطاقة",
cardDescription: "أدخل رقمك المكون من 16 رقمًا.",
cvv: "رمز الأمان",
month: "الشهر",
year: "السنة",
billingAddress: "عنوان الفواتير",
billingDescription: "عنوان الفواتير المرتبط بطريقة الدفع الخاصة بك",
sameAsShipping: "نفس عنوان الشحن",
comments: "تعليقات",
commentsPlaceholder: "أضف أي تعليقات إضافية",
submit: "إرسال",
cancel: "إلغاء",
},
he: {
dir: "rtl" as const,
locale: "he-IL",
paymentMethod: "אמצעי תשלום",
secureEncrypted: "כל העסקאות מאובטחות ומוצפנות",
nameOnCard: "שם על הכרטיס",
namePlaceholder: "ישראל ישראלי",
cardNumber: "מספר כרטיס",
cardDescription: "הזן את המספר בן 16 הספרות שלך.",
cvv: "קוד אבטחה",
month: "חודש",
year: "שנה",
billingAddress: "כתובת לחיוב",
billingDescription: "כתובת החיוב המשויכת לאמצעי התשלום שלך",
sameAsShipping: "זהה לכתובת המשלוח",
comments: "הערות",
commentsPlaceholder: "הוסף הערות נוספות",
submit: "שלח",
cancel: "ביטול",
},
}
function formatCardNumber(locale: string) {
const formatter = new Intl.NumberFormat(locale, { useGrouping: false })
return `${formatter.format(1234)} ${formatter.format(5678)} ${formatter.format(9012)} ${formatter.format(3456)}`
}
function formatCvv(locale: string) {
return new Intl.NumberFormat(locale, { useGrouping: false }).format(123)
}
function getMonths(locale: string) {
const formatter = new Intl.NumberFormat(locale, {
minimumIntegerDigits: 2,
useGrouping: false,
})
return Array.from({ length: 12 }, (_, i) => {
const value = String(i + 1).padStart(2, "0")
return { label: formatter.format(i + 1), value }
})
}
function getYears(locale: string) {
const formatter = new Intl.NumberFormat(locale, { useGrouping: false })
return Array.from({ length: 6 }, (_, i) => {
const year = 2024 + i
return { label: formatter.format(year), value: String(year) }
})
}
export function FieldDemo() {
const context = useLanguageContext()
const lang = context?.language === "he" ? "he" : "ar"
const t = translations[lang]
const months = getMonths(t.locale)
const years = getYears(t.locale)
const cardPlaceholder = formatCardNumber(t.locale)
const cvvPlaceholder = formatCvv(t.locale)
return (
<div dir={t.dir} className="w-full max-w-md rounded-lg border p-6">
<form>
<FieldGroup>
<FieldSet>
<FieldLegend>{t.paymentMethod}</FieldLegend>
<FieldDescription>{t.secureEncrypted}</FieldDescription>
<FieldGroup>
<Field>
<FieldLabel htmlFor="rtl-card-name">{t.nameOnCard}</FieldLabel>
<Input
id="rtl-card-name"
placeholder={t.namePlaceholder}
required
/>
</Field>
<div className="grid grid-cols-3 gap-4">
<Field className="col-span-2">
<FieldLabel htmlFor="rtl-card-number">
{t.cardNumber}
</FieldLabel>
<Input
id="rtl-card-number"
placeholder={cardPlaceholder}
required
/>
<FieldDescription>{t.cardDescription}</FieldDescription>
</Field>
<Field className="col-span-1">
<FieldLabel htmlFor="rtl-cvv">{t.cvv}</FieldLabel>
<Input id="rtl-cvv" placeholder={cvvPlaceholder} required />
</Field>
</div>
<div className="grid grid-cols-2 gap-4">
<Field>
<FieldLabel htmlFor="rtl-exp-month">{t.month}</FieldLabel>
<Select defaultValue="" items={months}>
<SelectTrigger id="rtl-exp-month">
<SelectValue placeholder="MM" />
</SelectTrigger>
<SelectContent data-lang={lang} dir={t.dir}>
<SelectGroup>
{months.map((item) => (
<SelectItem key={item.value} value={item.value}>
{item.label}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</Field>
<Field>
<FieldLabel htmlFor="rtl-exp-year">{t.year}</FieldLabel>
<Select defaultValue="" items={years}>
<SelectTrigger id="rtl-exp-year">
<SelectValue placeholder="YYYY" />
</SelectTrigger>
<SelectContent data-lang={lang} dir={t.dir}>
<SelectGroup>
{years.map((item) => (
<SelectItem key={item.value} value={item.value}>
{item.label}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</Field>
</div>
</FieldGroup>
</FieldSet>
<FieldSeparator />
<FieldSet>
<FieldLegend>{t.billingAddress}</FieldLegend>
<FieldDescription>{t.billingDescription}</FieldDescription>
<FieldGroup>
<Field orientation="horizontal">
<Checkbox id="rtl-same-as-shipping" defaultChecked />
<FieldLabel
htmlFor="rtl-same-as-shipping"
className="font-normal"
>
{t.sameAsShipping}
</FieldLabel>
</Field>
</FieldGroup>
</FieldSet>
<FieldSeparator />
<FieldSet>
<FieldGroup>
<Field>
<FieldLabel htmlFor="rtl-comments">{t.comments}</FieldLabel>
<Textarea
id="rtl-comments"
placeholder={t.commentsPlaceholder}
className="resize-none"
/>
</Field>
</FieldGroup>
</FieldSet>
<Field orientation="horizontal">
<Button type="submit">{t.submit}</Button>
<Button variant="outline" type="button">
{t.cancel}
</Button>
</Field>
</FieldGroup>
</form>
</div>
)
}

View File

@@ -1,90 +0,0 @@
"use client"
import { Card, CardContent } from "@/examples/base/ui-rtl/card"
import { Checkbox } from "@/examples/base/ui-rtl/checkbox"
import {
Field,
FieldDescription,
FieldGroup,
FieldLabel,
FieldLegend,
FieldSet,
FieldTitle,
} from "@/examples/base/ui-rtl/field"
import { useLanguageContext } from "@/components/language-selector"
const translations = {
ar: {
dir: "rtl" as const,
legend: "كيف سمعت عنا؟",
description: "اختر الخيار الذي يصف أفضل طريقة سمعت عنا من خلالها.",
socialMedia: "التواصل الاجتماعي",
searchEngine: "البحث",
referral: "إحالة",
other: "أخرى",
},
he: {
dir: "rtl" as const,
legend: "איך שמעת עלינו?",
description: "בחר את האפשרות שמתארת בצורה הטובה ביותר כיצד שמעת עלינו.",
socialMedia: "חברתיות",
searchEngine: "חיפוש",
referral: "הפניה",
other: "אחר",
},
}
export function FieldHear() {
const context = useLanguageContext()
const lang = context?.language === "he" ? "he" : "ar"
const t = translations[lang]
const options = [
{ label: t.socialMedia, value: "social-media" },
{ label: t.searchEngine, value: "search-engine" },
{ label: t.referral, value: "referral" },
{ label: t.other, value: "other" },
]
return (
<div dir={t.dir}>
<Card className="border-0 py-4 shadow-none">
<CardContent className="px-4">
<form>
<FieldGroup>
<FieldSet className="gap-4">
<FieldLegend>{t.legend}</FieldLegend>
<FieldDescription className="line-clamp-1">
{t.description}
</FieldDescription>
<FieldGroup className="flex flex-row flex-wrap gap-2 [--radius:9999rem]">
{options.map((option) => (
<FieldLabel
htmlFor={`rtl-${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={`rtl-${option.value}`}
defaultChecked={option.value === "social-media"}
className="-ms-6 translate-x-1 rounded-full transition-all duration-100 ease-linear data-checked:ms-0 data-checked:translate-x-0"
/>
<FieldTitle>{option.label}</FieldTitle>
</Field>
</FieldLabel>
))}
</FieldGroup>
</FieldSet>
</FieldGroup>
</form>
</CardContent>
</Card>
</div>
)
}

View File

@@ -1,67 +0,0 @@
"use client"
import { useState } from "react"
import {
Field,
FieldDescription,
FieldTitle,
} from "@/examples/base/ui-rtl/field"
import { Slider } from "@/examples/base/ui-rtl/slider"
import { useLanguageContext } from "@/components/language-selector"
const translations = {
ar: {
dir: "rtl" as const,
locale: "ar-SA",
title: "نطاق السعر",
description: "حدد نطاق ميزانيتك",
ariaLabel: "نطاق السعر",
currency: "﷼",
},
he: {
dir: "rtl" as const,
locale: "he-IL",
title: "טווח מחירים",
description: "הגדר את טווח התקציב שלך",
ariaLabel: "טווח מחירים",
currency: "₪",
},
}
function formatNumber(value: number, locale: string) {
return new Intl.NumberFormat(locale).format(value)
}
export function FieldSlider() {
const context = useLanguageContext()
const lang = context?.language === "he" ? "he" : "ar"
const t = translations[lang]
const [value, setValue] = useState([200, 800])
return (
<Field dir={t.dir}>
<FieldTitle>{t.title}</FieldTitle>
<FieldDescription>
{t.description} ({t.currency}
<span className="font-medium tabular-nums">
{formatNumber(value[0], t.locale)}
</span>{" "}
-{" "}
<span className="font-medium tabular-nums">
{formatNumber(value[1], t.locale)}
</span>
).
</FieldDescription>
<Slider
value={value}
onValueChange={(value) => setValue(value as [number, number])}
max={1000}
min={0}
step={10}
className="mt-2 w-full"
aria-label={t.ariaLabel}
/>
</Field>
)
}

View File

@@ -1,92 +0,0 @@
"use client"
import { DirectionProvider } from "@/examples/base/ui-rtl/direction"
import { FieldSeparator } from "@/examples/base/ui-rtl/field"
import {
LanguageProvider,
LanguageSelector,
useLanguageContext,
} from "@/components/language-selector"
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"
function RtlComponentsContent() {
const context = useLanguageContext()
if (!context) {
return null
}
const { language } = context
return (
<div
className="relative grid gap-8 p-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 xl:gap-6 2xl:gap-8"
dir="rtl"
data-lang={language}
data-slot="rtl-components"
>
<LanguageSelector
value={language}
onValueChange={context.setLanguage}
className="absolute -top-12 right-52 hidden h-8! data-[size=sm]:rounded-lg lg:flex"
languages={["ar", "he"]}
/>
<div className="flex flex-col gap-6 *:[div]:w-full *:[div]:max-w-full">
<FieldDemo />
</div>
<div className="flex flex-col gap-6 *:[div]:w-full *:[div]:max-w-full">
<EmptyAvatarGroup />
<SpinnerBadge />
<ButtonGroupInputGroup />
<FieldSlider />
<InputGroupDemo />
</div>
<div className="flex flex-col gap-6 *:[div]:w-full *:[div]:max-w-full">
<InputGroupButtonExample />
<ItemDemo />
<FieldSeparator className="my-4">
{language === "he" ? "הגדרות מראה" : "إعدادات المظهر"}
</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>
)
}
export function RtlComponents() {
return (
<LanguageProvider defaultLanguage="ar">
<DirectionProvider direction="rtl">
<RtlComponentsContent />
</DirectionProvider>
</LanguageProvider>
)
}

View File

@@ -1,97 +0,0 @@
"use client"
import * as React from "react"
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
} from "@/examples/base/ui-rtl/input-group"
import { Label } from "@/examples/base/ui-rtl/label"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/examples/base/ui-rtl/popover"
import { IconInfoCircle, IconStar } from "@tabler/icons-react"
import { useLanguageContext } from "@/components/language-selector"
const translations = {
ar: {
dir: "rtl" as const,
inputLabel: "السعر",
info: "معلومات",
priceInfo: "أدخل السعر بالريال السعودي.",
priceDescription: "سيتم تحويل السعر تلقائياً.",
favorite: "مفضل",
currency: "ر.س",
},
he: {
dir: "rtl" as const,
inputLabel: "מחיר",
info: "מידע",
priceInfo: "הזן את המחיר בשקלים.",
priceDescription: "המחיר יומר אוטומטית.",
favorite: "מועדף",
currency: "₪",
},
}
export function InputGroupButtonExample() {
const context = useLanguageContext()
const lang = context?.language === "he" ? "he" : "ar"
const t = translations[lang]
const [isFavorite, setIsFavorite] = React.useState(false)
return (
<div dir={t.dir} className="grid w-full max-w-sm gap-6">
<Label htmlFor="input-secure-rtl" className="sr-only">
{t.inputLabel}
</Label>
<InputGroup className="[--radius:9999px]">
<InputGroupInput id="input-secure-rtl" className="!pr-0.5" />
<InputGroupAddon>
<Popover>
<PopoverTrigger
render={
<InputGroupButton
variant="secondary"
size="icon-xs"
aria-label={t.info}
/>
}
>
<IconInfoCircle />
</PopoverTrigger>
<PopoverContent
align="end"
alignOffset={10}
className="flex flex-col gap-1 rounded-xl text-sm"
data-lang={lang}
dir={t.dir}
>
<p className="font-medium">{t.priceInfo}</p>
<p>{t.priceDescription}</p>
</PopoverContent>
</Popover>
</InputGroupAddon>
<InputGroupAddon className="text-muted-foreground">
{t.currency}
</InputGroupAddon>
<InputGroupAddon align="inline-end">
<InputGroupButton
onClick={() => setIsFavorite(!isFavorite)}
size="icon-xs"
aria-label={t.favorite}
>
<IconStar
data-favorite={isFavorite}
className="data-[favorite=true]:fill-primary data-[favorite=true]:stroke-primary"
/>
</InputGroupButton>
</InputGroupAddon>
</InputGroup>
</div>
)
}

View File

@@ -1,140 +0,0 @@
"use client"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/examples/base/ui-rtl/dropdown-menu"
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
InputGroupText,
InputGroupTextarea,
} from "@/examples/base/ui-rtl/input-group"
import { Separator } from "@/examples/base/ui-rtl/separator"
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/examples/base/ui-rtl/tooltip"
import {
IconCheck,
IconChevronDown,
IconInfoCircle,
IconPlus,
} from "@tabler/icons-react"
import { ArrowUpIcon, Search } from "lucide-react"
import { useLanguageContext } from "@/components/language-selector"
const translations = {
ar: {
dir: "rtl" as const,
search: "بحث...",
results: "12 نتيجة",
example: "example.com",
tooltipContent: "هذا محتوى في تلميح.",
askSearchChat: "اسأل، ابحث أو تحدث...",
add: "إضافة",
auto: "تلقائي",
agent: "وكيل",
manual: "يدوي",
used: "52% مستخدم",
send: "إرسال",
},
he: {
dir: "rtl" as const,
search: "חיפוש...",
results: "12 תוצאות",
example: "example.com",
tooltipContent: "זה תוכן בטולטיפ.",
askSearchChat: "שאל, חפש או שוחח...",
add: "הוסף",
auto: "אוטומטי",
agent: "סוכן",
manual: "ידני",
used: "52% בשימוש",
send: "שלח",
},
}
export function InputGroupDemo() {
const context = useLanguageContext()
const lang = context?.language === "he" ? "he" : "ar"
const t = translations[lang]
return (
<div dir={t.dir} className="grid w-full max-w-sm gap-6">
<InputGroup>
<InputGroupInput placeholder={t.search} />
<InputGroupAddon>
<Search />
</InputGroupAddon>
<InputGroupAddon align="inline-end">{t.results}</InputGroupAddon>
</InputGroup>
<InputGroup>
<InputGroupInput placeholder={t.example} />
<InputGroupAddon align="inline-end">
<Tooltip>
<TooltipTrigger
render={
<InputGroupButton
className="rounded-full"
size="icon-xs"
aria-label={t.add}
/>
}
>
<IconInfoCircle />
</TooltipTrigger>
<TooltipContent>{t.tooltipContent}</TooltipContent>
</Tooltip>
</InputGroupAddon>
</InputGroup>
<InputGroup>
<InputGroupTextarea placeholder={t.askSearchChat} />
<InputGroupAddon align="block-end">
<InputGroupButton
variant="outline"
className="rounded-full"
size="icon-xs"
aria-label={t.add}
>
<IconPlus />
</InputGroupButton>
<DropdownMenu>
<DropdownMenuTrigger render={<InputGroupButton variant="ghost" />}>
<IconChevronDown />
</DropdownMenuTrigger>
<DropdownMenuContent side="top" align="start">
<DropdownMenuItem>{t.auto}</DropdownMenuItem>
<DropdownMenuItem>{t.agent}</DropdownMenuItem>
<DropdownMenuItem>{t.manual}</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<InputGroupText className="ms-auto">{t.used}</InputGroupText>
<Separator orientation="vertical" className="!h-4" />
<InputGroupButton
variant="default"
className="rounded-full"
size="icon-xs"
>
<ArrowUpIcon />
<span className="sr-only">{t.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,64 +0,0 @@
"use client"
import { Button } from "@/examples/base/ui-rtl/button"
import {
Item,
ItemActions,
ItemContent,
ItemDescription,
ItemMedia,
ItemTitle,
} from "@/examples/base/ui-rtl/item"
import { BadgeCheckIcon, ChevronRightIcon } from "lucide-react"
import { useLanguageContext } from "@/components/language-selector"
const translations = {
ar: {
dir: "rtl" as const,
twoFactor: "المصادقة الثنائية",
twoFactorDescription: "التحقق عبر البريد الإلكتروني أو رقم الهاتف.",
enable: "تفعيل",
verified: "تم التحقق من ملفك الشخصي.",
},
he: {
dir: "rtl" as const,
twoFactor: "אימות דו-שלבי",
twoFactorDescription: "אמת באמצעות אימייל או מספר טלפון.",
enable: "הפעל",
verified: "הפרופיל שלך אומת.",
},
}
export function ItemDemo() {
const context = useLanguageContext()
const lang = context?.language === "he" ? "he" : "ar"
const t = translations[lang]
return (
<div dir={t.dir} className="flex w-full max-w-md flex-col gap-6">
<Item variant="outline">
<ItemContent>
<ItemTitle>{t.twoFactor}</ItemTitle>
<ItemDescription className="text-pretty xl:hidden 2xl:block">
{t.twoFactorDescription}
</ItemDescription>
</ItemContent>
<ItemActions>
<Button size="sm">{t.enable}</Button>
</ItemActions>
</Item>
<Item variant="outline" size="sm">
<ItemMedia>
<BadgeCheckIcon className="size-5" />
</ItemMedia>
<ItemContent>
<ItemTitle>{t.verified}</ItemTitle>
</ItemContent>
<ItemActions>
<ChevronRightIcon className="size-4 rtl:rotate-180" />
</ItemActions>
</Item>
</div>
)
}

View File

@@ -1,516 +0,0 @@
"use client"
import { useMemo, useState } from "react"
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@/examples/base/ui-rtl/avatar"
import { Badge } from "@/examples/base/ui-rtl/badge"
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/examples/base/ui-rtl/command"
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from "@/examples/base/ui-rtl/dropdown-menu"
import { Field, FieldLabel } from "@/examples/base/ui-rtl/field"
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupTextarea,
} from "@/examples/base/ui-rtl/input-group"
import { Popover, PopoverContent } from "@/examples/base/ui-rtl/popover"
import { Switch } from "@/examples/base/ui-rtl/switch"
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/examples/base/ui-rtl/tooltip"
import {
IconApps,
IconArrowUp,
IconAt,
IconBook,
IconCircleDashedPlus,
IconPaperclip,
IconPlus,
IconWorld,
IconX,
} from "@tabler/icons-react"
import { useLanguageContext } from "@/components/language-selector"
const translations = {
ar: {
dir: "rtl" as const,
prompt: "الأمر",
placeholder: "اسأل، ابحث، أو أنشئ أي شيء...",
addContext: "أضف سياق",
mentionTooltip: "اذكر شخصًا أو صفحة أو تاريخًا",
searchPages: "البحث في الصفحات...",
noPagesFound: "لم يتم العثور على صفحات",
pages: "الصفحات",
users: "المستخدمون",
attachFile: "إرفاق ملف",
selectModel: "اختر نموذج الذكاء الاصطناعي",
selectAgentMode: "اختر وضع الوكيل",
webSearch: "البحث على الويب",
appsIntegrations: "التطبيقات والتكاملات",
allSourcesAccess: "جميع المصادر التي يمكنني الوصول إليها",
findKnowledge: "ابحث أو استخدم المعرفة في...",
noKnowledgeFound: "لم يتم العثور على معرفة",
helpCenter: "مركز المساعدة",
connectApps: "ربط التطبيقات",
searchSourcesNote: "سنبحث فقط في المصادر المحددة هنا.",
send: "إرسال",
allSources: "جميع المصادر",
auto: "تلقائي",
agentMode: "وضع الوكيل",
planMode: "وضع التخطيط",
beta: "تجريبي",
workspace: "مساحة العمل",
meetingNotes: "ملاحظات الاجتماع",
projectDashboard: "لوحة المشروع",
ideasBrainstorming: "أفكار وعصف ذهني",
calendarEvents: "التقويم والأحداث",
documentation: "التوثيق",
goalsObjectives: "الأهداف والغايات",
budgetPlanning: "تخطيط الميزانية",
teamDirectory: "دليل الفريق",
technicalSpecs: "المواصفات التقنية",
analyticsReport: "تقرير التحليلات",
},
he: {
dir: "rtl" as const,
prompt: "פקודה",
placeholder: "שאל, חפש, או צור משהו...",
addContext: "הוסף הקשר",
mentionTooltip: "הזכר אדם, עמוד או תאריך",
searchPages: "חפש עמודים...",
noPagesFound: "לא נמצאו עמודים",
pages: "עמודים",
users: "משתמשים",
attachFile: "צרף קובץ",
selectModel: "בחר מודל AI",
selectAgentMode: "בחר מצב סוכן",
webSearch: "חיפוש באינטרנט",
appsIntegrations: "אפליקציות ואינטגרציות",
allSourcesAccess: "כל המקורות שיש לי גישה אליהם",
findKnowledge: "מצא או השתמש בידע ב...",
noKnowledgeFound: "לא נמצא ידע",
helpCenter: "מרכז עזרה",
connectApps: "חבר אפליקציות",
searchSourcesNote: "נחפש רק במקורות שנבחרו כאן.",
send: "שלח",
allSources: "כל המקורות",
auto: "אוטומטי",
agentMode: "מצב סוכן",
planMode: "מצב תכנון",
beta: "בטא",
workspace: "סביבת עבודה",
meetingNotes: "הערות פגישה",
projectDashboard: "לוח מחוונים לפרויקט",
ideasBrainstorming: "רעיונות וסיעור מוחות",
calendarEvents: "יומן ואירועים",
documentation: "תיעוד",
goalsObjectives: "מטרות ויעדים",
budgetPlanning: "תכנון תקציב",
teamDirectory: "ספריית צוות",
technicalSpecs: "מפרט טכני",
analyticsReport: "דוח אנליטיקה",
},
}
function MentionableIcon({
item,
}: {
item: { type: string; title: string; image: string }
}) {
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 context = useLanguageContext()
const lang = context?.language === "he" ? "he" : "ar"
const t = translations[lang]
const SAMPLE_DATA = useMemo(
() => ({
mentionable: [
{ type: "page", title: t.meetingNotes, image: "📝" },
{ type: "page", title: t.projectDashboard, image: "📊" },
{ type: "page", title: t.ideasBrainstorming, image: "💡" },
{ type: "page", title: t.calendarEvents, image: "📅" },
{ type: "page", title: t.documentation, image: "📚" },
{ type: "page", title: t.goalsObjectives, image: "🎯" },
{ type: "page", title: t.budgetPlanning, image: "💰" },
{ type: "page", title: t.teamDirectory, image: "👥" },
{ type: "page", title: t.technicalSpecs, image: "🔧" },
{ type: "page", title: t.analyticsReport, image: "📈" },
{
type: "user",
title: "shadcn",
image: "https://github.com/shadcn.png",
workspace: t.workspace,
},
{
type: "user",
title: "maxleiter",
image: "https://github.com/maxleiter.png",
workspace: t.workspace,
},
{
type: "user",
title: "evilrabbit",
image: "https://github.com/evilrabbit.png",
workspace: t.workspace,
},
],
models: [
{ name: t.auto },
{ name: t.agentMode, badge: t.beta },
{ name: t.planMode },
],
}),
[t]
)
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, SAMPLE_DATA])
const hasMentions = mentions.length > 0
return (
<div dir={t.dir}>
<form>
<Field>
<FieldLabel htmlFor="rtl-notion-prompt" className="sr-only">
{t.prompt}
</FieldLabel>
<InputGroup>
<InputGroupTextarea
id="rtl-notion-prompt"
placeholder={t.placeholder}
/>
<InputGroupAddon align="block-start">
<Popover
open={mentionPopoverOpen}
onOpenChange={setMentionPopoverOpen}
>
<Tooltip>
<TooltipTrigger
render={
<InputGroupButton
variant="outline"
size={!hasMentions ? "sm" : "icon-sm"}
className="rounded-full transition-transform"
/>
}
onFocusCapture={(e) => e.stopPropagation()}
>
<IconAt /> {!hasMentions && t.addContext}
</TooltipTrigger>
<TooltipContent>{t.mentionTooltip}</TooltipContent>
</Tooltip>
<PopoverContent className="p-0" align="start" dir={t.dir}>
<Command>
<CommandInput placeholder={t.searchPages} />
<CommandList>
<CommandEmpty>{t.noPagesFound}</CommandEmpty>
{Object.entries(grouped).map(([type, items]) => (
<CommandGroup
key={type}
heading={type === "page" ? t.pages : t.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 !pr-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
render={
<InputGroupButton
size="icon-sm"
className="rounded-full"
aria-label={t.attachFile}
/>
}
>
<IconPaperclip />
</TooltipTrigger>
<TooltipContent>{t.attachFile}</TooltipContent>
</Tooltip>
<DropdownMenu
open={modelPopoverOpen}
onOpenChange={setModelPopoverOpen}
>
<Tooltip>
<TooltipTrigger
render={
<InputGroupButton size="sm" className="rounded-full" />
}
>
{selectedModel.name}
</TooltipTrigger>
<TooltipContent>{t.selectModel}</TooltipContent>
</Tooltip>
<DropdownMenuContent
side="top"
align="start"
className="w-48"
dir={t.dir}
>
<DropdownMenuGroup className="w-48">
<DropdownMenuLabel className="text-muted-foreground text-xs">
{t.selectAgentMode}
</DropdownMenuLabel>
{SAMPLE_DATA.models.map((model) => (
<DropdownMenuCheckboxItem
key={model.name}
checked={model.name === selectedModel.name}
onCheckedChange={(checked) => {
if (checked) {
setSelectedModel(model)
}
}}
className="pr-2 *:[span:first-child]:right-auto *:[span:first-child]:left-2"
>
{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
render={
<InputGroupButton size="sm" className="rounded-full" />
}
>
<IconWorld /> {t.allSources}
</DropdownMenuTrigger>
<DropdownMenuContent
side="top"
align="end"
className="w-72"
dir={t.dir}
>
<DropdownMenuGroup>
<DropdownMenuItem
render={
<label htmlFor="rtl-web-search">
<IconWorld /> {t.webSearch}{" "}
<Switch
id="rtl-web-search"
className="ms-auto"
defaultChecked
size="sm"
/>
</label>
}
onSelect={(e) => e.preventDefault()}
></DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem
render={
<label htmlFor="rtl-apps">
<IconApps /> {t.appsIntegrations}
<Switch
id="rtl-apps"
className="ms-auto"
defaultChecked
size="sm"
/>
</label>
}
onSelect={(e) => e.preventDefault()}
></DropdownMenuItem>
<DropdownMenuItem>
<IconCircleDashedPlus /> {t.allSourcesAccess}
</DropdownMenuItem>
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<Avatar className="size-4">
<AvatarImage src="https://github.com/shadcn.png" />
<AvatarFallback>CN</AvatarFallback>
</Avatar>
shadcn
</DropdownMenuSubTrigger>
<DropdownMenuSubContent
className="w-72 rounded-lg p-0"
dir={t.dir}
side="left"
>
<Command>
<CommandInput
placeholder={t.findKnowledge}
autoFocus
/>
<CommandList>
<CommandEmpty>{t.noKnowledgeFound}</CommandEmpty>
<CommandGroup>
{SAMPLE_DATA.mentionable
.filter((item) => item.type === "user")
.map((user) => (
<CommandItem
key={user.title}
value={user.title}
onSelect={() => {
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 as { workspace?: string })
.workspace
}
</span>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</DropdownMenuSubContent>
</DropdownMenuSub>
<DropdownMenuItem>
<IconBook /> {t.helpCenter}
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
<IconPlus /> {t.connectApps}
</DropdownMenuItem>
<DropdownMenuLabel className="text-muted-foreground text-xs">
{t.searchSourcesNote}
</DropdownMenuLabel>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
<InputGroupButton
aria-label={t.send}
className="ms-auto rounded-full"
variant="default"
size="icon-sm"
>
<IconArrowUp />
</InputGroupButton>
</InputGroupAddon>
</InputGroup>
</Field>
</form>
</div>
)
}

View File

@@ -1,44 +0,0 @@
"use client"
import { Badge } from "@/examples/base/ui-rtl/badge"
import { Spinner } from "@/examples/base/ui-rtl/spinner"
import { useLanguageContext } from "@/components/language-selector"
const translations = {
ar: {
dir: "rtl" as const,
syncing: "جارٍ المزامنة",
updating: "جارٍ التحديث",
loading: "جارٍ التحميل",
},
he: {
dir: "rtl" as const,
syncing: "מסנכרן",
updating: "מעדכן",
loading: "טוען",
},
}
export function SpinnerBadge() {
const context = useLanguageContext()
const lang = context?.language === "he" ? "he" : "ar"
const t = translations[lang]
return (
<div dir={t.dir} className="flex items-center gap-2">
<Badge>
<Spinner />
{t.syncing}
</Badge>
<Badge variant="secondary">
<Spinner />
{t.updating}
</Badge>
<Badge variant="outline">
<Spinner />
{t.loading}
</Badge>
</div>
)
}

View File

@@ -1,52 +0,0 @@
"use client"
import { Button } from "@/examples/base/ui-rtl/button"
import {
Empty,
EmptyContent,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle,
} from "@/examples/base/ui-rtl/empty"
import { Spinner } from "@/examples/base/ui-rtl/spinner"
import { useLanguageContext } from "@/components/language-selector"
const translations = {
ar: {
dir: "rtl" as const,
title: "جارٍ معالجة طلبك",
description: "يرجى الانتظار بينما نعالج طلبك. لا تقم بتحديث الصفحة.",
cancel: "إلغاء",
},
he: {
dir: "rtl" as const,
title: "מעבד את הבקשה שלך",
description: "אנא המתן בזמן שאנו מעבדים את בקשתך. אל תרענן את הדף.",
cancel: "ביטול",
},
}
export function SpinnerEmpty() {
const context = useLanguageContext()
const lang = context?.language === "he" ? "he" : "ar"
const t = translations[lang]
return (
<Empty className="w-full border md:p-6" dir={t.dir}>
<EmptyHeader>
<EmptyMedia variant="icon">
<Spinner />
</EmptyMedia>
<EmptyTitle>{t.title}</EmptyTitle>
<EmptyDescription>{t.description}</EmptyDescription>
</EmptyHeader>
<EmptyContent>
<Button variant="outline" size="sm">
{t.cancel}
</Button>
</EmptyContent>
</Empty>
)
}

View File

@@ -1,14 +0,0 @@
import { type Metadata } from "next"
import { RtlComponents } from "./components"
export const metadata: Metadata = {
title: "RTL",
description: "RTL example page with right-to-left language support.",
}
export function RtlPage() {
return <RtlComponents />
}
export default RtlPage

View File

@@ -1,5 +1,6 @@
"use client" "use client"
import { DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu"
import { type Table } from "@tanstack/react-table" import { type Table } from "@tanstack/react-table"
import { Settings2 } from "lucide-react" import { Settings2 } from "lucide-react"
@@ -10,7 +11,6 @@ import {
DropdownMenuContent, DropdownMenuContent,
DropdownMenuLabel, DropdownMenuLabel,
DropdownMenuSeparator, DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/registry/new-york-v4/ui/dropdown-menu" } from "@/registry/new-york-v4/ui/dropdown-menu"
export function DataTableViewOptions<TData>({ export function DataTableViewOptions<TData>({

View File

@@ -3,23 +3,10 @@ import { NextResponse, type NextRequest } from "next/server"
import { processMdxForLLMs } from "@/lib/llm" import { processMdxForLLMs } from "@/lib/llm"
import { source } from "@/lib/source" import { source } from "@/lib/source"
import { getActiveStyle, type Style } from "@/registry/_legacy-styles" import { getActiveStyle } from "@/registry/_legacy-styles"
export const revalidate = false export const revalidate = false
function getStyleFromSlug(slug: string[] | undefined, fallbackStyle: string) {
// Detect base from URL: /docs/components/base/... or /docs/components/radix/...
if (slug && slug[0] === "components" && slug[1]) {
if (slug[1] === "base") {
return "base-nova"
}
if (slug[1] === "radix") {
return "new-york-v4"
}
}
return fallbackStyle
}
export async function GET( export async function GET(
_req: NextRequest, _req: NextRequest,
{ params }: { params: Promise<{ slug?: string[] }> } { params }: { params: Promise<{ slug?: string[] }> }
@@ -32,11 +19,9 @@ export async function GET(
notFound() notFound()
} }
const effectiveStyle = getStyleFromSlug(slug, activeStyle.name)
const processedContent = processMdxForLLMs( const processedContent = processMdxForLLMs(
await page.data.getText("raw"), await page.data.getText("raw"),
effectiveStyle as Style["name"] activeStyle.name
) )
return new NextResponse(processedContent, { return new NextResponse(processedContent, {

View File

@@ -2,11 +2,13 @@
import * as React from "react" import * as React from "react"
import Script from "next/script" import Script from "next/script"
import { DiceFaces05Icon } from "@hugeicons/core-free-icons" import { DiceFaces05Icon, Undo02Icon } from "@hugeicons/core-free-icons"
import { HugeiconsIcon } from "@hugeicons/react" import { HugeiconsIcon } from "@hugeicons/react"
import { cn } from "@/lib/utils"
import { import {
BASE_COLORS, BASE_COLORS,
DEFAULT_CONFIG,
getThemesForBaseColor, getThemesForBaseColor,
iconLibraries, iconLibraries,
MENU_ACCENTS, MENU_ACCENTS,
@@ -16,11 +18,6 @@ import {
} from "@/registry/config" } from "@/registry/config"
import { Button } from "@/registry/new-york-v4/ui/button" import { Button } from "@/registry/new-york-v4/ui/button"
import { Kbd } from "@/registry/new-york-v4/ui/kbd" import { Kbd } from "@/registry/new-york-v4/ui/kbd"
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/registry/new-york-v4/ui/tooltip"
import { useLocks } from "@/app/(create)/hooks/use-locks" import { useLocks } from "@/app/(create)/hooks/use-locks"
import { FONTS } from "@/app/(create)/lib/fonts" import { FONTS } from "@/app/(create)/lib/fonts"
import { import {
@@ -36,10 +33,26 @@ function randomItem<T>(array: readonly T[]): T {
return array[Math.floor(Math.random() * array.length)] return array[Math.floor(Math.random() * array.length)]
} }
export function RandomButton() { export function CustomizerControls({ className }: { className?: string }) {
const { locks } = useLocks() const { locks } = useLocks()
const [params, setParams] = useDesignSystemSearchParams() const [params, setParams] = useDesignSystemSearchParams()
const handleReset = React.useCallback(() => {
setParams({
base: params.base, // Keep the current base value
style: DEFAULT_CONFIG.style,
baseColor: DEFAULT_CONFIG.baseColor,
theme: DEFAULT_CONFIG.theme,
iconLibrary: DEFAULT_CONFIG.iconLibrary,
font: DEFAULT_CONFIG.font,
menuAccent: DEFAULT_CONFIG.menuAccent,
menuColor: DEFAULT_CONFIG.menuColor,
radius: DEFAULT_CONFIG.radius,
template: DEFAULT_CONFIG.template,
item: "preview",
})
}, [setParams, params.base])
const handleRandomize = React.useCallback(() => { const handleRandomize = React.useCallback(() => {
// Use current value if locked, otherwise randomize. // Use current value if locked, otherwise randomize.
const baseColor = locks.has("baseColor") const baseColor = locks.has("baseColor")
@@ -117,30 +130,33 @@ export function RandomButton() {
}, [handleRandomize]) }, [handleRandomize])
return ( return (
<Tooltip> <div className={cn("items-center gap-0", className)}>
<TooltipTrigger asChild> <Button
<Button variant="ghost"
variant="ghost" size="sm"
size="sm" onClick={handleRandomize}
onClick={handleRandomize} className="border-foreground/10 bg-muted/50 h-[calc(--spacing(13.5))] w-[140px] touch-manipulation justify-between rounded-xl border select-none focus-visible:border-transparent focus-visible:ring-1 sm:rounded-lg md:w-full md:rounded-lg md:border-transparent md:bg-transparent md:pr-3.5! md:pl-2!"
className="border-foreground/10 bg-muted/50 h-[calc(--spacing(13.5))] w-[140px] touch-manipulation justify-between rounded-xl border select-none focus-visible:border-transparent focus-visible:ring-1 sm:rounded-lg md:w-full md:rounded-lg md:border-transparent md:bg-transparent md:pr-3.5! md:pl-2!" >
> <div className="flex flex-col justify-start text-left">
<div className="flex flex-col justify-start text-left"> <div className="text-muted-foreground text-xs">Shuffle</div>
<div className="text-muted-foreground text-xs">Shuffle</div> <div className="text-foreground text-sm font-medium">Try Random</div>
<div className="text-foreground text-sm font-medium"> </div>
Try Random <HugeiconsIcon icon={DiceFaces05Icon} className="size-5 md:hidden" />
</div> <Kbd className="bg-foreground/10 text-foreground hidden md:flex">R</Kbd>
</div> </Button>
<HugeiconsIcon icon={DiceFaces05Icon} className="size-5 md:hidden" /> <Button
<Kbd className="bg-foreground/10 text-foreground hidden md:flex"> variant="ghost"
R size="sm"
</Kbd> onClick={handleReset}
</Button> className="border-foreground/10 bg-muted/50 hidden h-[calc(--spacing(13.5))] w-[140px] touch-manipulation justify-between rounded-xl border select-none focus-visible:border-transparent focus-visible:ring-1 sm:rounded-lg md:flex md:w-full md:rounded-lg md:border-transparent md:bg-transparent md:pr-3.5! md:pl-2!"
</TooltipTrigger> >
<TooltipContent side="left"> <div className="flex flex-col justify-start text-left">
Use browser back/forward to navigate history <div className="text-muted-foreground text-xs">Reset</div>
</TooltipContent> <div className="text-foreground text-sm font-medium">Start Over</div>
</Tooltip> </div>
<HugeiconsIcon icon={Undo02Icon} className="-translate-x-0.5" />
</Button>
</div>
) )
} }

View File

@@ -7,17 +7,15 @@ import { HugeiconsIcon } from "@hugeicons/react"
import { useIsMobile } from "@/hooks/use-mobile" import { useIsMobile } from "@/hooks/use-mobile"
import { getThemesForBaseColor, PRESETS, STYLES } from "@/registry/config" import { getThemesForBaseColor, PRESETS, STYLES } from "@/registry/config"
import { FieldGroup } from "@/registry/new-york-v4/ui/field" import { FieldGroup } from "@/registry/new-york-v4/ui/field"
import { Separator } from "@/registry/new-york-v4/ui/separator"
import { MenuAccentPicker } from "@/app/(create)/components/accent-picker" import { MenuAccentPicker } from "@/app/(create)/components/accent-picker"
import { BaseColorPicker } from "@/app/(create)/components/base-color-picker" import { BaseColorPicker } from "@/app/(create)/components/base-color-picker"
import { BasePicker } from "@/app/(create)/components/base-picker" import { BasePicker } from "@/app/(create)/components/base-picker"
import { CustomizerControls } from "@/app/(create)/components/customizer-controls"
import { FontPicker } from "@/app/(create)/components/font-picker" import { FontPicker } from "@/app/(create)/components/font-picker"
import { IconLibraryPicker } from "@/app/(create)/components/icon-library-picker" import { IconLibraryPicker } from "@/app/(create)/components/icon-library-picker"
import { MenuColorPicker } from "@/app/(create)/components/menu-picker" import { MenuColorPicker } from "@/app/(create)/components/menu-picker"
import { PresetPicker } from "@/app/(create)/components/preset-picker" import { PresetPicker } from "@/app/(create)/components/preset-picker"
import { RadiusPicker } from "@/app/(create)/components/radius-picker" import { RadiusPicker } from "@/app/(create)/components/radius-picker"
import { RandomButton } from "@/app/(create)/components/random-button"
import { ResetButton } from "@/app/(create)/components/reset-button"
import { StylePicker } from "@/app/(create)/components/style-picker" import { StylePicker } from "@/app/(create)/components/style-picker"
import { ThemePicker } from "@/app/(create)/components/theme-picker" import { ThemePicker } from "@/app/(create)/components/theme-picker"
import { FONTS } from "@/app/(create)/lib/fonts" import { FONTS } from "@/app/(create)/lib/fonts"
@@ -77,10 +75,7 @@ export function Customizer() {
<RadiusPicker isMobile={isMobile} anchorRef={anchorRef} /> <RadiusPicker isMobile={isMobile} anchorRef={anchorRef} />
<MenuColorPicker isMobile={isMobile} anchorRef={anchorRef} /> <MenuColorPicker isMobile={isMobile} anchorRef={anchorRef} />
<MenuAccentPicker isMobile={isMobile} anchorRef={anchorRef} /> <MenuAccentPicker isMobile={isMobile} anchorRef={anchorRef} />
<div className="mt-auto hidden w-full flex-col items-center gap-0 md:flex"> <CustomizerControls className="mt-auto hidden w-full flex-col md:flex" />
<RandomButton />
<ResetButton />
</div>
</FieldGroup> </FieldGroup>
</div> </div>
</div> </div>

View File

@@ -59,7 +59,7 @@ export function FontPicker({
anchor={isMobile ? anchorRef : undefined} anchor={isMobile ? anchorRef : undefined}
side={isMobile ? "top" : "right"} side={isMobile ? "top" : "right"}
align={isMobile ? "center" : "start"} align={isMobile ? "center" : "start"}
className="max-h-96 md:w-72" className="max-h-80 md:w-72"
> >
<PickerRadioGroup <PickerRadioGroup
value={currentFont?.value} value={currentFont?.value}

View File

@@ -45,12 +45,6 @@ const IconPhosphor = lazy(() =>
})) }))
) )
const IconRemixicon = lazy(() =>
import("@/registry/icons/icon-remixicon").then((mod) => ({
default: mod.IconRemixicon,
}))
)
const PREVIEW_ICONS = { const PREVIEW_ICONS = {
lucide: [ lucide: [
"CopyIcon", "CopyIcon",
@@ -116,22 +110,6 @@ const PREVIEW_ICONS = {
"CaretDownIcon", "CaretDownIcon",
"CaretRightIcon", "CaretRightIcon",
], ],
remixicon: [
"RiFileCopyLine",
"RiErrorWarningLine",
"RiDeleteBinLine",
"RiShareLine",
"RiShoppingBagLine",
"RiMoreLine",
"RiLoaderLine",
"RiAddLine",
"RiSubtractLine",
"RiArrowLeftLine",
"RiArrowRightLine",
"RiCheckLine",
"RiArrowDownSLine",
"RiArrowRightSLine",
],
} }
const logos = { const logos = {
@@ -216,17 +194,6 @@ const logos = {
/> />
</svg> </svg>
), ),
remixicon: (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="24"
height="24"
fill="currentColor"
>
<path d="M12 2C17.5228 2 22 6.47715 22 12C22 15.3137 19.3137 18 16 18C12.6863 18 10 15.3137 10 12C10 11.4477 9.55228 11 9 11C8.44772 11 8 11.4477 8 12C8 16.4183 11.5817 20 16 20C16.8708 20 17.7084 19.8588 18.4932 19.6016C16.7458 21.0956 14.4792 22 12 22C6.6689 22 2.3127 17.8283 2.0166 12.5713C2.23647 9.45772 4.83048 7 8 7C11.3137 7 14 9.68629 14 13C14 13.5523 14.4477 14 15 14C15.5523 14 16 13.5523 16 13C16 8.58172 12.4183 5 8 5C6.50513 5 5.1062 5.41032 3.90918 6.12402C5.72712 3.62515 8.67334 2 12 2Z" />
</svg>
),
} }
export function IconLibraryPicker({ export function IconLibraryPicker({
@@ -334,9 +301,7 @@ const IconLibraryPreview = memo(function IconLibraryPreview({
? IconTabler ? IconTabler
: iconLibrary === "hugeicons" : iconLibrary === "hugeicons"
? IconHugeicons ? IconHugeicons
: iconLibrary === "phosphor" : IconPhosphor
? IconPhosphor
: IconRemixicon
return ( return (
<Suspense <Suspense

View File

@@ -30,12 +30,6 @@ const IconPhosphor = lazy(() =>
})) }))
) )
const IconRemixicon = lazy(() =>
import("@/registry/icons/icon-remixicon").then((mod) => ({
default: mod.IconRemixicon,
}))
)
export function IconPlaceholder({ export function IconPlaceholder({
...props ...props
}: { }: {
@@ -58,9 +52,6 @@ export function IconPlaceholder({
{iconLibrary === "phosphor" && ( {iconLibrary === "phosphor" && (
<IconPhosphor name={iconName} {...props} /> <IconPhosphor name={iconName} {...props} />
)} )}
{iconLibrary === "remixicon" && (
<IconRemixicon name={iconName} {...props} />
)}
</Suspense> </Suspense>
) )
} }

View File

@@ -99,7 +99,7 @@ export function ItemPicker({
variant="outline" variant="outline"
aria-label="Select item" aria-label="Select item"
size="sm" size="sm"
className="data-popup-open:bg-muted dark:data-popup-open:bg-muted/50 bg-muted/50 sm:bg-background md:dark:bg-background border-foreground/10 dark:bg-muted/50 h-[calc(--spacing(13.5))] flex-1 touch-manipulation justify-between gap-2 rounded-xl pr-4! pl-2.5 text-left shadow-none select-none *:data-[slot=combobox-trigger-icon]:hidden sm:h-8 sm:max-w-56 sm:rounded-lg sm:pr-2! xl:max-w-64" className="data-popup-open:bg-muted dark:data-popup-open:bg-muted/50 bg-muted/50 sm:bg-background md:dark:bg-background border-foreground/10 dark:bg-muted/50 h-[calc(--spacing(13.5))] flex-1 touch-manipulation justify-between gap-2 rounded-xl pr-4! pl-2.5 text-left shadow-none select-none *:data-[slot=combobox-trigger-icon]:hidden sm:h-8 sm:max-w-56 sm:rounded-lg sm:pr-2! xl:max-w-md"
/> />
} }
> >
@@ -123,9 +123,9 @@ export function ItemPicker({
<HugeiconsIcon icon={Search01Icon} /> <HugeiconsIcon icon={Search01Icon} />
</ComboboxTrigger> </ComboboxTrigger>
<ComboboxContent <ComboboxContent
className="ring-foreground/10 min-w-[calc(var(--available-width)---spacing(4))] translate-x-2 animate-none rounded-xl border-0 ring-1 data-open:animate-none sm:min-w-[calc(var(--anchor-width)+--spacing(7))] sm:translate-x-0 xl:w-96" className="ring-foreground/10 min-w-[calc(var(--available-width)---spacing(4))] translate-x-2 animate-none rounded-xl border-0 ring-1 data-open:animate-none sm:min-w-[calc(var(--anchor-width)+--spacing(7))] sm:translate-x-0"
side="bottom" side="bottom"
align="end" align="center"
> >
<ComboboxInput <ComboboxInput
showTrigger={false} showTrigger={false}

View File

@@ -139,7 +139,6 @@ function PickerSubTrigger({
tabler="IconChevronRight" tabler="IconChevronRight"
hugeicons="ArrowRight01Icon" hugeicons="ArrowRight01Icon"
phosphor="CaretRightIcon" phosphor="CaretRightIcon"
remixicon="RiArrowRightSLine"
className="ml-auto" className="ml-auto"
/> />
</MenuPrimitive.SubmenuTrigger> </MenuPrimitive.SubmenuTrigger>
@@ -193,7 +192,6 @@ function PickerCheckboxItem({
tabler="IconCheck" tabler="IconCheck"
hugeicons="Tick02Icon" hugeicons="Tick02Icon"
phosphor="CheckIcon" phosphor="CheckIcon"
remixicon="RiCheckLine"
/> />
</MenuPrimitive.CheckboxItemIndicator> </MenuPrimitive.CheckboxItemIndicator>
</span> </span>
@@ -235,7 +233,6 @@ function PickerRadioItem({
tabler="IconCheck" tabler="IconCheck"
hugeicons="Tick02Icon" hugeicons="Tick02Icon"
phosphor="CheckIcon" phosphor="CheckIcon"
remixicon="RiCheckLine"
className="size-4 pointer-coarse:size-5" className="size-4 pointer-coarse:size-5"
/> />
</MenuPrimitive.RadioItemIndicator> </MenuPrimitive.RadioItemIndicator>

View File

@@ -5,8 +5,8 @@ import { type ImperativePanelHandle } from "react-resizable-panels"
import { DARK_MODE_FORWARD_TYPE } from "@/components/mode-switcher" import { DARK_MODE_FORWARD_TYPE } from "@/components/mode-switcher"
import { Badge } from "@/registry/new-york-v4/ui/badge" import { Badge } from "@/registry/new-york-v4/ui/badge"
import { RANDOMIZE_FORWARD_TYPE } from "@/app/(create)/components/customizer-controls"
import { CMD_K_FORWARD_TYPE } from "@/app/(create)/components/item-picker" import { CMD_K_FORWARD_TYPE } from "@/app/(create)/components/item-picker"
import { RANDOMIZE_FORWARD_TYPE } from "@/app/(create)/components/random-button"
import { sendToIframe } from "@/app/(create)/hooks/use-iframe-sync" import { sendToIframe } from "@/app/(create)/hooks/use-iframe-sync"
import { import {
serializeDesignSystemSearchParams, serializeDesignSystemSearchParams,

View File

@@ -1,75 +0,0 @@
"use client"
import * as React from "react"
import { Undo02Icon } from "@hugeicons/core-free-icons"
import { HugeiconsIcon } from "@hugeicons/react"
import { DEFAULT_CONFIG } from "@/registry/config"
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/registry/new-york-v4/ui/alert-dialog"
import { Button } from "@/registry/new-york-v4/ui/button"
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
export function ResetButton() {
const [params, setParams] = useDesignSystemSearchParams()
const handleReset = React.useCallback(() => {
setParams({
base: params.base, // Keep the current base value.
style: DEFAULT_CONFIG.style,
baseColor: DEFAULT_CONFIG.baseColor,
theme: DEFAULT_CONFIG.theme,
iconLibrary: DEFAULT_CONFIG.iconLibrary,
font: DEFAULT_CONFIG.font,
menuAccent: DEFAULT_CONFIG.menuAccent,
menuColor: DEFAULT_CONFIG.menuColor,
radius: DEFAULT_CONFIG.radius,
template: DEFAULT_CONFIG.template,
item: "preview",
})
}, [setParams, params.base])
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button
variant="ghost"
size="sm"
className="border-foreground/10 bg-muted/50 hidden h-[calc(--spacing(13.5))] w-[140px] touch-manipulation justify-between rounded-xl border select-none focus-visible:border-transparent focus-visible:ring-1 sm:rounded-lg md:flex md:w-full md:rounded-lg md:border-transparent md:bg-transparent md:pr-3.5! md:pl-2!"
>
<div className="flex flex-col justify-start text-left">
<div className="text-muted-foreground text-xs">Reset</div>
<div className="text-foreground text-sm font-medium">
Start Over
</div>
</div>
<HugeiconsIcon icon={Undo02Icon} className="-translate-x-0.5" />
</Button>
</AlertDialogTrigger>
<AlertDialogContent className="dialog-ring p-4 sm:max-w-sm">
<AlertDialogHeader>
<AlertDialogTitle>Reset to defaults?</AlertDialogTitle>
<AlertDialogDescription>
This will reset all customization options to their default values.
This action cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel className="rounded-lg">Cancel</AlertDialogCancel>
<AlertDialogAction className="rounded-lg" onClick={handleReset}>
Reset
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)
}

View File

@@ -56,7 +56,7 @@ export function ShareButton() {
<Button <Button
size="sm" size="sm"
variant="outline" variant="outline"
className="rounded-lg shadow-none lg:w-8 xl:w-fit" className="rounded-lg shadow-none"
onClick={handleCopy} onClick={handleCopy}
> >
{hasCopied ? ( {hasCopied ? (
@@ -64,7 +64,7 @@ export function ShareButton() {
) : ( ) : (
<HugeiconsIcon icon={Share03Icon} strokeWidth={2} /> <HugeiconsIcon icon={Share03Icon} strokeWidth={2} />
)} )}
<span className="lg:hidden xl:block">Share</span> Share
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent>Copy Link</TooltipContent> <TooltipContent>Copy Link</TooltipContent>

View File

@@ -76,7 +76,7 @@ export function ThemePicker({
anchor={isMobile ? anchorRef : undefined} anchor={isMobile ? anchorRef : undefined}
side={isMobile ? "top" : "right"} side={isMobile ? "top" : "right"}
align={isMobile ? "center" : "start"} align={isMobile ? "center" : "start"}
className="max-h-[23rem]" className="max-h-96"
> >
<PickerRadioGroup <PickerRadioGroup
value={currentTheme?.name} value={currentTheme?.name}

View File

@@ -1,7 +1,6 @@
"use client" "use client"
import * as React from "react" import * as React from "react"
import Link from "next/link"
import { import {
ComputerTerminal01Icon, ComputerTerminal01Icon,
Copy01Icon, Copy01Icon,
@@ -24,8 +23,6 @@ import {
} from "@/registry/new-york-v4/ui/dialog" } from "@/registry/new-york-v4/ui/dialog"
import { import {
Field, Field,
FieldContent,
FieldDescription,
FieldGroup, FieldGroup,
FieldLabel, FieldLabel,
FieldTitle, FieldTitle,
@@ -34,7 +31,6 @@ import {
RadioGroup, RadioGroup,
RadioGroupItem, RadioGroupItem,
} from "@/registry/new-york-v4/ui/radio-group" } from "@/registry/new-york-v4/ui/radio-group"
import { Switch } from "@/registry/new-york-v4/ui/switch"
import { import {
Tabs, Tabs,
TabsContent, TabsContent,
@@ -75,15 +71,14 @@ export function ToolbarControls() {
const packageManager = config.packageManager || "pnpm" const packageManager = config.packageManager || "pnpm"
const commands = React.useMemo(() => { const commands = React.useMemo(() => {
const origin = process.env.NEXT_PUBLIC_APP_URL || "http://localhost:4000" const origin = process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000"
const url = `${origin}/init?base=${params.base}&style=${params.style}&baseColor=${params.baseColor}&theme=${params.theme}&iconLibrary=${params.iconLibrary}&font=${params.font}&menuAccent=${params.menuAccent}&menuColor=${params.menuColor}&radius=${params.radius}&template=${params.template}&rtl=${params.rtl}` const url = `${origin}/init?base=${params.base}&style=${params.style}&baseColor=${params.baseColor}&theme=${params.theme}&iconLibrary=${params.iconLibrary}&font=${params.font}&menuAccent=${params.menuAccent}&menuColor=${params.menuColor}&radius=${params.radius}&template=${params.template}`
const templateFlag = params.template ? ` --template ${params.template}` : "" const templateFlag = params.template ? ` --template ${params.template}` : ""
const rtlFlag = params.rtl ? " --rtl" : ""
return { return {
pnpm: `pnpm dlx shadcn@latest create${rtlFlag} --preset "${url}"${templateFlag}`, pnpm: `pnpm dlx shadcn@latest create --preset "${url}"${templateFlag}`,
npm: `npx shadcn@latest create${rtlFlag} --preset "${url}"${templateFlag}`, npm: `npx shadcn@latest create --preset "${url}"${templateFlag}`,
yarn: `yarn dlx shadcn@latest create${rtlFlag} --preset "${url}"${templateFlag}`, yarn: `yarn dlx shadcn@latest create --preset "${url}"${templateFlag}`,
bun: `bunx --bun shadcn@latest create${rtlFlag} --preset "${url}"${templateFlag}`, bun: `bunx --bun shadcn@latest create --preset "${url}"${templateFlag}`,
} }
}, [ }, [
params.base, params.base,
@@ -96,7 +91,6 @@ export function ToolbarControls() {
params.menuColor, params.menuColor,
params.radius, params.radius,
params.template, params.template,
params.rtl,
]) ])
const command = commands[packageManager] const command = commands[packageManager]
@@ -170,7 +164,7 @@ export function ToolbarControls() {
{selectedTemplate?.title} + shadcn/ui project. {selectedTemplate?.title} + shadcn/ui project.
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<FieldGroup className="gap-3"> <FieldGroup>
<Field> <Field>
<FieldLabel htmlFor="template" className="sr-only"> <FieldLabel htmlFor="template" className="sr-only">
Template Template
@@ -189,7 +183,7 @@ export function ToolbarControls() {
<FieldLabel <FieldLabel
key={template.value} key={template.value}
htmlFor={template.value} htmlFor={template.value}
className="has-data-[state=checked]:border-primary/10 rounded-lg!" className="rounded-lg!"
> >
<Field className="flex min-w-0 flex-col items-center justify-center gap-2 p-3! text-center *:w-auto!"> <Field className="flex min-w-0 flex-col items-center justify-center gap-2 p-3! text-center *:w-auto!">
<RadioGroupItem <RadioGroupItem
@@ -211,81 +205,59 @@ export function ToolbarControls() {
))} ))}
</RadioGroup> </RadioGroup>
</Field> </Field>
<FieldLabel className="has-data-[state=checked]:border-primary/10 rounded-lg!">
<Field orientation="horizontal">
<FieldContent className="gap-1">
<FieldTitle>Enable RTL</FieldTitle>
<FieldDescription>
<a
href={`/docs/rtl/${params.template}`}
className="text-foreground underline"
target="_blank"
rel="noopener noreferrer"
>
View the RTL setup guide for {selectedTemplate?.title}.
</a>
</FieldDescription>
</FieldContent>
<Switch
checked={params.rtl}
onCheckedChange={(rtl) => setParams({ rtl })}
className="shadow-none"
/>
</Field>
</FieldLabel>
<Tabs
value={packageManager}
onValueChange={(value) => {
setConfig({
...config,
packageManager: value as "pnpm" | "npm" | "yarn" | "bun",
})
}}
className="bg-surface min-w-0 gap-0 overflow-hidden rounded-lg border"
>
<div className="flex items-center gap-2 p-2">
<TabsList className="*:data-[slot=tabs-trigger]:data-[state=active]:border-input h-auto rounded-none bg-transparent p-0 font-mono group-data-[orientation=horizontal]/tabs:h-8 *:data-[slot=tabs-trigger]:h-7 *:data-[slot=tabs-trigger]:border *:data-[slot=tabs-trigger]:border-transparent *:data-[slot=tabs-trigger]:pt-0.5 *:data-[slot=tabs-trigger]:shadow-none!">
<TabsTrigger value="pnpm">pnpm</TabsTrigger>
<TabsTrigger value="npm">npm</TabsTrigger>
<TabsTrigger value="yarn">yarn</TabsTrigger>
<TabsTrigger value="bun">bun</TabsTrigger>
</TabsList>
<Tooltip>
<TooltipTrigger asChild>
<Button
size="icon-sm"
variant="ghost"
className="ml-auto size-7 rounded-lg"
onClick={handleCopyFromTabs}
>
{hasCopied ? (
<HugeiconsIcon icon={Tick02Icon} className="size-4" />
) : (
<HugeiconsIcon icon={Copy01Icon} className="size-4" />
)}
<span className="sr-only">Copy command</span>
</Button>
</TooltipTrigger>
<TooltipContent>
{hasCopied ? "Copied!" : "Copy command"}
</TooltipContent>
</Tooltip>
</div>
{Object.entries(commands).map(([key, cmd]) => {
return (
<TabsContent key={key} value={key}>
<div className="bg-surface border-border/50 text-surface-foreground relative overflow-hidden border-t px-3 py-3">
<div className="no-scrollbar overflow-x-auto">
<code className="font-mono text-sm whitespace-nowrap">
{cmd}
</code>
</div>
</div>
</TabsContent>
)
})}
</Tabs>
</FieldGroup> </FieldGroup>
<Tabs
value={packageManager}
onValueChange={(value) => {
setConfig({
...config,
packageManager: value as "pnpm" | "npm" | "yarn" | "bun",
})
}}
className="bg-surface min-w-0 gap-0 overflow-hidden rounded-lg border"
>
<div className="flex items-center gap-2 p-2">
<TabsList className="*:data-[slot=tabs-trigger]:data-[state=active]:border-input h-auto rounded-none bg-transparent p-0 font-mono *:data-[slot=tabs-trigger]:border *:data-[slot=tabs-trigger]:border-transparent *:data-[slot=tabs-trigger]:pt-0.5 *:data-[slot=tabs-trigger]:shadow-none!">
<TabsTrigger value="pnpm">pnpm</TabsTrigger>
<TabsTrigger value="npm">npm</TabsTrigger>
<TabsTrigger value="yarn">yarn</TabsTrigger>
<TabsTrigger value="bun">bun</TabsTrigger>
</TabsList>
<Tooltip>
<TooltipTrigger asChild>
<Button
size="icon-sm"
variant="ghost"
className="ml-auto size-7 rounded-lg"
onClick={handleCopyFromTabs}
>
{hasCopied ? (
<HugeiconsIcon icon={Tick02Icon} className="size-4" />
) : (
<HugeiconsIcon icon={Copy01Icon} className="size-4" />
)}
<span className="sr-only">Copy command</span>
</Button>
</TooltipTrigger>
<TooltipContent>
{hasCopied ? "Copied!" : "Copy command"}
</TooltipContent>
</Tooltip>
</div>
{Object.entries(commands).map(([key, cmd]) => {
return (
<TabsContent key={key} value={key}>
<div className="bg-surface border-border/50 text-surface-foreground relative overflow-hidden border-t px-3 py-3">
<div className="no-scrollbar overflow-x-auto">
<code className="font-mono text-sm whitespace-nowrap">
{cmd}
</code>
</div>
</div>
</TabsContent>
)
})}
</Tabs>
<DialogFooter className="bg-muted/50 -mx-6 mt-2 -mb-6 flex flex-col gap-2 border-t p-6 sm:flex-col"> <DialogFooter className="bg-muted/50 -mx-6 mt-2 -mb-6 flex flex-col gap-2 border-t p-6 sm:flex-col">
<Button <Button
size="sm" size="sm"

View File

@@ -14,7 +14,7 @@ import {
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params" import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
export function V0Button({ className }: { className?: string }) { export function V0Button({ className }: { className?: string }) {
const [params] = useDesignSystemSearchParams() const [params, setParams] = useDesignSystemSearchParams()
const isMobile = useIsMobile() const isMobile = useIsMobile()
const isMounted = useMounted() const isMounted = useMounted()
@@ -32,7 +32,7 @@ export function V0Button({ className }: { className?: string }) {
size="sm" size="sm"
variant={isMobile ? "default" : "outline"} variant={isMobile ? "default" : "outline"}
className={cn( className={cn(
"w-24 rounded-lg shadow-none data-[variant=default]:h-[31px] lg:w-8 xl:w-24", "w-24 rounded-lg shadow-none data-[variant=default]:h-[31px]",
className className
)} )}
asChild asChild
@@ -41,8 +41,7 @@ export function V0Button({ className }: { className?: string }) {
href={`${process.env.NEXT_PUBLIC_V0_URL}/chat/api/open?url=${encodeURIComponent(url)}&title=${params.item}`} href={`${process.env.NEXT_PUBLIC_V0_URL}/chat/api/open?url=${encodeURIComponent(url)}&title=${params.item}`}
target="_blank" target="_blank"
> >
<span className="lg:hidden xl:block">Open in</span> Open in <Icons.v0 className="size-5" />
<Icons.v0 className="size-5" />
</a> </a>
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>

View File

@@ -4,11 +4,7 @@ import { ArrowLeftIcon } from "lucide-react"
import type { SearchParams } from "nuqs/server" import type { SearchParams } from "nuqs/server"
import { siteConfig } from "@/lib/config" import { siteConfig } from "@/lib/config"
import { source } from "@/lib/source"
import { absoluteUrl } from "@/lib/utils" import { absoluteUrl } from "@/lib/utils"
import { Icons } from "@/components/icons"
import { MainNav } from "@/components/main-nav"
import { MobileNav } from "@/components/mobile-nav"
import { ModeSwitcher } from "@/components/mode-switcher" import { ModeSwitcher } from "@/components/mode-switcher"
import { SiteConfig } from "@/components/site-config" import { SiteConfig } from "@/components/site-config"
import { BASES } from "@/registry/config" import { BASES } from "@/registry/config"
@@ -16,11 +12,10 @@ import { Button } from "@/registry/new-york-v4/ui/button"
import { Separator } from "@/registry/new-york-v4/ui/separator" import { Separator } from "@/registry/new-york-v4/ui/separator"
import { SidebarProvider } from "@/registry/new-york-v4/ui/sidebar" import { SidebarProvider } from "@/registry/new-york-v4/ui/sidebar"
import { Customizer } from "@/app/(create)/components/customizer" import { Customizer } from "@/app/(create)/components/customizer"
import { CustomizerControls } from "@/app/(create)/components/customizer-controls"
import { ItemExplorer } from "@/app/(create)/components/item-explorer" import { ItemExplorer } from "@/app/(create)/components/item-explorer"
import { ItemPicker } from "@/app/(create)/components/item-picker" import { ItemPicker } from "@/app/(create)/components/item-picker"
import { Preview } from "@/app/(create)/components/preview" import { Preview } from "@/app/(create)/components/preview"
import { RandomButton } from "@/app/(create)/components/random-button"
import { ResetButton } from "@/app/(create)/components/reset-button"
import { ShareButton } from "@/app/(create)/components/share-button" import { ShareButton } from "@/app/(create)/components/share-button"
import { ToolbarControls } from "@/app/(create)/components/toolbar-controls" import { ToolbarControls } from "@/app/(create)/components/toolbar-controls"
import { V0Button } from "@/app/(create)/components/v0-button" import { V0Button } from "@/app/(create)/components/v0-button"
@@ -68,7 +63,6 @@ export default async function CreatePage({
const params = await loadDesignSystemSearchParams(searchParams) const params = await loadDesignSystemSearchParams(searchParams)
const base = BASES.find((b) => b.name === params.base) ?? BASES[0] const base = BASES.find((b) => b.name === params.base) ?? BASES[0]
const pageTree = source.pageTree
const items = await getItemsForBase(base.name) const items = await getItemsForBase(base.name)
const filteredItems = items const filteredItems = items
@@ -87,34 +81,35 @@ export default async function CreatePage({
<header className="sticky top-0 z-50 w-full"> <header className="sticky top-0 z-50 w-full">
<div className="container-wrapper 3xl:fixed:px-0 px-6"> <div className="container-wrapper 3xl:fixed:px-0 px-6">
<div className="3xl:fixed:container flex h-(--header-height) items-center **:data-[slot=separator]:!h-4"> <div className="3xl:fixed:container flex h-(--header-height) items-center **:data-[slot=separator]:!h-4">
<div className="3xl:fixed:container flex h-(--header-height) items-center **:data-[slot=separator]:!h-4"> <div className="flex items-center xl:w-1/3">
<MobileNav
tree={pageTree}
items={siteConfig.navItems}
className="flex lg:hidden"
/>
<Button <Button
asChild asChild
variant="ghost" variant="outline"
size="icon" size="sm"
className="hidden size-8 lg:flex" className="rounded-lg shadow-none"
> >
<Link href="/"> <Link href="/">
<Icons.logo className="size-5" /> <ArrowLeftIcon />
<span className="sr-only">{siteConfig.name}</span> Back
</Link> </Link>
</Button> </Button>
<MainNav items={siteConfig.navItems} className="hidden lg:flex" /> <Separator
</div> orientation="vertical"
<div className="fixed inset-x-0 bottom-0 ml-auto flex flex-1 items-center justify-end gap-2 px-4.5 pb-4 sm:static sm:p-0 lg:ml-0"> className="mx-2 hidden sm:mx-4 lg:flex"
<ItemPicker items={filteredItems} /> />
<div className="items-center gap-0 sm:hidden"> <div className="text-muted-foreground hidden text-sm font-medium lg:flex">
<RandomButton /> New Project
<ResetButton />
</div> </div>
<Separator orientation="vertical" className="mr-2 flex" />
</div> </div>
<div className="ml-auto flex items-center gap-2 sm:ml-0 md:justify-end"> <div className="fixed inset-x-0 bottom-0 ml-auto flex flex-1 items-center gap-2 px-4.5 pb-4 sm:static sm:justify-end sm:p-0 lg:ml-0 xl:justify-center">
<ItemPicker items={filteredItems} />
<CustomizerControls className="sm:hidden" />
<Separator
orientation="vertical"
className="mr-2 hidden sm:flex xl:hidden"
/>
</div>
<div className="ml-auto flex items-center gap-2 sm:ml-0 md:justify-end xl:ml-auto xl:w-1/3">
<SiteConfig className="3xl:flex hidden" /> <SiteConfig className="3xl:flex hidden" />
<Separator orientation="vertical" className="3xl:flex hidden" /> <Separator orientation="vertical" className="3xl:flex hidden" />
<ModeSwitcher /> <ModeSwitcher />

View File

@@ -45,6 +45,18 @@ export async function GET(request: NextRequest) {
} }
const designSystemConfig = parseResult.data const designSystemConfig = parseResult.data
const registryBase = buildRegistryBase(designSystemConfig)
const validateResult = registryItemSchema.safeParse(registryBase)
if (!validateResult.success) {
return NextResponse.json(
{
error: "Invalid registry base item",
details: validateResult.error.format(),
},
{ status: 500 }
)
}
track("create_open_in_v0", designSystemConfig) track("create_open_in_v0", designSystemConfig)
@@ -63,23 +75,28 @@ export async function GET(request: NextRequest) {
} }
async function buildV0Payload(designSystemConfig: DesignSystemConfig) { async function buildV0Payload(designSystemConfig: DesignSystemConfig) {
const registryBase = buildRegistryBase(designSystemConfig) const files: z.infer<typeof registryItemFileSchema>[] = []
// Build all files in parallel. // Build globals.css file.
const [globalsCss, layoutFile, componentFiles] = await Promise.all([ files.push(buildGlobalsCss(designSystemConfig))
buildGlobalsCss(registryBase),
buildLayoutFile(designSystemConfig), // Build layout.tsx file.
buildComponentFiles(designSystemConfig), files.push(buildLayoutFile(designSystemConfig))
])
// Build component files.
const componentFiles = await buildComponentFiles(designSystemConfig)
files.push(...componentFiles)
return registryItemSchema.parse({ return registryItemSchema.parse({
name: designSystemConfig.item ?? "Item", name: designSystemConfig.item ?? "Item",
type: "registry:item", type: "registry:item",
files: [globalsCss, layoutFile, ...componentFiles], files,
}) })
} }
function buildGlobalsCss(registryBase: RegistryItem) { function buildGlobalsCss(designSystemConfig: DesignSystemConfig) {
const registryBase = buildRegistryBase(designSystemConfig)
const lightVars = Object.entries(registryBase.cssVars?.light ?? {}) const lightVars = Object.entries(registryBase.cssVars?.light ?? {})
.map(([key, value]) => ` --${key}: ${value};`) .map(([key, value]) => ` --${key}: ${value};`)
.join("\n") .join("\n")

View File

@@ -19,7 +19,6 @@ export async function GET(request: NextRequest) {
menuColor: searchParams.get("menuColor"), menuColor: searchParams.get("menuColor"),
radius: searchParams.get("radius"), radius: searchParams.get("radius"),
template: searchParams.get("template"), template: searchParams.get("template"),
rtl: searchParams.get("rtl") === "true",
}) })
if (!result.success) { if (!result.success) {

View File

@@ -38,15 +38,15 @@ const jetbrainsMono = JetBrains_Mono({
variable: "--font-jetbrains-mono", variable: "--font-jetbrains-mono",
}) })
const geistSans = Geist({ // const geistSans = Geist({
subsets: ["latin"], // subsets: ["latin"],
variable: "--font-geist-sans", // variable: "--font-geist-sans",
}) // })
const geistMono = Geist_Mono({ // const geistMono = Geist_Mono({
subsets: ["latin"], // subsets: ["latin"],
variable: "--font-geist-mono", // variable: "--font-geist-mono",
}) // })
const roboto = Roboto({ const roboto = Roboto({
subsets: ["latin"], subsets: ["latin"],
@@ -74,12 +74,12 @@ const outfit = Outfit({
}) })
export const FONTS = [ export const FONTS = [
{ // {
name: "Geist", // name: "Geist Sans",
value: "geist", // value: "geist",
font: geistSans, // font: geistSans,
type: "sans", // type: "sans",
}, // },
{ {
name: "Inter", name: "Inter",
value: "inter", value: "inter",
@@ -134,18 +134,18 @@ export const FONTS = [
font: outfit, font: outfit,
type: "sans", type: "sans",
}, },
{
name: "Geist Mono",
value: "geist-mono",
font: geistMono,
type: "mono",
},
{ {
name: "JetBrains Mono", name: "JetBrains Mono",
value: "jetbrains-mono", value: "jetbrains-mono",
font: jetbrainsMono, font: jetbrainsMono,
type: "mono", type: "mono",
}, },
// {
// name: "Geist Mono",
// value: "geist-mono",
// font: geistMono,
// type: "mono",
// },
] as const ] as const
export type Font = (typeof FONTS)[number] export type Font = (typeof FONTS)[number]

View File

@@ -66,7 +66,6 @@ const designSystemSearchParams = {
"start", "start",
"vite", "vite",
] as const).withDefault("next"), ] as const).withDefault("next"),
rtl: parseAsBoolean.withDefault(false),
size: parseAsInteger.withDefault(100), size: parseAsInteger.withDefault(100),
custom: parseAsBoolean.withDefault(false), custom: parseAsBoolean.withDefault(false),
} }

View File

@@ -7,10 +7,10 @@ import { absoluteUrl } from "@/lib/utils"
import { DarkModeScript } from "@/components/mode-switcher" import { DarkModeScript } from "@/components/mode-switcher"
import { TailwindIndicator } from "@/components/tailwind-indicator" import { TailwindIndicator } from "@/components/tailwind-indicator"
import { BASES, type Base } from "@/registry/config" import { BASES, type Base } from "@/registry/config"
import { RandomizeScript } from "@/app/(create)/components/customizer-controls"
import { DesignSystemProvider } from "@/app/(create)/components/design-system-provider" import { DesignSystemProvider } from "@/app/(create)/components/design-system-provider"
import { ItemPickerScript } from "@/app/(create)/components/item-picker" import { ItemPickerScript } from "@/app/(create)/components/item-picker"
import { PreviewStyle } from "@/app/(create)/components/preview-style" import { PreviewStyle } from "@/app/(create)/components/preview-style"
import { RandomizeScript } from "@/app/(create)/components/random-button"
import { getBaseComponent, getBaseItem } from "@/app/(create)/lib/api" import { getBaseComponent, getBaseItem } from "@/app/(create)/lib/api"
import { ALLOWED_ITEM_TYPES } from "@/app/(create)/lib/constants" import { ALLOWED_ITEM_TYPES } from "@/app/(create)/lib/constants"

View File

@@ -1,5 +1,3 @@
import { TrashIcon } from "lucide-react"
import { import {
AlertDialog, AlertDialog,
AlertDialogAction, AlertDialogAction,
@@ -8,7 +6,6 @@ import {
AlertDialogDescription, AlertDialogDescription,
AlertDialogFooter, AlertDialogFooter,
AlertDialogHeader, AlertDialogHeader,
AlertDialogMedia,
AlertDialogTitle, AlertDialogTitle,
AlertDialogTrigger, AlertDialogTrigger,
} from "@/registry/new-york-v4/ui/alert-dialog" } from "@/registry/new-york-v4/ui/alert-dialog"
@@ -16,66 +13,23 @@ import { Button } from "@/registry/new-york-v4/ui/button"
export function AlertDialogDemo() { export function AlertDialogDemo() {
return ( return (
<div className="flex flex-wrap gap-4"> <AlertDialog>
<AlertDialog> <AlertDialogTrigger asChild>
<AlertDialogTrigger asChild> <Button variant="outline">Show Dialog</Button>
<Button variant="outline">Default</Button> </AlertDialogTrigger>
</AlertDialogTrigger> <AlertDialogContent>
<AlertDialogContent> <AlertDialogHeader>
<AlertDialogHeader> <AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle> <AlertDialogDescription>
<AlertDialogDescription> This action cannot be undone. This will permanently delete your
This action cannot be undone. This will permanently delete your account and remove your data from our servers.
account and remove your data from our servers. </AlertDialogDescription>
</AlertDialogDescription> </AlertDialogHeader>
</AlertDialogHeader> <AlertDialogFooter>
<AlertDialogFooter> <AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogCancel>Cancel</AlertDialogCancel> <AlertDialogAction>Continue</AlertDialogAction>
<AlertDialogAction>Continue</AlertDialogAction> </AlertDialogFooter>
</AlertDialogFooter> </AlertDialogContent>
</AlertDialogContent> </AlertDialog>
</AlertDialog>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="outline">With Media</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogMedia>
<TrashIcon className="size-8" />
</AlertDialogMedia>
<AlertDialogTitle>Delete this item?</AlertDialogTitle>
<AlertDialogDescription>
This action cannot be undone. This will permanently delete the
item from your account.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction variant="destructive">Delete</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="outline">Small Size</Button>
</AlertDialogTrigger>
<AlertDialogContent size="sm">
<AlertDialogHeader>
<AlertDialogMedia>
<TrashIcon className="size-8" />
</AlertDialogMedia>
<AlertDialogTitle>Delete this item?</AlertDialogTitle>
<AlertDialogDescription>
This action cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction variant="destructive">Delete</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
) )
} }

View File

@@ -1,173 +1,89 @@
import { PlusIcon } from "lucide-react"
import { import {
Avatar, Avatar,
AvatarBadge,
AvatarFallback, AvatarFallback,
AvatarGroup,
AvatarGroupCount,
AvatarImage, AvatarImage,
} from "@/registry/new-york-v4/ui/avatar" } from "@/registry/new-york-v4/ui/avatar"
export function AvatarDemo() { export function AvatarDemo() {
return ( return (
<div className="flex flex-col gap-6"> <div className="flex flex-row flex-wrap items-center gap-4">
{/* Sizes. */} <Avatar>
<div className="flex flex-row flex-wrap items-center gap-4"> <AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
<Avatar size="sm"> <AvatarFallback>CN</AvatarFallback>
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" /> </Avatar>
<AvatarFallback>CN</AvatarFallback> <Avatar>
</Avatar> <AvatarFallback>CN</AvatarFallback>
</Avatar>
<Avatar className="size-12">
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
<AvatarFallback>CN</AvatarFallback>
</Avatar>
<Avatar className="rounded-lg">
<AvatarImage
src="https://github.com/evilrabbit.png"
alt="@evilrabbit"
/>
<AvatarFallback>ER</AvatarFallback>
</Avatar>
<div className="*:data-[slot=avatar]:ring-background flex -space-x-2 *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:grayscale">
<Avatar> <Avatar>
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" /> <AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
<AvatarFallback>CN</AvatarFallback> <AvatarFallback>CN</AvatarFallback>
</Avatar> </Avatar>
<Avatar size="lg"> <Avatar>
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" /> <AvatarImage
<AvatarFallback>CN</AvatarFallback> src="https://github.com/maxleiter.png"
</Avatar> alt="@maxleiter"
</div> />
{/* Fallback. */} <AvatarFallback>LR</AvatarFallback>
<div className="flex flex-row flex-wrap items-center gap-4">
<Avatar size="sm">
<AvatarFallback>CN</AvatarFallback>
</Avatar> </Avatar>
<Avatar> <Avatar>
<AvatarFallback>CN</AvatarFallback> <AvatarImage
</Avatar> src="https://github.com/evilrabbit.png"
<Avatar size="lg"> alt="@evilrabbit"
<AvatarFallback>CN</AvatarFallback> />
<AvatarFallback>ER</AvatarFallback>
</Avatar> </Avatar>
</div> </div>
{/* With badge. */} <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">
<div className="flex flex-row flex-wrap items-center gap-4">
<Avatar size="sm">
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
<AvatarFallback>CN</AvatarFallback>
<AvatarBadge />
</Avatar>
<Avatar> <Avatar>
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" /> <AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
<AvatarFallback>CN</AvatarFallback> <AvatarFallback>CN</AvatarFallback>
<AvatarBadge />
</Avatar> </Avatar>
<Avatar size="lg"> <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>
<div className="*:data-[slot=avatar]:ring-background flex -space-x-2 hover:space-x-1 *:data-[slot=avatar]:size-12 *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:grayscale *:data-[slot=avatar]:transition-all *:data-[slot=avatar]:duration-300 *:data-[slot=avatar]:ease-in-out">
<Avatar>
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" /> <AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
<AvatarFallback>CN</AvatarFallback> <AvatarFallback>CN</AvatarFallback>
<AvatarBadge>
<PlusIcon />
</AvatarBadge>
</Avatar> </Avatar>
</div> <Avatar>
{/* Avatar group. */} <AvatarImage
<div className="flex flex-row flex-wrap items-center gap-4"> src="https://github.com/maxleiter.png"
<AvatarGroup> alt="@maxleiter"
<Avatar size="sm"> />
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" /> <AvatarFallback>LR</AvatarFallback>
<AvatarFallback>CN</AvatarFallback> </Avatar>
</Avatar> <Avatar>
<Avatar size="sm"> <AvatarImage
<AvatarImage src="https://github.com/evilrabbit.png"
src="https://github.com/maxleiter.png" alt="@evilrabbit"
alt="@maxleiter" />
/> <AvatarFallback>ER</AvatarFallback>
<AvatarFallback>ML</AvatarFallback> </Avatar>
</Avatar>
<Avatar size="sm">
<AvatarImage
src="https://github.com/evilrabbit.png"
alt="@evilrabbit"
/>
<AvatarFallback>ER</AvatarFallback>
</Avatar>
</AvatarGroup>
<AvatarGroup>
<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>ML</AvatarFallback>
</Avatar>
<Avatar>
<AvatarImage
src="https://github.com/evilrabbit.png"
alt="@evilrabbit"
/>
<AvatarFallback>ER</AvatarFallback>
</Avatar>
</AvatarGroup>
<AvatarGroup>
<Avatar size="lg">
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
<AvatarFallback>CN</AvatarFallback>
</Avatar>
<Avatar size="lg">
<AvatarImage
src="https://github.com/maxleiter.png"
alt="@maxleiter"
/>
<AvatarFallback>ML</AvatarFallback>
</Avatar>
<Avatar size="lg">
<AvatarImage
src="https://github.com/evilrabbit.png"
alt="@evilrabbit"
/>
<AvatarFallback>ER</AvatarFallback>
</Avatar>
</AvatarGroup>
</div>
{/* Avatar group with count. */}
<div className="flex flex-row flex-wrap items-center gap-4">
<AvatarGroup>
<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>ML</AvatarFallback>
</Avatar>
<Avatar>
<AvatarImage
src="https://github.com/evilrabbit.png"
alt="@evilrabbit"
/>
<AvatarFallback>ER</AvatarFallback>
</Avatar>
<AvatarGroupCount>+3</AvatarGroupCount>
</AvatarGroup>
<AvatarGroup>
<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>ML</AvatarFallback>
</Avatar>
<Avatar>
<AvatarImage
src="https://github.com/evilrabbit.png"
alt="@evilrabbit"
/>
<AvatarFallback>ER</AvatarFallback>
</Avatar>
<AvatarGroupCount>
<PlusIcon />
</AvatarGroupCount>
</AvatarGroup>
</div> </div>
</div> </div>
) )

View File

@@ -10,10 +10,6 @@ export function BadgeDemo() {
<Badge variant="secondary">Secondary</Badge> <Badge variant="secondary">Secondary</Badge>
<Badge variant="destructive">Destructive</Badge> <Badge variant="destructive">Destructive</Badge>
<Badge variant="outline">Outline</Badge> <Badge variant="outline">Outline</Badge>
<Badge variant="ghost">Ghost</Badge>
<Badge variant="link">Link</Badge>
</div>
<div className="flex w-full flex-wrap gap-2">
<Badge variant="outline"> <Badge variant="outline">
<CheckIcon /> <CheckIcon />
Badge Badge
@@ -59,16 +55,6 @@ export function BadgeDemo() {
Link <ArrowRightIcon /> Link <ArrowRightIcon />
</a> </a>
</Badge> </Badge>
<Badge asChild variant="ghost">
<a href="#">
Link <ArrowRightIcon />
</a>
</Badge>
<Badge asChild variant="link">
<a href="#">
Link <ArrowRightIcon />
</a>
</Badge>
</div> </div>
</div> </div>
) )

View File

@@ -1,4 +1,4 @@
import { ArrowRightIcon, Loader2Icon, PlusIcon, SendIcon } from "lucide-react" import { ArrowRightIcon, Loader2Icon, SendIcon } from "lucide-react"
import { Button } from "@/registry/new-york-v4/ui/button" import { Button } from "@/registry/new-york-v4/ui/button"
@@ -6,25 +6,22 @@ export function ButtonDemo() {
return ( return (
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
<div className="flex flex-wrap items-center gap-2 md:flex-row"> <div className="flex flex-wrap items-center gap-2 md:flex-row">
<Button size="xs">Extra Small</Button> <Button>Button</Button>
<Button variant="outline" size="xs"> <Button variant="outline">Outline</Button>
Outline <Button variant="ghost">Ghost</Button>
</Button> <Button variant="destructive">Destructive</Button>
<Button variant="ghost" size="xs"> <Button variant="secondary">Secondary</Button>
Ghost <Button variant="link">Link</Button>
</Button> <Button variant="outline">
<Button variant="destructive" size="xs">
Destructive
</Button>
<Button variant="secondary" size="xs">
Secondary
</Button>
<Button variant="link" size="xs">
Link
</Button>
<Button variant="outline" size="xs">
<SendIcon /> Send <SendIcon /> Send
</Button> </Button>
<Button variant="outline">
Learn More <ArrowRightIcon />
</Button>
<Button disabled variant="outline">
<Loader2Icon className="animate-spin" />
Please wait
</Button>
</div> </div>
<div className="flex flex-wrap items-center gap-2 md:flex-row"> <div className="flex flex-wrap items-center gap-2 md:flex-row">
<Button size="sm">Small</Button> <Button size="sm">Small</Button>
@@ -46,21 +43,10 @@ export function ButtonDemo() {
<Button variant="outline" size="sm"> <Button variant="outline" size="sm">
<SendIcon /> Send <SendIcon /> Send
</Button> </Button>
</div> <Button variant="outline" size="sm">
<div className="flex flex-wrap items-center gap-2 md:flex-row">
<Button>Button</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="destructive">Destructive</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="link">Link</Button>
<Button variant="outline">
<SendIcon /> Send
</Button>
<Button variant="outline">
Learn More <ArrowRightIcon /> Learn More <ArrowRightIcon />
</Button> </Button>
<Button disabled variant="outline"> <Button disabled size="sm" variant="outline">
<Loader2Icon className="animate-spin" /> <Loader2Icon className="animate-spin" />
Please wait Please wait
</Button> </Button>
@@ -85,19 +71,12 @@ export function ButtonDemo() {
<Button variant="outline" size="lg"> <Button variant="outline" size="lg">
<SendIcon /> Send <SendIcon /> Send
</Button> </Button>
</div> <Button variant="outline" size="lg">
<div className="flex flex-wrap items-center gap-2 md:flex-row"> Learn More <ArrowRightIcon />
<Button size="icon-xs" variant="outline">
<PlusIcon />
</Button> </Button>
<Button size="icon-sm" variant="outline"> <Button disabled size="lg" variant="outline">
<PlusIcon /> <Loader2Icon className="animate-spin" />
</Button> Please wait
<Button size="icon" variant="outline">
<PlusIcon />
</Button>
<Button size="icon-lg" variant="outline">
<PlusIcon />
</Button> </Button>
</div> </div>
</div> </div>

View File

@@ -1,183 +1,405 @@
"use client" "use client"
import * as React from "react" import * as React from "react"
import {
CheckIcon,
ChevronDownIcon,
ChevronsUpDown,
PlusCircleIcon,
} from "lucide-react"
import { cn } from "@/lib/utils"
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@/registry/new-york-v4/ui/avatar"
import { Button } from "@/registry/new-york-v4/ui/button" import { Button } from "@/registry/new-york-v4/ui/button"
import { import {
Combobox, Command,
ComboboxChip, CommandEmpty,
ComboboxChips, CommandGroup,
ComboboxChipsInput, CommandInput,
ComboboxCollection, CommandItem,
ComboboxContent, CommandList,
ComboboxEmpty, CommandSeparator,
ComboboxGroup, } from "@/registry/new-york-v4/ui/command"
ComboboxInput, import {
ComboboxItem, Popover,
ComboboxLabel, PopoverContent,
ComboboxList, PopoverTrigger,
ComboboxTrigger, } from "@/registry/new-york-v4/ui/popover"
ComboboxValue,
useComboboxAnchor,
} from "@/registry/new-york-v4/ui/combobox"
const frameworks = [ const frameworks = [
"Next.js", {
"SvelteKit", value: "next.js",
"Nuxt.js", label: "Next.js",
"Remix", },
"Astro", {
value: "sveltekit",
label: "SvelteKit",
},
{
value: "nuxt.js",
label: "Nuxt.js",
},
{
value: "remix",
label: "Remix",
},
{
value: "astro",
label: "Astro",
},
]
type Framework = (typeof frameworks)[number]
const users = [
{
id: "1",
username: "shadcn",
},
{
id: "2",
username: "maxleiter",
},
{
id: "3",
username: "evilrabbit",
},
] as const ] as const
type User = (typeof users)[number]
const timezones = [ const timezones = [
{ {
value: "Americas", label: "Americas",
items: ["(GMT-5) New York", "(GMT-8) Los Angeles", "(GMT-6) Chicago"], timezones: [
{ value: "America/New_York", label: "(GMT-5) New York" },
{ value: "America/Los_Angeles", label: "(GMT-8) Los Angeles" },
{ value: "America/Chicago", label: "(GMT-6) Chicago" },
{ value: "America/Toronto", label: "(GMT-5) Toronto" },
{ value: "America/Vancouver", label: "(GMT-8) Vancouver" },
{ value: "America/Sao_Paulo", label: "(GMT-3) São Paulo" },
],
}, },
{ {
value: "Europe", label: "Europe",
items: ["(GMT+0) London", "(GMT+1) Paris", "(GMT+1) Berlin"], timezones: [
{ value: "Europe/London", label: "(GMT+0) London" },
{ value: "Europe/Paris", label: "(GMT+1) Paris" },
{ value: "Europe/Berlin", label: "(GMT+1) Berlin" },
{ value: "Europe/Rome", label: "(GMT+1) Rome" },
{ value: "Europe/Madrid", label: "(GMT+1) Madrid" },
{ value: "Europe/Amsterdam", label: "(GMT+1) Amsterdam" },
],
}, },
{ {
value: "Asia/Pacific", label: "Asia/Pacific",
items: ["(GMT+9) Tokyo", "(GMT+8) Shanghai", "(GMT+8) Singapore"], timezones: [
{ value: "Asia/Tokyo", label: "(GMT+9) Tokyo" },
{ value: "Asia/Shanghai", label: "(GMT+8) Shanghai" },
{ value: "Asia/Singapore", label: "(GMT+8) Singapore" },
{ value: "Asia/Dubai", label: "(GMT+4) Dubai" },
{ value: "Australia/Sydney", label: "(GMT+11) Sydney" },
{ value: "Asia/Seoul", label: "(GMT+9) Seoul" },
],
}, },
] as const ] as const
const countries = [ type Timezone = (typeof timezones)[number]
{ code: "", value: "", label: "Select country" },
{ code: "us", value: "united-states", label: "United States" },
{ code: "ca", value: "canada", label: "Canada" },
{ code: "gb", value: "united-kingdom", label: "United Kingdom" },
{ code: "de", value: "germany", label: "Germany" },
{ code: "fr", value: "france", label: "France" },
{ code: "jp", value: "japan", label: "Japan" },
]
export function ComboboxDemo() { export function ComboboxDemo() {
return ( return (
<div className="flex w-full flex-col gap-6"> <div className="flex w-full flex-wrap items-start gap-4">
{/* Basic combobox. */} <FrameworkCombobox frameworks={[...frameworks]} />
<div className="flex flex-wrap items-start gap-4"> <UserCombobox users={[...users]} selectedUserId={users[0].id} />
<Combobox items={frameworks}> <TimezoneCombobox
<ComboboxInput placeholder="Select a framework" /> timezones={[...timezones]}
<ComboboxContent> selectedTimezone={timezones[0].timezones[0]}
<ComboboxEmpty>No items found.</ComboboxEmpty> />
<ComboboxList> <ComboboxWithCheckbox frameworks={[...frameworks]} />
{(item) => (
<ComboboxItem key={item} value={item}>
{item}
</ComboboxItem>
)}
</ComboboxList>
</ComboboxContent>
</Combobox>
</div>
{/* With clear button. */}
<div className="flex flex-wrap items-start gap-4">
<Combobox items={frameworks} defaultValue={frameworks[0]}>
<ComboboxInput placeholder="Select a framework" showClear />
<ComboboxContent>
<ComboboxEmpty>No items found.</ComboboxEmpty>
<ComboboxList>
{(item) => (
<ComboboxItem key={item} value={item}>
{item}
</ComboboxItem>
)}
</ComboboxList>
</ComboboxContent>
</Combobox>
</div>
{/* With groups. */}
<div className="flex flex-wrap items-start gap-4">
<Combobox items={timezones}>
<ComboboxInput placeholder="Select a timezone" />
<ComboboxContent>
<ComboboxEmpty>No timezones found.</ComboboxEmpty>
<ComboboxList>
{(group) => (
<ComboboxGroup key={group.value} items={group.items}>
<ComboboxLabel>{group.value}</ComboboxLabel>
<ComboboxCollection>
{(item) => (
<ComboboxItem key={item} value={item}>
{item}
</ComboboxItem>
)}
</ComboboxCollection>
</ComboboxGroup>
)}
</ComboboxList>
</ComboboxContent>
</Combobox>
</div>
{/* With trigger button. */}
<div className="flex flex-wrap items-start gap-4">
<Combobox items={countries} defaultValue={countries[0]}>
<ComboboxTrigger
render={
<Button
variant="outline"
className="w-64 justify-between font-normal"
/>
}
>
<ComboboxValue />
</ComboboxTrigger>
<ComboboxContent>
<ComboboxInput showTrigger={false} placeholder="Search" />
<ComboboxEmpty>No items found.</ComboboxEmpty>
<ComboboxList>
{(item) => (
<ComboboxItem key={item.code} value={item}>
{item.label}
</ComboboxItem>
)}
</ComboboxList>
</ComboboxContent>
</Combobox>
</div>
{/* Multiple selection with chips. */}
<ComboboxMultiple />
</div> </div>
) )
} }
function ComboboxMultiple() { function FrameworkCombobox({ frameworks }: { frameworks: Framework[] }) {
const anchor = useComboboxAnchor() const [open, setOpen] = React.useState(false)
const [value, setValue] = React.useState("")
return ( return (
<div className="flex flex-wrap items-start gap-4"> <Popover open={open} onOpenChange={setOpen}>
<Combobox <PopoverTrigger asChild>
multiple <Button
autoHighlight variant="outline"
items={frameworks} role="combobox"
defaultValue={[frameworks[0]]} aria-expanded={open}
> className="w-full justify-between md:max-w-[200px]"
<ComboboxChips ref={anchor}> >
<ComboboxValue> {value
{(values) => ( ? frameworks.find((framework) => framework.value === value)?.label
<React.Fragment> : "Select framework..."}
{values.map((value: string) => ( <ChevronsUpDown className="text-muted-foreground" />
<ComboboxChip key={value}>{value}</ComboboxChip> </Button>
))} </PopoverTrigger>
<ComboboxChipsInput placeholder="Add framework..." /> <PopoverContent className="w-(--radix-popover-trigger-width) p-0">
</React.Fragment> <Command>
)} <CommandInput placeholder="Search framework..." />
</ComboboxValue> <CommandList>
</ComboboxChips> <CommandEmpty>No framework found.</CommandEmpty>
<ComboboxContent anchor={anchor}> <CommandGroup>
<ComboboxEmpty>No items found.</ComboboxEmpty> {frameworks.map((framework) => (
<ComboboxList> <CommandItem
{(item) => ( key={framework.value}
<ComboboxItem key={item} value={item}> value={framework.value}
{item} onSelect={(currentValue) => {
</ComboboxItem> setValue(currentValue === value ? "" : currentValue)
)} setOpen(false)
</ComboboxList> }}
</ComboboxContent> >
</Combobox> {framework.label}
</div> <CheckIcon
className={cn(
"ml-auto",
value === framework.value ? "opacity-100" : "opacity-0"
)}
/>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
)
}
function UserCombobox({
users,
selectedUserId,
}: {
users: User[]
selectedUserId: string
}) {
const [open, setOpen] = React.useState(false)
const [value, setValue] = React.useState(selectedUserId)
const selectedUser = React.useMemo(
() => users.find((user) => user.id === value),
[value, users]
)
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={open}
className="w-full justify-between px-2 md:max-w-[200px]"
>
{selectedUser ? (
<div className="flex items-center gap-2">
<Avatar className="size-5">
<AvatarImage
src={`https://github.com/${selectedUser.username}.png`}
/>
<AvatarFallback>{selectedUser.username[0]}</AvatarFallback>
</Avatar>
{selectedUser.username}
</div>
) : (
"Select user..."
)}
<ChevronsUpDown className="text-muted-foreground" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-(--radix-popover-trigger-width) p-0">
<Command>
<CommandInput placeholder="Search user..." />
<CommandList>
<CommandEmpty>No user found.</CommandEmpty>
<CommandGroup>
{users.map((user) => (
<CommandItem
key={user.id}
value={user.id}
onSelect={(currentValue) => {
setValue(currentValue === value ? "" : currentValue)
setOpen(false)
}}
>
<Avatar className="size-5">
<AvatarImage
src={`https://github.com/${user.username}.png`}
/>
<AvatarFallback>{user.username[0]}</AvatarFallback>
</Avatar>
{user.username}
<CheckIcon
className={cn(
"ml-auto",
value === user.id ? "opacity-100" : "opacity-0"
)}
/>
</CommandItem>
))}
</CommandGroup>
<CommandSeparator />
<CommandGroup>
<CommandItem>
<PlusCircleIcon />
Create user
</CommandItem>
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
)
}
function TimezoneCombobox({
timezones,
selectedTimezone,
}: {
timezones: Timezone[]
selectedTimezone: Timezone["timezones"][number]
}) {
const [open, setOpen] = React.useState(false)
const [value, setValue] = React.useState(selectedTimezone.value)
const selectedGroup = React.useMemo(
() =>
timezones.find((group) =>
group.timezones.find((tz) => tz.value === value)
),
[value, timezones]
)
const selectedTimezoneLabel = React.useMemo(
() => selectedGroup?.timezones.find((tz) => tz.value === value)?.label,
[value, selectedGroup]
)
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
className="h-12 w-full justify-between px-2.5 md:max-w-[200px]"
>
{selectedTimezone ? (
<div className="flex flex-col items-start gap-0.5">
<span className="text-muted-foreground text-xs font-normal">
{selectedGroup?.label}
</span>
<span>{selectedTimezoneLabel}</span>
</div>
) : (
"Select timezone"
)}
<ChevronDownIcon className="text-muted-foreground" />
</Button>
</PopoverTrigger>
<PopoverContent className="p-0" align="start">
<Command>
<CommandInput placeholder="Search timezone..." />
<CommandList className="scroll-pb-12">
<CommandEmpty>No timezone found.</CommandEmpty>
{timezones.map((region) => (
<CommandGroup key={region.label} heading={region.label}>
{region.timezones.map((timezone) => (
<CommandItem
key={timezone.value}
value={timezone.value}
onSelect={(currentValue) => {
setValue(
currentValue as Timezone["timezones"][number]["value"]
)
setOpen(false)
}}
>
{timezone.label}
<CheckIcon
className="ml-auto opacity-0 data-[selected=true]:opacity-100"
data-selected={value === timezone.value}
/>
</CommandItem>
))}
</CommandGroup>
))}
<CommandSeparator className="sticky bottom-10" />
<CommandGroup className="bg-popover sticky bottom-0">
<CommandItem>
<PlusCircleIcon />
Create timezone
</CommandItem>
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
)
}
function ComboboxWithCheckbox({ frameworks }: { frameworks: Framework[] }) {
const [open, setOpen] = React.useState(false)
const [selectedFrameworks, setSelectedFrameworks] = React.useState<
Framework[]
>([])
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={open}
className="w-fit min-w-[280px] justify-between"
>
{selectedFrameworks.length > 0
? selectedFrameworks.map((framework) => framework.label).join(", ")
: "Select frameworks (multi-select)..."}
<ChevronsUpDown className="text-muted-foreground" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[300px] p-0" align="start">
<Command>
<CommandInput placeholder="Search framework..." />
<CommandList>
<CommandEmpty>No framework found.</CommandEmpty>
<CommandGroup>
{frameworks.map((framework) => (
<CommandItem
key={framework.value}
value={framework.value}
onSelect={(currentValue) => {
setSelectedFrameworks(
selectedFrameworks.some((f) => f.value === currentValue)
? selectedFrameworks.filter(
(f) => f.value !== currentValue
)
: [...selectedFrameworks, framework]
)
}}
>
<div
className="border-input data-[selected=true]:border-primary data-[selected=true]:bg-primary data-[selected=true]:text-primary-foreground pointer-events-none size-4 shrink-0 rounded-[4px] border transition-all select-none *:[svg]:opacity-0 data-[selected=true]:*:[svg]:opacity-100"
data-selected={selectedFrameworks.some(
(f) => f.value === framework.value
)}
>
<CheckIcon className="size-3.5 text-current" />
</div>
{framework.label}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
) )
} }

View File

@@ -118,7 +118,6 @@ function DropdownMenuCheckboxes() {
tabler="IconUser" tabler="IconUser"
hugeicons="UserIcon" hugeicons="UserIcon"
phosphor="UserIcon" phosphor="UserIcon"
remixicon="RiUserLine"
/> />
Profile Profile
</DropdownMenuItem> </DropdownMenuItem>
@@ -128,7 +127,6 @@ function DropdownMenuCheckboxes() {
tabler="IconCreditCard" tabler="IconCreditCard"
hugeicons="CreditCardIcon" hugeicons="CreditCardIcon"
phosphor="CreditCardIcon" phosphor="CreditCardIcon"
remixicon="RiBankCardLine"
/> />
Billing Billing
</DropdownMenuItem> </DropdownMenuItem>
@@ -138,7 +136,6 @@ function DropdownMenuCheckboxes() {
tabler="IconSettings" tabler="IconSettings"
hugeicons="SettingsIcon" hugeicons="SettingsIcon"
phosphor="GearIcon" phosphor="GearIcon"
remixicon="RiSettings3Line"
/> />
Settings Settings
</DropdownMenuItem> </DropdownMenuItem>
@@ -174,7 +171,6 @@ function DropdownMenuCheckboxes() {
tabler="IconLogout" tabler="IconLogout"
hugeicons="LogoutIcon" hugeicons="LogoutIcon"
phosphor="SignOutIcon" phosphor="SignOutIcon"
remixicon="RiLogoutBoxLine"
/> />
Sign Out Sign Out
</DropdownMenuItem> </DropdownMenuItem>
@@ -231,7 +227,6 @@ function DropdownMenuWithAvatar() {
tabler="IconChevronsUpDown" tabler="IconChevronsUpDown"
hugeicons="ChevronUpDownIcon" hugeicons="ChevronUpDownIcon"
phosphor="CaretUpDownIcon" phosphor="CaretUpDownIcon"
remixicon="RiExpandUpDownLine"
className="text-muted-foreground ml-auto" className="text-muted-foreground ml-auto"
/> />
</Button> </Button>
@@ -262,7 +257,6 @@ function DropdownMenuWithAvatar() {
tabler="IconSparkles" tabler="IconSparkles"
hugeicons="SparklesIcon" hugeicons="SparklesIcon"
phosphor="SparklesIcon" phosphor="SparklesIcon"
remixicon="RiSparklingLine"
/> />
Upgrade to Pro Upgrade to Pro
</DropdownMenuItem> </DropdownMenuItem>
@@ -275,7 +269,6 @@ function DropdownMenuWithAvatar() {
tabler="IconBadgeCheck" tabler="IconBadgeCheck"
hugeicons="BadgeCheckIcon" hugeicons="BadgeCheckIcon"
phosphor="CheckCircleIcon" phosphor="CheckCircleIcon"
remixicon="RiVerifiedBadgeLine"
/> />
Account Account
</DropdownMenuItem> </DropdownMenuItem>
@@ -285,7 +278,6 @@ function DropdownMenuWithAvatar() {
tabler="IconCreditCard" tabler="IconCreditCard"
hugeicons="CreditCardIcon" hugeicons="CreditCardIcon"
phosphor="CreditCardIcon" phosphor="CreditCardIcon"
remixicon="RiBankCardLine"
/> />
Billing Billing
</DropdownMenuItem> </DropdownMenuItem>
@@ -295,7 +287,6 @@ function DropdownMenuWithAvatar() {
tabler="IconBell" tabler="IconBell"
hugeicons="BellIcon" hugeicons="BellIcon"
phosphor="BellIcon" phosphor="BellIcon"
remixicon="RiNotification3Line"
/> />
Notifications Notifications
</DropdownMenuItem> </DropdownMenuItem>
@@ -307,7 +298,6 @@ function DropdownMenuWithAvatar() {
tabler="IconLogout" tabler="IconLogout"
hugeicons="LogoutIcon" hugeicons="LogoutIcon"
phosphor="SignOutIcon" phosphor="SignOutIcon"
remixicon="RiLogoutBoxLine"
/> />
Sign Out Sign Out
</DropdownMenuItem> </DropdownMenuItem>
@@ -362,7 +352,6 @@ function DropdownMenuAvatarOnly() {
tabler="IconSparkles" tabler="IconSparkles"
hugeicons="SparklesIcon" hugeicons="SparklesIcon"
phosphor="SparklesIcon" phosphor="SparklesIcon"
remixicon="RiSparklingLine"
/> />
Upgrade to Pro Upgrade to Pro
</DropdownMenuItem> </DropdownMenuItem>
@@ -375,7 +364,6 @@ function DropdownMenuAvatarOnly() {
tabler="IconBadgeCheck" tabler="IconBadgeCheck"
hugeicons="BadgeCheckIcon" hugeicons="BadgeCheckIcon"
phosphor="CheckCircleIcon" phosphor="CheckCircleIcon"
remixicon="RiVerifiedBadgeLine"
/> />
Account Account
</DropdownMenuItem> </DropdownMenuItem>
@@ -385,7 +373,6 @@ function DropdownMenuAvatarOnly() {
tabler="IconCreditCard" tabler="IconCreditCard"
hugeicons="CreditCardIcon" hugeicons="CreditCardIcon"
phosphor="CreditCardIcon" phosphor="CreditCardIcon"
remixicon="RiBankCardLine"
/> />
Billing Billing
</DropdownMenuItem> </DropdownMenuItem>
@@ -395,7 +382,6 @@ function DropdownMenuAvatarOnly() {
tabler="IconBell" tabler="IconBell"
hugeicons="BellIcon" hugeicons="BellIcon"
phosphor="BellIcon" phosphor="BellIcon"
remixicon="RiNotification3Line"
/> />
Notifications Notifications
</DropdownMenuItem> </DropdownMenuItem>
@@ -407,7 +393,6 @@ function DropdownMenuAvatarOnly() {
tabler="IconLogout" tabler="IconLogout"
hugeicons="LogoutIcon" hugeicons="LogoutIcon"
phosphor="SignOutIcon" phosphor="SignOutIcon"
remixicon="RiLogoutBoxLine"
/> />
Sign Out Sign Out
</DropdownMenuItem> </DropdownMenuItem>
@@ -426,7 +411,6 @@ function DropdownMenuIconColor() {
tabler="IconDots" tabler="IconDots"
hugeicons="MoreHorizontalCircle01Icon" hugeicons="MoreHorizontalCircle01Icon"
phosphor="DotsThreeOutlineIcon" phosphor="DotsThreeOutlineIcon"
remixicon="RiMoreLine"
/> />
<span className="sr-only">Toggle menu</span> <span className="sr-only">Toggle menu</span>
</Button> </Button>
@@ -439,7 +423,6 @@ function DropdownMenuIconColor() {
tabler="IconPencil" tabler="IconPencil"
hugeicons="EditIcon" hugeicons="EditIcon"
phosphor="PencilIcon" phosphor="PencilIcon"
remixicon="RiPencilLine"
/> />
Edit Edit
</DropdownMenuItem> </DropdownMenuItem>
@@ -449,7 +432,6 @@ function DropdownMenuIconColor() {
tabler="IconShare" tabler="IconShare"
hugeicons="ShareIcon" hugeicons="ShareIcon"
phosphor="ShareIcon" phosphor="ShareIcon"
remixicon="RiShareLine"
/> />
Share Share
</DropdownMenuItem> </DropdownMenuItem>
@@ -460,7 +442,6 @@ function DropdownMenuIconColor() {
tabler="IconTrash" tabler="IconTrash"
hugeicons="DeleteIcon" hugeicons="DeleteIcon"
phosphor="TrashIcon" phosphor="TrashIcon"
remixicon="RiDeleteBinLine"
/> />
Delete Delete
</DropdownMenuItem> </DropdownMenuItem>

View File

@@ -4,64 +4,59 @@ import { Label } from "@/registry/new-york-v4/ui/label"
import { import {
Popover, Popover,
PopoverContent, PopoverContent,
PopoverDescription,
PopoverHeader,
PopoverTitle,
PopoverTrigger, PopoverTrigger,
} from "@/registry/new-york-v4/ui/popover" } from "@/registry/new-york-v4/ui/popover"
export function PopoverDemo() { export function PopoverDemo() {
return ( return (
<div className="flex gap-4"> <Popover>
<Popover> <PopoverTrigger asChild>
<PopoverTrigger asChild> <Button variant="outline">Open popover</Button>
<Button variant="outline">Open popover</Button> </PopoverTrigger>
</PopoverTrigger> <PopoverContent className="w-80" align="start">
<PopoverContent className="w-80" align="start"> <div className="grid gap-4">
<div className="grid gap-4"> <div className="grid gap-1.5">
<PopoverHeader> <h4 className="leading-none font-medium">Dimensions</h4>
<PopoverTitle>Dimensions</PopoverTitle> <p className="text-muted-foreground text-sm">
<PopoverDescription> Set the dimensions for the layer.
Set the dimensions for the layer. </p>
</PopoverDescription> </div>
</PopoverHeader> <div className="grid gap-2">
<div className="grid gap-2"> <div className="grid grid-cols-3 items-center gap-4">
<div className="grid grid-cols-3 items-center gap-4"> <Label htmlFor="width">Width</Label>
<Label htmlFor="width">Width</Label> <Input
<Input id="width"
id="width" defaultValue="100%"
defaultValue="100%" className="col-span-2 h-8"
className="col-span-2 h-8" />
/> </div>
</div> <div className="grid grid-cols-3 items-center gap-4">
<div className="grid grid-cols-3 items-center gap-4"> <Label htmlFor="maxWidth">Max. width</Label>
<Label htmlFor="maxWidth">Max. width</Label> <Input
<Input id="maxWidth"
id="maxWidth" defaultValue="300px"
defaultValue="300px" className="col-span-2 h-8"
className="col-span-2 h-8" />
/> </div>
</div> <div className="grid grid-cols-3 items-center gap-4">
<div className="grid grid-cols-3 items-center gap-4"> <Label htmlFor="height">Height</Label>
<Label htmlFor="height">Height</Label> <Input
<Input id="height"
id="height" defaultValue="25px"
defaultValue="25px" className="col-span-2 h-8"
className="col-span-2 h-8" />
/> </div>
</div> <div className="grid grid-cols-3 items-center gap-4">
<div className="grid grid-cols-3 items-center gap-4"> <Label htmlFor="maxHeight">Max. height</Label>
<Label htmlFor="maxHeight">Max. height</Label> <Input
<Input id="maxHeight"
id="maxHeight" defaultValue="none"
defaultValue="none" className="col-span-2 h-8"
className="col-span-2 h-8" />
/>
</div>
</div> </div>
</div> </div>
</PopoverContent> </div>
</Popover> </PopoverContent>
</div> </Popover>
) )
} }

View File

@@ -47,27 +47,6 @@ export function SheetDemo() {
</SheetFooter> </SheetFooter>
</SheetContent> </SheetContent>
</Sheet> </Sheet>
<Sheet>
<SheetTrigger asChild>
<Button variant="outline">No Close Button</Button>
</SheetTrigger>
<SheetContent showCloseButton={false}>
<SheetHeader>
<SheetTitle>Custom Close</SheetTitle>
<SheetDescription>
This sheet has no default close button. Use the footer buttons
instead.
</SheetDescription>
</SheetHeader>
<div className="flex-1 px-4" />
<SheetFooter>
<SheetClose asChild>
<Button variant="outline">Cancel</Button>
</SheetClose>
<Button type="submit">Save</Button>
</SheetFooter>
</SheetContent>
</Sheet>
<div className="flex gap-2"> <div className="flex gap-2">
{SHEET_SIDES.map((side) => ( {SHEET_SIDES.map((side) => (
<Sheet key={side}> <Sheet key={side}>

View File

@@ -4,17 +4,6 @@ import { Switch } from "@/registry/new-york-v4/ui/switch"
export function SwitchDemo() { export function SwitchDemo() {
return ( return (
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
{/* Sizes. */}
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<Switch id="switch-demo-sm" size="sm" />
<Label htmlFor="switch-demo-sm">Small</Label>
</div>
<div className="flex items-center gap-2">
<Switch id="switch-demo-default" />
<Label htmlFor="switch-demo-default">Default</Label>
</div>
</div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Switch id="switch-demo-airplane-mode" /> <Switch id="switch-demo-airplane-mode" />
<Label htmlFor="switch-demo-airplane-mode">Airplane Mode</Label> <Label htmlFor="switch-demo-airplane-mode">Airplane Mode</Label>

View File

@@ -101,45 +101,6 @@ export function TabsDemo() {
</TabsTrigger> </TabsTrigger>
</TabsList> </TabsList>
</Tabs> </Tabs>
{/* Line variant. */}
<Tabs defaultValue="preview">
<TabsList variant="line">
<TabsTrigger value="preview">
<AppWindowIcon />
Preview
</TabsTrigger>
<TabsTrigger value="code">
<CodeIcon />
Code
</TabsTrigger>
</TabsList>
</Tabs>
{/* Vertical orientation. */}
<Tabs defaultValue="preview" orientation="vertical">
<TabsList>
<TabsTrigger value="preview">
<AppWindowIcon />
Preview
</TabsTrigger>
<TabsTrigger value="code">
<CodeIcon />
Code
</TabsTrigger>
</TabsList>
</Tabs>
{/* Vertical orientation with line variant. */}
<Tabs defaultValue="preview" orientation="vertical">
<TabsList variant="line">
<TabsTrigger value="preview">
<AppWindowIcon />
Preview
</TabsTrigger>
<TabsTrigger value="code">
<CodeIcon />
Code
</TabsTrigger>
</TabsList>
</Tabs>
</div> </div>
) )
} }

View File

@@ -1,13 +1,5 @@
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
export function ComponentPreview({ children }: { children: React.ReactNode }) { export function ComponentPreview({ children }: { children: React.ReactNode }) {
return ( return <div className={cn("bg-background")}>{children}</div>
<div
className={cn(
"bg-background *:data-[slot=card]:has-[[data-slot=chart]]:shadow-none"
)}
>
{children}
</div>
)
} }

View File

@@ -4,16 +4,10 @@ import { type Metadata } from "next"
import { notFound } from "next/navigation" import { notFound } from "next/navigation"
import { siteConfig } from "@/lib/config" import { siteConfig } from "@/lib/config"
import { import { getRegistryComponent, getRegistryItem } from "@/lib/registry"
getDemoItem,
getRegistryComponent,
getRegistryItem,
} from "@/lib/registry"
import { absoluteUrl } from "@/lib/utils" import { absoluteUrl } from "@/lib/utils"
import { getStyle, legacyStyles, type Style } from "@/registry/_legacy-styles" import { getStyle, legacyStyles, type Style } from "@/registry/_legacy-styles"
import "@/styles/legacy-themes.css"
import { ComponentPreview } from "./component-preview" import { ComponentPreview } from "./component-preview"
export const revalidate = false export const revalidate = false
@@ -22,12 +16,7 @@ export const dynamicParams = false
const getCachedRegistryItem = React.cache( const getCachedRegistryItem = React.cache(
async (name: string, styleName: Style["name"]) => { async (name: string, styleName: Style["name"]) => {
// Try registry item first, then fallback to demo item (for examples). return await getRegistryItem(name, styleName)
const item = await getRegistryItem(name, styleName)
if (item) {
return item
}
return await getDemoItem(name, styleName)
} }
) )
@@ -84,54 +73,9 @@ export async function generateMetadata({
export async function generateStaticParams() { export async function generateStaticParams() {
const { Index } = await import("@/registry/__index__") const { Index } = await import("@/registry/__index__")
// const { Index: BasesIndex } = await import("@/registry/bases/__index__")
const { ExamplesIndex } = await import("@/examples/__index__")
const params: Array<{ style: string; name: string }> = [] const params: Array<{ style: string; name: string }> = []
for (const style of legacyStyles) { for (const style of legacyStyles) {
// Check if this is a base-prefixed style (e.g., base-nova, radix-nova).
const baseMatch = style.name.match(/^(base|radix)-/)
if (baseMatch) {
const baseName = baseMatch[1]
// Add examples from ExamplesIndex.
const examples = ExamplesIndex[baseName]
if (examples) {
for (const exampleName of Object.keys(examples)) {
if (exampleName.startsWith("sidebar-")) {
params.push({
style: style.name,
name: exampleName,
})
}
}
}
// // Add UI components from BasesIndex.
// const baseIndex = BasesIndex[baseName]
// if (baseIndex) {
// for (const itemName in baseIndex) {
// const item = baseIndex[itemName]
// if (
// [
// "registry:block",
// "registry:component",
// "registry:example",
// "registry:internal",
// ].includes(item.type)
// ) {
// params.push({
// style: style.name,
// name: item.name,
// })
// }
// }
// }
continue
}
// Handle legacy styles (e.g., new-york-v4).
if (!Index[style.name]) { if (!Index[style.name]) {
continue continue
} }

View File

@@ -57,11 +57,6 @@ export const metadata: Metadata = {
apple: "/apple-touch-icon.png", apple: "/apple-touch-icon.png",
}, },
manifest: `${siteConfig.url}/site.webmanifest`, manifest: `${siteConfig.url}/site.webmanifest`,
alternates: {
types: {
"application/rss+xml": `${siteConfig.url}/rss.xml`,
},
},
} }
export default function RootLayout({ export default function RootLayout({

View File

@@ -1,44 +0,0 @@
import { NextResponse } from "next/server"
import { getChangelogPages, type ChangelogPageData } from "@/lib/changelog"
import { siteConfig } from "@/lib/config"
export const revalidate = false
export async function GET() {
const pages = getChangelogPages()
const items = pages
.map((page) => {
const data = page.data as ChangelogPageData
const date = page.date?.toUTCString() ?? new Date().toUTCString()
const link = `${siteConfig.url}/docs/${page.slugs.join("/")}`
return ` <item>
<title><![CDATA[${data.title}]]></title>
<link>${link}</link>
<guid>${link}</guid>
<description><![CDATA[${data.description || ""}]]></description>
<pubDate>${date}</pubDate>
</item>`
})
.join("\n")
const xml = `<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>${siteConfig.name} Changelog</title>
<link>${siteConfig.url}</link>
<description>${siteConfig.description}</description>
<language>en-us</language>
<atom:link href="${siteConfig.url}/rss.xml" rel="self" type="application/rss+xml"/>
${items}
</channel>
</rss>`
return new NextResponse(xml, {
headers: {
"Content-Type": "application/rss+xml; charset=utf-8",
},
})
}

View File

@@ -3,26 +3,12 @@ import { ArrowRightIcon } from "lucide-react"
import { Badge } from "@/registry/new-york-v4/ui/badge" import { Badge } from "@/registry/new-york-v4/ui/badge"
function BaseUILogo() {
return (
<svg width="17" height="24" viewBox="0 0 17 24" className="size-3">
<path
fill="currentColor"
d="M9.5001 7.01537C9.2245 6.99837 9 7.22385 9 7.49999V23C13.4183 23 17 19.4183 17 15C17 10.7497 13.6854 7.27351 9.5001 7.01537Z"
/>
<path
fill="currentColor"
d="M8 9.8V12V23C3.58172 23 0 19.0601 0 14.2V12V1C4.41828 1 8 4.93989 8 9.8Z"
/>
</svg>
)
}
export function Announcement() { export function Announcement() {
return ( return (
<Badge asChild variant="secondary" className="bg-muted"> <Badge asChild variant="secondary" className="bg-transparent">
<Link href="/docs/changelog/2026-01-rtl"> <Link href="/docs/changelog">
RTL Support <ArrowRightIcon /> <span className="flex size-2 rounded-full bg-blue-500" title="New" />
npx shadcn create <ArrowRightIcon />
</Link> </Link>
</Badge> </Badge>
) )

View File

@@ -20,7 +20,7 @@ export function Callout({
<Alert <Alert
data-variant={variant} data-variant={variant}
className={cn( className={cn(
"bg-surface text-surface-foreground border-surface mt-6 w-auto rounded-xl md:-mx-1 **:[code]:border", "bg-background text-foreground mt-6 w-auto border md:-mx-1",
className className
)} )}
{...props} {...props}

View File

@@ -5,7 +5,6 @@ import { type z } from "zod"
import { highlightCode } from "@/lib/highlight-code" import { highlightCode } from "@/lib/highlight-code"
import { getRegistryItem } from "@/lib/registry" import { getRegistryItem } from "@/lib/registry"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { ChartIframe } from "@/components/chart-iframe"
import { ChartToolbar } from "@/components/chart-toolbar" import { ChartToolbar } from "@/components/chart-toolbar"
import { type Style } from "@/registry/_legacy-styles" import { type Style } from "@/registry/_legacy-styles"
@@ -13,43 +12,50 @@ export type Chart = z.infer<typeof registryItemSchema> & {
highlightedCode: string highlightedCode: string
} }
export function ChartDisplay({ export async function ChartDisplay({
chart, name,
style, styleName,
children,
className, className,
}: { }: {
chart: Chart name: string
style: string styleName: Style["name"]
} & React.ComponentProps<"div">) { } & React.ComponentProps<"div">) {
const chart = await getCachedRegistryItem(name, styleName)
const highlightedCode = await getChartHighlightedCode(
chart?.files?.[0]?.content ?? ""
)
if (!chart || !highlightedCode) {
return null
}
return ( return (
<div <div
className={cn( className={cn(
"themes-wrapper group relative flex flex-col overflow-hidden rounded-xl transition-all duration-200 ease-in-out hover:z-30", "themes-wrapper group relative flex flex-col overflow-hidden rounded-xl border transition-all duration-200 ease-in-out hover:z-30",
className className
)} )}
> >
<ChartToolbar <ChartToolbar
chart={chart} chart={{ ...chart, highlightedCode }}
className="relative z-20 flex justify-end px-3 py-2.5" className="bg-card text-card-foreground relative z-20 flex justify-end border-b px-3 py-2.5"
/> >
<div className="bg-background relative z-10 overflow-hidden rounded-xl"> {children}
<ChartIframe </ChartToolbar>
src={`/view/${style}/${chart.name}`} <div className="relative z-10 [&>div]:rounded-none [&>div]:border-none [&>div]:shadow-none">
height={460} {children}
title={chart.name}
/>
</div> </div>
</div> </div>
) )
} }
// Exported for parallel prefetching in page components. const getCachedRegistryItem = React.cache(
export const getCachedRegistryItem = React.cache(
async (name: string, styleName: Style["name"]) => { async (name: string, styleName: Style["name"]) => {
return await getRegistryItem(name, styleName) return await getRegistryItem(name, styleName)
} }
) )
export const getChartHighlightedCode = React.cache(async (content: string) => { const getChartHighlightedCode = React.cache(async (content: string) => {
return await highlightCode(content) return await highlightCode(content)
}) })

View File

@@ -1,31 +0,0 @@
"use client"
import * as React from "react"
import { cn } from "@/lib/utils"
export function ChartIframe({
src,
height,
title,
}: {
src: string
height: number
title: string
}) {
const [loaded, setLoaded] = React.useState(false)
return (
<iframe
src={src}
className={cn(
"w-full border-none transition-opacity duration-300",
loaded ? "opacity-100" : "opacity-0"
)}
height={height}
loading="lazy"
title={title}
onLoad={() => setLoaded(true)}
/>
)
}

View File

@@ -46,7 +46,7 @@ export function ChartToolbar({
} }
function ChartTitle({ chart }: { chart: Chart }) { function ChartTitle({ chart }: { chart: Chart }) {
if (chart.name.includes("chart-line")) { if (chart.name.includes("charts-line")) {
return ( return (
<> <>
<LineChartIcon /> Line Chart <LineChartIcon /> Line Chart

View File

@@ -88,7 +88,7 @@ export function CodeBlockCommand({
<TabsTrigger <TabsTrigger
key={key} key={key}
value={key} value={key}
className="data-[state=active]:bg-background! data-[state=active]:border-input h-7 border border-transparent pt-0.5 shadow-none!" className="data-[state=active]:bg-accent data-[state=active]:border-input h-7 border border-transparent pt-0.5 data-[state=active]:shadow-none"
> >
{key} {key}
</TabsTrigger> </TabsTrigger>

View File

@@ -18,7 +18,7 @@ export function CodeTabs({ children }: React.ComponentProps<typeof Tabs>) {
onValueChange={(value) => onValueChange={(value) =>
setConfig({ ...config, installationType: value as "cli" | "manual" }) setConfig({ ...config, installationType: value as "cli" | "manual" })
} }
className="relative mt-6 w-full *:data-[slot=tabs-list]:gap-6" className="relative mt-6 w-full"
> >
{children} {children}
</Tabs> </Tabs>

View File

@@ -1,16 +1,15 @@
"use client" "use client"
import * as React from "react" import * as React from "react"
import { usePathname, useRouter } from "next/navigation" import { useRouter } from "next/navigation"
import { type DialogProps } from "@radix-ui/react-dialog"
import { IconArrowRight } from "@tabler/icons-react" import { IconArrowRight } from "@tabler/icons-react"
import { useDocsSearch } from "fumadocs-core/search/client" import { useDocsSearch } from "fumadocs-core/search/client"
import { CornerDownLeftIcon, SquareDashedIcon } from "lucide-react" import { CornerDownLeftIcon, SquareDashedIcon } from "lucide-react"
import { Dialog as DialogPrimitive } from "radix-ui"
import { type Color, type ColorPalette } from "@/lib/colors" import { type Color, type ColorPalette } from "@/lib/colors"
import { trackEvent } from "@/lib/events" import { trackEvent } from "@/lib/events"
import { showMcpDocs } from "@/lib/flags" import { showMcpDocs } from "@/lib/flags"
import { getCurrentBase, getPagesFromFolder } from "@/lib/page-tree"
import { type source } from "@/lib/source" import { type source } from "@/lib/source"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { useConfig } from "@/hooks/use-config" import { useConfig } from "@/hooks/use-config"
@@ -27,13 +26,13 @@ import {
} from "@/registry/new-york-v4/ui/command" } from "@/registry/new-york-v4/ui/command"
import { import {
Dialog, Dialog,
DialogContent,
DialogDescription, DialogDescription,
DialogHeader, DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
} from "@/registry/new-york-v4/ui/dialog" } from "@/registry/new-york-v4/ui/dialog"
import { Kbd, KbdGroup } from "@/registry/new-york-v4/ui/kbd"
import { Separator } from "@/registry/new-york-v4/ui/separator" import { Separator } from "@/registry/new-york-v4/ui/separator"
import { Spinner } from "@/registry/new-york-v4/ui/spinner" import { Spinner } from "@/registry/new-york-v4/ui/spinner"
@@ -43,18 +42,15 @@ export function CommandMenu({
blocks, blocks,
navItems, navItems,
...props ...props
}: React.ComponentProps<typeof Dialog> & { }: DialogProps & {
tree: typeof source.pageTree tree: typeof source.pageTree
colors: ColorPalette[] colors: ColorPalette[]
blocks?: { name: string; description: string; categories: string[] }[] blocks?: { name: string; description: string; categories: string[] }[]
navItems?: { href: string; label: string }[] navItems?: { href: string; label: string }[]
}) { }) {
const router = useRouter() const router = useRouter()
const pathname = usePathname()
const [config] = useConfig() const [config] = useConfig()
const currentBase = getCurrentBase(pathname)
const [open, setOpen] = React.useState(false) const [open, setOpen] = React.useState(false)
const [renderDelayedGroups, setRenderDelayedGroups] = React.useState(false)
const [selectedType, setSelectedType] = React.useState< const [selectedType, setSelectedType] = React.useState<
"color" | "page" | "component" | "block" | null "color" | "page" | "component" | "block" | null
>(null) >(null)
@@ -94,30 +90,14 @@ export function CommandMenu({
// Set new timeout to debounce both search and tracking. // Set new timeout to debounce both search and tracking.
searchTimeoutRef.current = setTimeout(() => { searchTimeoutRef.current = setTimeout(() => {
React.startTransition(() => { setSearch(value)
setSearch(value) trackSearchQuery(value)
trackSearchQuery(value)
})
}, 500) }, 500)
}, },
[setSearch, trackSearchQuery] [setSearch, trackSearchQuery]
) )
// Cleanup timeout on unmount. // Cleanup timeout on unmount.
React.useEffect(() => {
if (open) {
const frame = requestAnimationFrame(() => {
setRenderDelayedGroups(true)
})
return () => {
cancelAnimationFrame(frame)
}
}
setRenderDelayedGroups(false)
}, [open])
React.useEffect(() => { React.useEffect(() => {
return () => { return () => {
if (searchTimeoutRef.current) { if (searchTimeoutRef.current) {
@@ -126,17 +106,6 @@ export function CommandMenu({
} }
}, []) }, [])
const commandFilter = React.useCallback(
(value: string, searchValue: string, keywords?: string[]) => {
const extendValue = value + " " + (keywords?.join(" ") || "")
if (extendValue.toLowerCase().includes(searchValue.toLowerCase())) {
return 1
}
return 0
},
[]
)
const handlePageHighlight = React.useCallback( const handlePageHighlight = React.useCallback(
(isComponent: boolean, item: { url: string; name?: React.ReactNode }) => { (isComponent: boolean, item: { url: string; name?: React.ReactNode }) => {
if (isComponent) { if (isComponent) {
@@ -169,175 +138,10 @@ export function CommandMenu({
[setSelectedType, setCopyPayload, packageManager] [setSelectedType, setCopyPayload, packageManager]
) )
const runCommand = React.useCallback( const runCommand = React.useCallback((command: () => unknown) => {
(command: () => unknown) => { setOpen(false)
setOpen(false) command()
command() }, [])
},
[setOpen]
)
const navItemsSection = React.useMemo(() => {
if (!navItems || navItems.length === 0) {
return null
}
return (
<CommandGroup
heading="Pages"
className="!p-0 [&_[cmdk-group-heading]]:scroll-mt-16 [&_[cmdk-group-heading]]:!p-3 [&_[cmdk-group-heading]]:!pb-1"
>
{navItems.map((item) => (
<CommandMenuItem
key={item.href}
value={`Navigation ${item.label}`}
keywords={["nav", "navigation", item.label.toLowerCase()]}
onHighlight={() => {
setSelectedType("page")
setCopyPayload("")
}}
onSelect={() => {
runCommand(() => router.push(item.href))
}}
>
<IconArrowRight />
{item.label}
</CommandMenuItem>
))}
</CommandGroup>
)
}, [navItems, runCommand, router])
const pageGroupsSection = React.useMemo(() => {
return tree.children.map((group) => {
if (group.type !== "folder") {
return null
}
const pages = getPagesFromFolder(group, currentBase).filter((item) => {
if (!showMcpDocs && item.url.includes("/mcp")) {
return false
}
return true
})
if (pages.length === 0) {
return null
}
return (
<CommandGroup
key={group.$id}
heading={group.name}
className="!p-0 [&_[cmdk-group-heading]]:scroll-mt-16 [&_[cmdk-group-heading]]:!p-3 [&_[cmdk-group-heading]]:!pb-1"
>
{pages.map((item) => {
const isComponent = item.url.includes("/components/")
return (
<CommandMenuItem
key={item.url}
value={
item.name?.toString() ? `${group.name} ${item.name}` : ""
}
keywords={isComponent ? ["component"] : undefined}
onHighlight={() => handlePageHighlight(isComponent, item)}
onSelect={() => {
runCommand(() => router.push(item.url))
}}
>
{isComponent ? (
<div className="border-muted-foreground aspect-square size-4 rounded-full border border-dashed" />
) : (
<IconArrowRight />
)}
{item.name}
</CommandMenuItem>
)
})}
</CommandGroup>
)
})
}, [tree.children, currentBase, handlePageHighlight, runCommand, router])
const colorGroupsSection = React.useMemo(() => {
return colors.map((colorPalette) => (
<CommandGroup
key={colorPalette.name}
heading={
colorPalette.name.charAt(0).toUpperCase() + colorPalette.name.slice(1)
}
className="!p-0 [&_[cmdk-group-heading]]:!p-3"
>
{colorPalette.colors.map((color) => (
<CommandMenuItem
key={color.hex}
value={color.className}
keywords={["color", color.name, color.className]}
onHighlight={() => handleColorHighlight(color)}
onSelect={() => {
runCommand(() =>
copyToClipboardWithMeta(color.oklch, {
name: "copy_color",
properties: { color: color.oklch },
})
)
}}
>
<div
className="border-ghost aspect-square size-4 rounded-sm bg-(--color) after:rounded-sm"
style={{ "--color": color.oklch } as React.CSSProperties}
/>
{color.className}
<span className="text-muted-foreground ml-auto font-mono text-xs font-normal tabular-nums">
{color.oklch}
</span>
</CommandMenuItem>
))}
</CommandGroup>
))
}, [colors, handleColorHighlight, runCommand])
const blocksSection = React.useMemo(() => {
if (!blocks || blocks.length === 0) {
return null
}
return (
<CommandGroup
heading="Blocks"
className="!p-0 [&_[cmdk-group-heading]]:!p-3"
>
{blocks.map((block) => (
<CommandMenuItem
key={block.name}
value={block.name}
onHighlight={() => {
handleBlockHighlight(block)
}}
keywords={[
"block",
block.name,
block.description,
...block.categories,
]}
onSelect={() => {
runCommand(() =>
router.push(`/blocks/${block.categories[0]}#${block.name}`)
)
}}
>
<SquareDashedIcon />
{block.description}
<span className="text-muted-foreground ml-auto font-mono text-xs font-normal tabular-nums">
{block.name}
</span>
</CommandMenuItem>
))}
</CommandGroup>
)
}, [blocks, handleBlockHighlight, runCommand, router])
React.useEffect(() => { React.useEffect(() => {
const down = (e: KeyboardEvent) => { const down = (e: KeyboardEvent) => {
@@ -391,29 +195,39 @@ export function CommandMenu({
<Button <Button
variant="outline" variant="outline"
className={cn( className={cn(
"text-foreground dark:bg-card hover:bg-muted/50 relative h-8 w-full justify-start rounded-lg pl-3 font-normal shadow-none sm:pr-12 md:w-48 lg:w-56 xl:w-64" "text-foreground dark:bg-card hover:bg-muted/50 relative h-8 w-full justify-start pl-3 font-normal shadow-none sm:pr-12 md:w-48 lg:w-56 xl:w-64"
)} )}
onClick={() => setOpen(true)} onClick={() => setOpen(true)}
{...props} {...props}
> >
<span className="hidden lg:inline-flex">Search documentation...</span> <span className="hidden lg:inline-flex">Search documentation...</span>
<span className="inline-flex lg:hidden">Search...</span> <span className="inline-flex lg:hidden">Search...</span>
<div className="absolute top-1.5 right-1.5 hidden gap-1 group-has-[[data-slot=designer]]/body:hidden sm:flex">
<Kbd>K</Kbd>
</div>
</Button> </Button>
</DialogTrigger> </DialogTrigger>
<DialogContent className="rounded-xl border-none bg-clip-padding p-2 pb-11 shadow-2xl ring-4 ring-neutral-200/80 dark:bg-neutral-900 dark:ring-neutral-800"> <DialogContent
showCloseButton={false}
className="rounded-xl border-none bg-clip-padding p-2 pb-11 shadow-2xl ring-4 ring-neutral-200/80 dark:bg-neutral-900 dark:ring-neutral-800"
>
<DialogHeader className="sr-only"> <DialogHeader className="sr-only">
<DialogTitle>Search documentation...</DialogTitle> <DialogTitle>Search documentation...</DialogTitle>
<DialogDescription>Search for a command to run...</DialogDescription> <DialogDescription>Search for a command to run...</DialogDescription>
</DialogHeader> </DialogHeader>
<Command <Command
className="**:data-[slot=command-input-wrapper]:bg-input/50 **:data-[slot=command-input-wrapper]:border-input rounded-none bg-transparent **:data-[slot=command-input]:!h-9 **:data-[slot=command-input]:py-0 **:data-[slot=command-input-wrapper]:mb-0 **:data-[slot=command-input-wrapper]:!h-9 **:data-[slot=command-input-wrapper]:rounded-md **:data-[slot=command-input-wrapper]:border" className="**:data-[slot=command-input-wrapper]:bg-input/50 **:data-[slot=command-input-wrapper]:border-input rounded-none bg-transparent **:data-[slot=command-input]:!h-9 **:data-[slot=command-input]:py-0 **:data-[slot=command-input-wrapper]:mb-0 **:data-[slot=command-input-wrapper]:!h-9 **:data-[slot=command-input-wrapper]:rounded-md **:data-[slot=command-input-wrapper]:border"
filter={commandFilter} filter={(value, search, keywords) => {
handleSearchChange(search)
const extendValue = value + " " + (keywords?.join(" ") || "")
if (extendValue.toLowerCase().includes(search.toLowerCase())) {
return 1
}
return 0
}}
> >
<div className="relative"> <div className="relative">
<CommandInput <CommandInput placeholder="Search documentation..." />
placeholder="Search documentation..."
onValueChange={handleSearchChange}
/>
{query.isLoading && ( {query.isLoading && (
<div className="pointer-events-none absolute top-1/2 right-3 z-10 flex -translate-y-1/2 items-center justify-center"> <div className="pointer-events-none absolute top-1/2 right-3 z-10 flex -translate-y-1/2 items-center justify-center">
<Spinner className="text-muted-foreground size-4" /> <Spinner className="text-muted-foreground size-4" />
@@ -424,19 +238,151 @@ export function CommandMenu({
<CommandEmpty className="text-muted-foreground py-12 text-center text-sm"> <CommandEmpty className="text-muted-foreground py-12 text-center text-sm">
{query.isLoading ? "Searching..." : "No results found."} {query.isLoading ? "Searching..." : "No results found."}
</CommandEmpty> </CommandEmpty>
{navItemsSection} {navItems && navItems.length > 0 && (
{renderDelayedGroups ? ( <CommandGroup
<> heading="Pages"
{pageGroupsSection} className="!p-0 [&_[cmdk-group-heading]]:scroll-mt-16 [&_[cmdk-group-heading]]:!p-3 [&_[cmdk-group-heading]]:!pb-1"
{colorGroupsSection} >
{blocksSection} {navItems.map((item) => (
<SearchResults <CommandMenuItem
setOpen={setOpen} key={item.href}
query={query} value={`Navigation ${item.label}`}
search={search} keywords={["nav", "navigation", item.label.toLowerCase()]}
/> onHighlight={() => {
</> setSelectedType("page")
setCopyPayload("")
}}
onSelect={() => {
runCommand(() => router.push(item.href))
}}
>
<IconArrowRight />
{item.label}
</CommandMenuItem>
))}
</CommandGroup>
)}
{tree.children.map((group) => (
<CommandGroup
key={group.$id}
heading={group.name}
className="!p-0 [&_[cmdk-group-heading]]:scroll-mt-16 [&_[cmdk-group-heading]]:!p-3 [&_[cmdk-group-heading]]:!pb-1"
>
{group.type === "folder" &&
group.children.map((item) => {
if (item.type === "page") {
const isComponent = item.url.includes("/components/")
if (!showMcpDocs && item.url.includes("/mcp")) {
return null
}
return (
<CommandMenuItem
key={item.url}
value={
item.name?.toString()
? `${group.name} ${item.name}`
: ""
}
keywords={isComponent ? ["component"] : undefined}
onHighlight={() =>
handlePageHighlight(isComponent, item)
}
onSelect={() => {
runCommand(() => router.push(item.url))
}}
>
{isComponent ? (
<div className="border-muted-foreground aspect-square size-4 rounded-full border border-dashed" />
) : (
<IconArrowRight />
)}
{item.name}
</CommandMenuItem>
)
}
return null
})}
</CommandGroup>
))}
{colors.map((colorPalette) => (
<CommandGroup
key={colorPalette.name}
heading={
colorPalette.name.charAt(0).toUpperCase() +
colorPalette.name.slice(1)
}
className="!p-0 [&_[cmdk-group-heading]]:!p-3"
>
{colorPalette.colors.map((color) => (
<CommandMenuItem
key={color.hex}
value={color.className}
keywords={["color", color.name, color.className]}
onHighlight={() => handleColorHighlight(color)}
onSelect={() => {
runCommand(() =>
copyToClipboardWithMeta(color.oklch, {
name: "copy_color",
properties: { color: color.oklch },
})
)
}}
>
<div
className="border-ghost aspect-square size-4 rounded-sm bg-(--color) after:rounded-sm"
style={{ "--color": color.oklch } as React.CSSProperties}
/>
{color.className}
<span className="text-muted-foreground ml-auto font-mono text-xs font-normal tabular-nums">
{color.oklch}
</span>
</CommandMenuItem>
))}
</CommandGroup>
))}
{blocks?.length ? (
<CommandGroup
heading="Blocks"
className="!p-0 [&_[cmdk-group-heading]]:!p-3"
>
{blocks.map((block) => (
<CommandMenuItem
key={block.name}
value={block.name}
onHighlight={() => {
handleBlockHighlight(block)
}}
keywords={[
"block",
block.name,
block.description,
...block.categories,
]}
onSelect={() => {
runCommand(() =>
router.push(
`/blocks/${block.categories[0]}#${block.name}`
)
)
}}
>
<SquareDashedIcon />
{block.description}
<span className="text-muted-foreground ml-auto font-mono text-xs font-normal tabular-nums">
{block.name}
</span>
</CommandMenuItem>
))}
</CommandGroup>
) : null} ) : null}
<SearchResults
open={open}
setOpen={setOpen}
query={query}
search={search}
/>
</CommandList> </CommandList>
</Command> </Command>
<div className="text-muted-foreground absolute inset-x-0 bottom-0 z-20 flex h-10 items-center gap-2 rounded-b-xl border-t border-t-neutral-100 bg-neutral-50 px-4 text-xs font-medium dark:border-t-neutral-700 dark:bg-neutral-800"> <div className="text-muted-foreground absolute inset-x-0 bottom-0 z-20 flex h-10 items-center gap-2 rounded-b-xl border-t border-t-neutral-100 bg-neutral-50 px-4 text-xs font-medium dark:border-t-neutral-700 dark:bg-neutral-800">
@@ -522,24 +468,23 @@ function SearchResults({
query, query,
search, search,
}: { }: {
open: boolean
setOpen: (open: boolean) => void setOpen: (open: boolean) => void
query: Query query: Query
search: string search: string
}) { }) {
const router = useRouter() const router = useRouter()
const uniqueResults = React.useMemo(() => { const uniqueResults =
if (!query.data || !Array.isArray(query.data)) { query.data && Array.isArray(query.data)
return [] ? query.data.filter(
} (item, index, self) =>
!(
return query.data.filter( item.type === "text" &&
(item, index, self) => item.content.trim().split(/\s+/).length <= 1
!( ) && index === self.findIndex((t) => t.content === item.content)
item.type === "text" && item.content.trim().split(/\s+/).length <= 1 )
) && index === self.findIndex((t) => t.content === item.content) : []
)
}, [query.data])
if (!search.trim()) { if (!search.trim()) {
return null return null
@@ -578,27 +523,3 @@ function SearchResults({
</CommandGroup> </CommandGroup>
) )
} }
function DialogContent({
className,
children,
...props
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
showCloseButton?: boolean
}) {
return (
<DialogPortal data-slot="dialog-portal">
{/* <DialogOverlay /> */}
<DialogPrimitive.Content
data-slot="dialog-content"
className={cn(
"bg-background fixed top-1/3 left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 outline-none sm:max-w-lg",
className
)}
{...props}
>
{children}
</DialogPrimitive.Content>
</DialogPortal>
)
}

View File

@@ -1,267 +1,51 @@
"use client" "use client"
import * as React from "react" import * as React from "react"
import Link from "next/link"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/examples/base/ui/popover"
import { IconAlertCircle } from "@tabler/icons-react"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import {
LanguageProvider,
LanguageSelector,
useLanguageContext,
useTranslation,
type Translations,
} from "@/components/language-selector"
import { DirectionProvider as BaseDirectionProvider } from "@/registry/bases/base/ui/direction"
import { DirectionProvider as RadixDirectionProvider } from "@/registry/bases/radix/ui/direction"
import { Button } from "@/registry/new-york-v4/ui/button"
import { Separator } from "@/registry/new-york-v4/ui/separator"
export function ComponentPreviewTabs({ export function ComponentPreviewTabs({
className, className,
previewClassName,
align = "center", align = "center",
hideCode = false, hideCode = false,
chromeLessOnMobile = false, chromeLessOnMobile = false,
component, component,
source, source,
sourcePreview,
direction = "ltr",
styleName,
...props ...props
}: React.ComponentProps<"div"> & { }: React.ComponentProps<"div"> & {
previewClassName?: string
align?: "center" | "start" | "end" align?: "center" | "start" | "end"
hideCode?: boolean hideCode?: boolean
chromeLessOnMobile?: boolean chromeLessOnMobile?: boolean
component: React.ReactNode component: React.ReactNode
source: React.ReactNode source: React.ReactNode
sourcePreview?: React.ReactNode
direction?: "ltr" | "rtl"
styleName?: string
}) { }) {
const [isMobileCodeVisible, setIsMobileCodeVisible] = React.useState(false)
const base = styleName?.split("-")[0]
return ( return (
<div <div
data-slot="component-preview"
className={cn( className={cn(
"group relative mt-4 mb-12 flex flex-col overflow-hidden rounded-xl border", "group relative mt-4 mb-12 flex flex-col gap-2 rounded-lg border",
className className
)} )}
{...props} {...props}
> >
{direction === "rtl" ? ( <div data-slot="preview">
<LanguageProvider defaultLanguage="ar">
<div className="flex h-16 items-center border-b px-4">
<RtlLanguageSelector />
<Popover>
<PopoverTrigger
render={
<Button
variant="ghost"
size="icon-sm"
className="ml-auto size-7"
>
<IconAlertCircle />
<span className="sr-only">Toggle</span>
</Button>
}
></PopoverTrigger>
<PopoverContent
side="bottom"
align="end"
className="w-56 text-xs"
>
<div>
I used AI to translate the text for demonstration purposes.
It&apos;s not perfect and may contain errors.
</div>
<Separator className="-mx-2.5 w-auto!" />
<div data-lang="ar">
لقد استخدمت الذكاء الاصطناعي لترجمة النص للأغراض التجريبية
فقط. قد لا تكون الترجمة دقيقة وقد تحتوي على أخطاء.
</div>
<Separator className="-mx-2.5 w-auto!" />
<div data-lang="he">
השתמשתי בבינה מלאכותית כדי לתרגם את הטקסט למטרות הדגמה. זה לא
מושלם ויכול להכיל שגיאות.
</div>
</PopoverContent>
</Popover>
</div>
<PreviewWrapper
align={align}
chromeLessOnMobile={chromeLessOnMobile}
previewClassName={previewClassName}
>
<DirectionProviderWrapper base={base}>
{component}
</DirectionProviderWrapper>
</PreviewWrapper>
</LanguageProvider>
) : (
<DirectionProviderWrapper base={base} dir="ltr">
<PreviewWrapper
align={align}
chromeLessOnMobile={chromeLessOnMobile}
previewClassName={previewClassName}
dir="ltr"
>
{component}
</PreviewWrapper>
</DirectionProviderWrapper>
)}
{!hideCode && (
<div <div
data-slot="code" data-align={align}
data-mobile-code-visible={isMobileCodeVisible} className={cn(
className="relative overflow-hidden **:data-[slot=copy-button]:right-4 **:data-[slot=copy-button]:hidden data-[mobile-code-visible=true]:**:data-[slot=copy-button]:flex [&_[data-rehype-pretty-code-figure]]:!m-0 [&_[data-rehype-pretty-code-figure]]:rounded-t-none [&_[data-rehype-pretty-code-figure]]:border-t [&_pre]:max-h-72" "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"
{isMobileCodeVisible ? (
<>
{direction === "rtl" && (
<div className="bg-code text-muted-foreground no-scrollbar relative z-10 overflow-x-auto border-t p-6 font-mono text-sm">
<pre>{`// You will notice this example uses dir and data-lang attributes.
// This is because this site is not RTL by default.
// In your application, you won't need these.`}</pre>
<span>
{"// See the "}
<Link
href="/docs/rtl"
className="underline underline-offset-4"
>
RTL guide
</Link>
{" for more information."}
</span>
</div>
)}
{source}
</>
) : (
<div className="relative">
{sourcePreview}
<div className="absolute inset-0 flex items-center justify-center pb-4">
<div
className="absolute inset-0"
style={{
background:
"linear-gradient(to top, var(--color-code), color-mix(in oklab, var(--color-code) 60%, transparent), transparent)",
}}
/>
<Button
type="button"
size="sm"
variant="outline"
className="bg-background text-foreground dark:bg-background dark:text-foreground hover:bg-muted dark:hover:bg-muted relative z-10 rounded-lg shadow-none"
onClick={() => {
setIsMobileCodeVisible(true)
}}
>
View Code
</Button>
</div>
</div>
)} )}
>
{component}
</div> </div>
)} {!hideCode && (
</div> <div
) data-slot="code"
} className="overflow-hidden [&_[data-rehype-pretty-code-figure]]:!m-0 [&_[data-rehype-pretty-code-figure]]:rounded-t-none [&_[data-rehype-pretty-code-figure]]:border-t [&_pre]:max-h-[400px]"
>
const directionTranslations: Translations<Record<string, never>> = { {source}
en: { </div>
dir: "ltr",
values: {},
},
ar: {
dir: "rtl",
values: {},
},
he: {
dir: "rtl",
values: {},
},
}
function RtlLanguageSelector({ className }: { className?: string }) {
const context = useLanguageContext()
if (!context) {
return null
}
return (
<LanguageSelector
value={context.language}
onValueChange={context.setLanguage}
className={className}
/>
)
}
function PreviewWrapper({
align,
chromeLessOnMobile,
previewClassName,
dir: explicitDir,
children,
}: {
align: "center" | "start" | "end"
chromeLessOnMobile: boolean
previewClassName?: string
dir?: "ltr" | "rtl"
children: React.ReactNode
}) {
// useTranslation handles the case when there's no LanguageProvider context.
// It will fall back to local state with defaultLanguage.
const translation = useTranslation(directionTranslations, "ar")
const dir = explicitDir ?? translation.dir
return (
<div
data-slot="preview"
dir={dir}
data-lang={dir === "rtl" ? translation.language : undefined}
>
<div
data-align={align}
data-chromeless={chromeLessOnMobile}
className={cn(
"preview relative flex h-72 w-full justify-center p-10 data-[align=center]:items-center data-[align=end]:items-end data-[align=start]:items-start data-[chromeless=true]:h-auto data-[chromeless=true]:p-0",
previewClassName
)} )}
>
{children}
</div> </div>
</div> </div>
) )
} }
function DirectionProviderWrapper({
base,
dir: explicitDir,
children,
}: {
base?: string
dir?: "ltr" | "rtl"
children: React.ReactNode
}) {
// useTranslation handles the case when there's no LanguageProvider context.
// It will fall back to local state with defaultLanguage.
const translation = useTranslation(directionTranslations, "ar")
const dir = explicitDir ?? translation.dir
if (base === "base") {
return (
<BaseDirectionProvider direction={dir}>{children}</BaseDirectionProvider>
)
}
return <RadixDirectionProvider dir={dir}>{children}</RadixDirectionProvider>
}

View File

@@ -1,37 +1,45 @@
import * as React from "react"
import Image from "next/image" import Image from "next/image"
import { getRegistryComponent } from "@/lib/registry"
import { ComponentPreviewTabs } from "@/components/component-preview-tabs" import { ComponentPreviewTabs } from "@/components/component-preview-tabs"
import { ComponentSource } from "@/components/component-source" import { ComponentSource } from "@/components/component-source"
import { Index } from "@/registry/__index__"
import { type Style } from "@/registry/_legacy-styles"
export function ComponentPreview({ export function ComponentPreview({
name, name,
styleName = "new-york-v4",
type, type,
className, className,
previewClassName,
align = "center", align = "center",
hideCode = false, hideCode = false,
chromeLessOnMobile = false, chromeLessOnMobile = false,
styleName = "new-york-v4",
direction = "ltr",
caption,
...props ...props
}: React.ComponentProps<"div"> & { }: React.ComponentProps<"div"> & {
name: string name: string
styleName?: string styleName?: Style["name"]
align?: "center" | "start" | "end" align?: "center" | "start" | "end"
description?: string description?: string
hideCode?: boolean hideCode?: boolean
type?: "block" | "component" | "example" type?: "block" | "component" | "example"
chromeLessOnMobile?: boolean chromeLessOnMobile?: boolean
previewClassName?: string
direction?: "ltr" | "rtl"
caption?: string
}) { }) {
const Component = Index[styleName]?.[name]?.component
if (!Component) {
return (
<p className="text-muted-foreground mt-6 text-sm">
Component{" "}
<code className="bg-muted relative rounded px-[0.3rem] py-[0.2rem] font-mono text-sm">
{name}
</code>{" "}
not found in registry.
</p>
)
}
if (type === "block") { if (type === "block") {
const content = ( return (
<div className="relative mt-6 aspect-[4/2.5] w-full overflow-hidden rounded-xl border md:-mx-1"> <div className="relative aspect-[4/2.5] w-full overflow-hidden rounded-md border md:-mx-1">
<Image <Image
src={`/r/styles/new-york-v4/${name}-light.png`} src={`/r/styles/new-york-v4/${name}-light.png`}
alt={name} alt={name}
@@ -51,42 +59,14 @@ export function ComponentPreview({
</div> </div>
</div> </div>
) )
if (caption) {
return (
<figure className="flex flex-col gap-4">
{content}
<figcaption className="text-muted-foreground text-center text-sm">
{caption}
</figcaption>
</figure>
)
}
return content
} }
const Component = getRegistryComponent(name, styleName) return (
if (!Component) {
return (
<p className="text-muted-foreground mt-6 text-sm">
Component{" "}
<code className="bg-muted relative rounded px-[0.3rem] py-[0.2rem] font-mono text-sm">
{name}
</code>{" "}
not found in registry.
</p>
)
}
const content = (
<ComponentPreviewTabs <ComponentPreviewTabs
className={className} className={className}
previewClassName={previewClassName}
align={align} align={align}
hideCode={hideCode} hideCode={hideCode}
component={React.createElement(Component)} component={<Component />}
source={ source={
<ComponentSource <ComponentSource
name={name} name={name}
@@ -94,34 +74,8 @@ export function ComponentPreview({
styleName={styleName} styleName={styleName}
/> />
} }
sourcePreview={
<ComponentSource
name={name}
collapsible={false}
styleName={styleName}
maxLines={3}
/>
}
chromeLessOnMobile={chromeLessOnMobile} chromeLessOnMobile={chromeLessOnMobile}
direction={direction}
styleName={styleName}
{...props} {...props}
/> />
) )
if (caption) {
return (
<figure
data-hide-code={hideCode}
className="flex flex-col data-[hide-code=true]:gap-4"
>
{content}
<figcaption className="text-muted-foreground -mt-8 text-center text-sm data-[hide-code=true]:mt-0">
{caption}
</figcaption>
</figure>
)
}
return content
} }

View File

@@ -3,12 +3,12 @@ import path from "node:path"
import * as React from "react" import * as React from "react"
import { highlightCode } from "@/lib/highlight-code" import { highlightCode } from "@/lib/highlight-code"
import { getDemoItem, getRegistryItem } from "@/lib/registry" import { getRegistryItem } from "@/lib/registry"
import { formatCode } from "@/lib/rehype"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { CodeCollapsibleWrapper } from "@/components/code-collapsible-wrapper" import { CodeCollapsibleWrapper } from "@/components/code-collapsible-wrapper"
import { CopyButton } from "@/components/copy-button" import { CopyButton } from "@/components/copy-button"
import { getIconForLanguageExtension } from "@/components/icons" import { getIconForLanguageExtension } from "@/components/icons"
import { type Style } from "@/registry/_legacy-styles"
export async function ComponentSource({ export async function ComponentSource({
name, name,
@@ -18,15 +18,13 @@ export async function ComponentSource({
collapsible = true, collapsible = true,
className, className,
styleName = "new-york-v4", styleName = "new-york-v4",
maxLines,
}: React.ComponentProps<"div"> & { }: React.ComponentProps<"div"> & {
name?: string name?: string
src?: string src?: string
title?: string title?: string
language?: string language?: string
collapsible?: boolean collapsible?: boolean
styleName?: string styleName?: Style["name"]
maxLines?: number
}) { }) {
if (!name && !src) { if (!name && !src) {
return null return null
@@ -35,9 +33,7 @@ export async function ComponentSource({
let code: string | undefined let code: string | undefined
if (name) { if (name) {
const item = const item = await getRegistryItem(name, styleName)
(await getDemoItem(name, styleName)) ??
(await getRegistryItem(name, styleName))
code = item?.files?.[0]?.content code = item?.files?.[0]?.content
} }
@@ -50,13 +46,13 @@ export async function ComponentSource({
return null return null
} }
code = await formatCode(code, styleName) // Fix imports.
code = code.replaceAll("/* eslint-disable react/no-children-prop */\n", "") // Replace @/registry/${style}/ with @/components/.
code = code.replaceAll(`@/registry/${styleName}/`, "@/components/")
// Truncate code if maxLines is set. // Replace export default with export.
if (maxLines) { code = code.replaceAll("export default", "export")
code = code.split("\n").slice(0, maxLines).join("\n") code = code.replaceAll("/* eslint-disable react/no-children-prop */\n", "")
}
const lang = language ?? title?.split(".").pop() ?? "tsx" const lang = language ?? title?.split(".").pop() ?? "tsx"
const highlightedCode = await highlightCode(code, lang) const highlightedCode = await highlightCode(code, lang)

View File

@@ -1,19 +1,23 @@
import Link from "next/link" import Link from "next/link"
import { PAGES_NEW } from "@/lib/docs" import { PAGES_NEW } from "@/lib/docs"
import { getPagesFromFolder, type PageTreeFolder } from "@/lib/page-tree" import { source } from "@/lib/source"
export function ComponentsList({ export function ComponentsList() {
componentsFolder, const components = source.pageTree.children.find(
currentBase, (page) => page.$id === "components"
}: { )
componentsFolder: PageTreeFolder
currentBase: string if (components?.type !== "folder") {
}) { return
const list = getPagesFromFolder(componentsFolder, currentBase) }
const list = components.children.filter(
(component) => component.type === "page"
)
return ( return (
<div className="grid grid-cols-2 gap-4 md:grid-cols-3 md:gap-x-8 lg:gap-x-16 lg:gap-y-6 xl:gap-x-20"> <div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 md:gap-x-8 lg:gap-x-16 lg:gap-y-6 xl:gap-x-20">
{list.map((component) => ( {list.map((component) => (
<Link <Link
key={component.$id} key={component.$id}

View File

@@ -1,11 +1,13 @@
"use client" "use client"
import * as React from "react" import * as React from "react"
import { IconCheck, IconCopy, IconPlus } from "@tabler/icons-react" import Link from "next/link"
import { IconCheck } from "@tabler/icons-react"
import { useConfig } from "@/hooks/use-config" import { cn } from "@/lib/utils"
import { useCopyToClipboard } from "@/hooks/use-copy-to-clipboard"
import { useIsMobile } from "@/hooks/use-mobile" import { useIsMobile } from "@/hooks/use-mobile"
import { copyToClipboardWithMeta } from "@/components/copy-button" import { CopyButton } from "@/components/copy-button"
import { Button } from "@/registry/new-york-v4/ui/button" import { Button } from "@/registry/new-york-v4/ui/button"
import { import {
Dialog, Dialog,
@@ -27,114 +29,77 @@ import {
DrawerTitle, DrawerTitle,
DrawerTrigger, DrawerTrigger,
} from "@/registry/new-york-v4/ui/drawer" } from "@/registry/new-york-v4/ui/drawer"
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "@/registry/new-york-v4/ui/tabs"
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/registry/new-york-v4/ui/tooltip"
export function DirectoryAddButton({ export function DirectoryAddButton({
registry, registry,
}: { }: {
registry: { name: string } registry: {
name: string
url: string
}
}) { }) {
const [config, setConfig] = useConfig() const { copyToClipboard, isCopied } = useCopyToClipboard()
const [hasCopied, setHasCopied] = React.useState(false)
const [open, setOpen] = React.useState(false)
const isMobile = useIsMobile() const isMobile = useIsMobile()
const [open, setOpen] = React.useState(false)
const packageManager = config.packageManager || "pnpm" const jsonValue = `{
"registries": {
const commands = React.useMemo(() => { "${registry.name}": "${registry.url}"
return { }
pnpm: `pnpm dlx shadcn@latest registry add ${registry.name}`, }`
npm: `npx shadcn@latest registry add ${registry.name}`,
yarn: `yarn dlx shadcn@latest registry add ${registry.name}`,
bun: `bunx --bun shadcn@latest registry add ${registry.name}`,
}
}, [registry.name])
const command = commands[packageManager]
React.useEffect(() => {
if (hasCopied) {
const timer = setTimeout(() => setHasCopied(false), 2000)
return () => clearTimeout(timer)
}
}, [hasCopied])
const handleCopy = React.useCallback(() => {
copyToClipboardWithMeta(command, {
name: "copy_registry_add_command",
properties: {
command,
registry: registry.name,
},
})
setHasCopied(true)
}, [command, registry.name])
const Trigger = ( const Trigger = (
<Button size="sm" variant="outline" className="relative z-10"> <Button
Add <IconPlus /> size="sm"
variant="outline"
className="relative z-10"
onClick={() => setOpen(true)}
>
{isCopied ? (
<IconCheck />
) : (
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<title>Model Context Protocol</title>
<path
d="M13.85 0a4.16 4.16 0 0 0-2.95 1.217L1.456 10.66a.835.835 0 0 0 0 1.18.835.835 0 0 0 1.18 0l9.442-9.442a2.49 2.49 0 0 1 3.541 0 2.49 2.49 0 0 1 0 3.541L8.59 12.97l-.1.1a.835.835 0 0 0 0 1.18.835.835 0 0 0 1.18 0l.1-.098 7.03-7.034a2.49 2.49 0 0 1 3.542 0l.049.05a2.49 2.49 0 0 1 0 3.54l-8.54 8.54a1.96 1.96 0 0 0 0 2.755l1.753 1.753a.835.835 0 0 0 1.18 0 .835.835 0 0 0 0-1.18l-1.753-1.753a.266.266 0 0 1 0-.394l8.54-8.54a4.185 4.185 0 0 0 0-5.9l-.05-.05a4.16 4.16 0 0 0-2.95-1.218c-.2 0-.401.02-.6.048a4.17 4.17 0 0 0-1.17-3.552A4.16 4.16 0 0 0 13.85 0m0 3.333a.84.84 0 0 0-.59.245L6.275 10.56a4.186 4.186 0 0 0 0 5.902 4.186 4.186 0 0 0 5.902 0L19.16 9.48a.835.835 0 0 0 0-1.18.835.835 0 0 0-1.18 0l-6.985 6.984a2.49 2.49 0 0 1-3.54 0 2.49 2.49 0 0 1 0-3.54l6.983-6.985a.835.835 0 0 0 0-1.18.84.84 0 0 0-.59-.245"
className="fill-foreground"
/>
</svg>
)}
MCP
</Button> </Button>
) )
const Content = ( const Content = (
<Tabs <>
value={packageManager} <figure
onValueChange={(value) => { data-rehype-pretty-code-figure
setConfig({ className={cn(
...config, "group relative mt-0",
packageManager: value as "pnpm" | "npm" | "yarn" | "bun", !isMobile &&
}) "dark:bg-background dark:[&_[data-line]:not([data-highlighted-line]):before]:bg-background!"
}} )}
className="gap-0 overflow-hidden rounded-lg border" >
> <CopyButton
<div className="flex items-center gap-2 border-b p-2"> value={jsonValue}
<TabsList className="*:data-[slot=tabs-trigger]:data-[state=active]:border-input h-auto rounded-none bg-transparent p-0 font-mono *:data-[slot=tabs-trigger]:border *:data-[slot=tabs-trigger]:border-transparent *:data-[slot=tabs-trigger]:pt-0.5 *:data-[slot=tabs-trigger]:shadow-none!"> className="top-3 right-2"
<TabsTrigger value="pnpm">pnpm</TabsTrigger> tooltip="Copy Code"
<TabsTrigger value="npm">npm</TabsTrigger> />
<TabsTrigger value="yarn">yarn</TabsTrigger> <div data-rehype-pretty-code-title>components.json</div>
<TabsTrigger value="bun">bun</TabsTrigger> <pre className="no-scrollbar min-w-0 overflow-x-auto px-4 py-3.5 outline-none has-[[data-highlighted-line]]:px-0 has-[[data-line-numbers]]:px-0 has-[[data-slot=tabs]]:p-0">
</TabsList> <code data-line-numbers data-language="json">
<Tooltip> <span data-line>{"{"}</span>
<TooltipTrigger asChild> <span data-line>{' "registries": {'}</span>
<Button <span
size="icon-sm" data-line
variant="ghost" data-highlighted-line
className="ml-auto size-7 rounded-lg" >{` "${registry.name}": "${registry.url}"`}</span>
onClick={handleCopy} <span data-line>{" }"}</span>
> <span data-line>{"}"}</span>
{hasCopied ? ( </code>
<IconCheck className="size-4" /> </pre>
) : ( </figure>
<IconCopy className="size-4" /> </>
)}
<span className="sr-only">Copy command</span>
</Button>
</TooltipTrigger>
<TooltipContent>
{hasCopied ? "Copied!" : "Copy command"}
</TooltipContent>
</Tooltip>
</div>
{Object.entries(commands).map(([key, cmd]) => (
<TabsContent key={key} value={key} className="mt-0">
<div className="bg-surface text-surface-foreground px-3 py-3">
<div className="no-scrollbar overflow-x-auto">
<code className="font-mono text-sm whitespace-nowrap">{cmd}</code>
</div>
</div>
</TabsContent>
))}
</Tabs>
) )
if (isMobile) { if (isMobile) {
@@ -143,16 +108,20 @@ export function DirectoryAddButton({
<DrawerTrigger asChild>{Trigger}</DrawerTrigger> <DrawerTrigger asChild>{Trigger}</DrawerTrigger>
<DrawerContent> <DrawerContent>
<DrawerHeader> <DrawerHeader>
<DrawerTitle>Add Registry</DrawerTitle> <DrawerTitle>Configure MCP</DrawerTitle>
<DrawerDescription> <DrawerDescription>
Run this command to add {registry.name} to your project. Copy and paste the following code into your project&apos;s
components.json.
</DrawerDescription> </DrawerDescription>
</DrawerHeader> </DrawerHeader>
<div className="px-4">{Content}</div> <div className="px-6">{Content}</div>
<DrawerFooter> <DrawerFooter>
<DrawerClose asChild> <DrawerClose asChild>
<Button size="sm">Done</Button> <Button size="sm">Close</Button>
</DrawerClose> </DrawerClose>
<Button size="sm" asChild variant="outline">
<Link href="/docs/mcp">Read the docs</Link>
</Button>
</DrawerFooter> </DrawerFooter>
</DrawerContent> </DrawerContent>
</Drawer> </Drawer>
@@ -162,15 +131,22 @@ export function DirectoryAddButton({
return ( return (
<Dialog open={open} onOpenChange={setOpen}> <Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>{Trigger}</DialogTrigger> <DialogTrigger asChild>{Trigger}</DialogTrigger>
<DialogContent className="dialog-ring animate-none! rounded-xl sm:max-w-md"> <DialogContent
className="rounded-xl border-none bg-clip-padding shadow-2xl ring-4 ring-neutral-200/80 sm:max-w-[600px] dark:bg-neutral-900 dark:ring-neutral-800"
onOpenAutoFocus={(e) => e.preventDefault()}
>
<DialogHeader> <DialogHeader>
<DialogTitle>Add Registry</DialogTitle> <DialogTitle>Configure MCP</DialogTitle>
<DialogDescription> <DialogDescription>
Run this command to add {registry.name} to your project. Copy and paste the following code into your project&apos;s
components.json.
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
{Content} {Content}
<DialogFooter> <DialogFooter className="justify-between!">
<Button size="sm" asChild variant="ghost">
<Link href="/docs/mcp">Read the docs</Link>
</Button>
<DialogClose asChild> <DialogClose asChild>
<Button size="sm">Done</Button> <Button size="sm">Done</Button>
</DialogClose> </DialogClose>

View File

@@ -50,10 +50,8 @@ export function DirectoryList() {
href={getHomepageUrl(registry.homepage)} href={getHomepageUrl(registry.homepage)}
target="_blank" target="_blank"
rel="noopener noreferrer external" rel="noopener noreferrer external"
className="group flex items-center gap-1"
> >
{registry.name}{" "} {registry.name}
<IconArrowUpRight className="size-4 opacity-0 group-hover:opacity-100" />
</a> </a>
</ItemTitle> </ItemTitle>
{registry.description && ( {registry.description && (
@@ -63,6 +61,15 @@ export function DirectoryList() {
)} )}
</ItemContent> </ItemContent>
<ItemActions className="relative z-10 hidden self-start sm:flex"> <ItemActions className="relative z-10 hidden self-start sm:flex">
<Button size="sm" variant="outline" asChild>
<a
href={getHomepageUrl(registry.homepage)}
target="_blank"
rel="noopener noreferrer external"
>
View <IconArrowUpRight />
</a>
</Button>
<DirectoryAddButton registry={registry} /> <DirectoryAddButton registry={registry} />
</ItemActions> </ItemActions>
<ItemFooter className="justify-start pl-16 sm:hidden"> <ItemFooter className="justify-start pl-16 sm:hidden">

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