mirror of
https://github.com/shadcn-ui/ui.git
synced 2026-06-12 18:31:46 +00:00
Compare commits
1 Commits
feature/im
...
shadcn/deb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f1131db844 |
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"shadcn": patch
|
||||
---
|
||||
|
||||
Fix: skip all transforms for universal registry items
|
||||
63
.github/ISSUE_TEMPLATE/registry_directory.yml
vendored
Normal file
63
.github/ISSUE_TEMPLATE/registry_directory.yml
vendored
Normal file
@@ -0,0 +1,63 @@
|
||||
name: Add registry to directory
|
||||
description: Add your registry to the directory
|
||||
title: "[Registry Directory]: "
|
||||
labels: ["registry", "directory"]
|
||||
assignees: []
|
||||
body:
|
||||
- type: input
|
||||
id: name
|
||||
attributes:
|
||||
label: Name
|
||||
description: The name of your registry. This is also the namespace.
|
||||
placeholder: e.g., "@acme"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: url
|
||||
attributes:
|
||||
label: URL
|
||||
description: The URL to your registry index. Use {name} placeholder.
|
||||
placeholder: https://ui.acme.com/r/{name}.json
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: homepage
|
||||
attributes:
|
||||
label: Homepage
|
||||
description: The URL to your registry homepage. This is where users can browse your registry.
|
||||
placeholder: https://ui.acme.com
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Description
|
||||
description: Briefly describe what is your registry and what type of components or code it distributes.
|
||||
placeholder:
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: logo
|
||||
attributes:
|
||||
label: Logo
|
||||
description: Add your SVG logo here.
|
||||
placeholder:
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
id: requirements
|
||||
attributes:
|
||||
label: Checklist
|
||||
description: Verify that your registry meets the following requirements.
|
||||
options:
|
||||
- label: The registry must be open source and publicly accessible.
|
||||
- label: The registry must be a valid JSON file that conforms to the [registry schema](https://ui.shadcn.com/docs/registry/registry-json) specification.
|
||||
- label: The `files` array, if present on your registry items, must NOT include a `content` property.
|
||||
- label: I've attached a square SVG logo to this issue
|
||||
validations:
|
||||
required: true
|
||||
13
.github/workflows/release.yml
vendored
13
.github/workflows/release.yml
vendored
@@ -7,11 +7,6 @@ on:
|
||||
branches:
|
||||
- main
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
release:
|
||||
if: ${{ github.repository_owner == 'shadcn-ui' }}
|
||||
@@ -29,15 +24,12 @@ jobs:
|
||||
version: 9.0.6
|
||||
|
||||
- name: Use Node.js 20
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
version: 9.0.6
|
||||
node-version: 20
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Update npm for OIDC support
|
||||
run: npm install -g npm@latest
|
||||
|
||||
- name: Install NPM Dependencies
|
||||
run: pnpm install
|
||||
|
||||
@@ -57,4 +49,5 @@ jobs:
|
||||
publish: npx changeset publish
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_ACCESS_TOKEN }}
|
||||
NODE_ENV: "production"
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Button } from "@/examples/radix/ui/button"
|
||||
import { ButtonGroup } from "@/examples/radix/ui/button-group"
|
||||
import { IconMinus, IconPlus } from "@tabler/icons-react"
|
||||
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import { ButtonGroup } from "@/registry/new-york-v4/ui/button-group"
|
||||
import {
|
||||
Field,
|
||||
FieldContent,
|
||||
@@ -13,11 +15,13 @@ import {
|
||||
FieldSeparator,
|
||||
FieldSet,
|
||||
FieldTitle,
|
||||
} from "@/examples/radix/ui/field"
|
||||
import { Input } from "@/examples/radix/ui/input"
|
||||
import { RadioGroup, RadioGroupItem } from "@/examples/radix/ui/radio-group"
|
||||
import { Switch } from "@/examples/radix/ui/switch"
|
||||
import { IconMinus, IconPlus } from "@tabler/icons-react"
|
||||
} from "@/registry/new-york-v4/ui/field"
|
||||
import { Input } from "@/registry/new-york-v4/ui/input"
|
||||
import {
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
} from "@/registry/new-york-v4/ui/radio-group"
|
||||
import { Switch } from "@/registry/new-york-v4/ui/switch"
|
||||
|
||||
export function AppearanceSettings() {
|
||||
const [gpuCount, setGpuCount] = React.useState(8)
|
||||
@@ -93,7 +97,7 @@ export function AppearanceSettings() {
|
||||
value={gpuCount}
|
||||
onChange={handleGpuInputChange}
|
||||
size={3}
|
||||
className="h-7 !w-14 font-mono"
|
||||
className="h-8 !w-14 font-mono"
|
||||
maxLength={3}
|
||||
/>
|
||||
<Button
|
||||
|
||||
@@ -1,8 +1,20 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Button } from "@/examples/radix/ui/button"
|
||||
import { ButtonGroup } from "@/examples/radix/ui/button-group"
|
||||
import {
|
||||
ArchiveIcon,
|
||||
ArrowLeftIcon,
|
||||
CalendarPlusIcon,
|
||||
ClockIcon,
|
||||
ListFilterIcon,
|
||||
MailCheckIcon,
|
||||
MoreHorizontalIcon,
|
||||
TagIcon,
|
||||
Trash2Icon,
|
||||
} from "lucide-react"
|
||||
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import { ButtonGroup } from "@/registry/new-york-v4/ui/button-group"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -15,18 +27,7 @@ import {
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/examples/radix/ui/dropdown-menu"
|
||||
import {
|
||||
ArchiveIcon,
|
||||
ArrowLeftIcon,
|
||||
CalendarPlusIcon,
|
||||
ClockIcon,
|
||||
ListFilterIcon,
|
||||
MailCheckIcon,
|
||||
MoreHorizontalIcon,
|
||||
TagIcon,
|
||||
Trash2Icon,
|
||||
} from "lucide-react"
|
||||
} from "@/registry/new-york-v4/ui/dropdown-menu"
|
||||
|
||||
export function ButtonGroupDemo() {
|
||||
const [label, setLabel] = React.useState("personal")
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Button } from "@/examples/radix/ui/button"
|
||||
import { ButtonGroup } from "@/examples/radix/ui/button-group"
|
||||
import { AudioLinesIcon, PlusIcon } from "lucide-react"
|
||||
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import { ButtonGroup } from "@/registry/new-york-v4/ui/button-group"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupInput,
|
||||
} from "@/examples/radix/ui/input-group"
|
||||
} from "@/registry/new-york-v4/ui/input-group"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/examples/radix/ui/tooltip"
|
||||
import { AudioLinesIcon, PlusIcon } from "lucide-react"
|
||||
} from "@/registry/new-york-v4/ui/tooltip"
|
||||
|
||||
export function ButtonGroupInputGroup() {
|
||||
const [voiceEnabled, setVoiceEnabled] = React.useState(false)
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
"use client"
|
||||
|
||||
import { Button } from "@/examples/radix/ui/button"
|
||||
import { ButtonGroup } from "@/examples/radix/ui/button-group"
|
||||
import { ArrowLeftIcon, ArrowRightIcon } from "lucide-react"
|
||||
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import { ButtonGroup } from "@/registry/new-york-v4/ui/button-group"
|
||||
|
||||
export function ButtonGroupNested() {
|
||||
return (
|
||||
<ButtonGroup>
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { Button } from "@/examples/radix/ui/button"
|
||||
import { ButtonGroup } from "@/examples/radix/ui/button-group"
|
||||
import { BotIcon, ChevronDownIcon } from "lucide-react"
|
||||
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import { ButtonGroup } from "@/registry/new-york-v4/ui/button-group"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/examples/radix/ui/popover"
|
||||
import { Separator } from "@/examples/radix/ui/separator"
|
||||
import { Textarea } from "@/examples/radix/ui/textarea"
|
||||
import { BotIcon, ChevronDownIcon } from "lucide-react"
|
||||
} from "@/registry/new-york-v4/ui/popover"
|
||||
import { Separator } from "@/registry/new-york-v4/ui/separator"
|
||||
import { Textarea } from "@/registry/new-york-v4/ui/textarea"
|
||||
|
||||
export function ButtonGroupPopover() {
|
||||
return (
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { PlusIcon } from "lucide-react"
|
||||
|
||||
import {
|
||||
Avatar,
|
||||
AvatarFallback,
|
||||
AvatarGroup,
|
||||
AvatarImage,
|
||||
} from "@/examples/radix/ui/avatar"
|
||||
import { Button } from "@/examples/radix/ui/button"
|
||||
} from "@/registry/new-york-v4/ui/avatar"
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import {
|
||||
Empty,
|
||||
EmptyContent,
|
||||
@@ -12,15 +13,14 @@ import {
|
||||
EmptyHeader,
|
||||
EmptyMedia,
|
||||
EmptyTitle,
|
||||
} from "@/examples/radix/ui/empty"
|
||||
import { PlusIcon } from "lucide-react"
|
||||
} from "@/registry/new-york-v4/ui/empty"
|
||||
|
||||
export function EmptyAvatarGroup() {
|
||||
return (
|
||||
<Empty className="flex-none border py-10">
|
||||
<Empty className="flex-none border">
|
||||
<EmptyHeader>
|
||||
<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>
|
||||
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
|
||||
<AvatarFallback>CN</AvatarFallback>
|
||||
@@ -39,7 +39,7 @@ export function EmptyAvatarGroup() {
|
||||
/>
|
||||
<AvatarFallback>ER</AvatarFallback>
|
||||
</Avatar>
|
||||
</AvatarGroup>
|
||||
</div>
|
||||
</EmptyMedia>
|
||||
<EmptyTitle>No Team Members</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Checkbox } from "@/examples/radix/ui/checkbox"
|
||||
import { Field, FieldLabel } from "@/examples/radix/ui/field"
|
||||
import { Checkbox } from "@/registry/new-york-v4/ui/checkbox"
|
||||
import { Field, FieldLabel } from "@/registry/new-york-v4/ui/field"
|
||||
|
||||
export function FieldCheckbox() {
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Button } from "@/examples/radix/ui/button"
|
||||
import { Checkbox } from "@/examples/radix/ui/checkbox"
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import { Checkbox } from "@/registry/new-york-v4/ui/checkbox"
|
||||
import {
|
||||
Field,
|
||||
FieldDescription,
|
||||
@@ -8,16 +8,16 @@ import {
|
||||
FieldLegend,
|
||||
FieldSeparator,
|
||||
FieldSet,
|
||||
} from "@/examples/radix/ui/field"
|
||||
import { Input } from "@/examples/radix/ui/input"
|
||||
} from "@/registry/new-york-v4/ui/field"
|
||||
import { Input } from "@/registry/new-york-v4/ui/input"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/examples/radix/ui/select"
|
||||
import { Textarea } from "@/examples/radix/ui/textarea"
|
||||
} from "@/registry/new-york-v4/ui/select"
|
||||
import { Textarea } from "@/registry/new-york-v4/ui/textarea"
|
||||
|
||||
export function FieldDemo() {
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Card, CardContent } from "@/examples/radix/ui/card"
|
||||
import { Checkbox } from "@/examples/radix/ui/checkbox"
|
||||
import { Card, CardContent } from "@/registry/new-york-v4/ui/card"
|
||||
import { Checkbox } from "@/registry/new-york-v4/ui/checkbox"
|
||||
import {
|
||||
Field,
|
||||
FieldDescription,
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
FieldLegend,
|
||||
FieldSet,
|
||||
FieldTitle,
|
||||
} from "@/examples/radix/ui/field"
|
||||
} from "@/registry/new-york-v4/ui/field"
|
||||
|
||||
const options = [
|
||||
{
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
"use client"
|
||||
|
||||
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() {
|
||||
const [value, setValue] = useState([200, 800])
|
||||
|
||||
@@ -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 { ButtonGroupDemo } from "./button-group-demo"
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { IconInfoCircle, IconStar } from "@tabler/icons-react"
|
||||
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupInput,
|
||||
} from "@/examples/radix/ui/input-group"
|
||||
import { Label } from "@/examples/radix/ui/label"
|
||||
} from "@/registry/new-york-v4/ui/input-group"
|
||||
import { Label } from "@/registry/new-york-v4/ui/label"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/examples/radix/ui/popover"
|
||||
import { IconInfoCircle, IconStar } from "@tabler/icons-react"
|
||||
} from "@/registry/new-york-v4/ui/popover"
|
||||
|
||||
export function InputGroupButtonExample() {
|
||||
const [isFavorite, setIsFavorite] = React.useState(false)
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { IconCheck, IconInfoCircle, IconPlus } from "@tabler/icons-react"
|
||||
import { ArrowUpIcon, Search } from "lucide-react"
|
||||
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/examples/radix/ui/dropdown-menu"
|
||||
} from "@/registry/new-york-v4/ui/dropdown-menu"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
@@ -11,15 +14,13 @@ import {
|
||||
InputGroupInput,
|
||||
InputGroupText,
|
||||
InputGroupTextarea,
|
||||
} from "@/examples/radix/ui/input-group"
|
||||
import { Separator } from "@/examples/radix/ui/separator"
|
||||
} from "@/registry/new-york-v4/ui/input-group"
|
||||
import { Separator } from "@/registry/new-york-v4/ui/separator"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/examples/radix/ui/tooltip"
|
||||
import { IconCheck, IconInfoCircle, IconPlus } from "@tabler/icons-react"
|
||||
import { ArrowUpIcon, Search } from "lucide-react"
|
||||
} from "@/registry/new-york-v4/ui/tooltip"
|
||||
|
||||
export function InputGroupDemo() {
|
||||
return (
|
||||
|
||||
@@ -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 {
|
||||
Item,
|
||||
ItemActions,
|
||||
@@ -6,8 +8,7 @@ import {
|
||||
ItemDescription,
|
||||
ItemMedia,
|
||||
ItemTitle,
|
||||
} from "@/examples/radix/ui/item"
|
||||
import { BadgeCheckIcon, ChevronRightIcon } from "lucide-react"
|
||||
} from "@/registry/new-york-v4/ui/item"
|
||||
|
||||
export function ItemDemo() {
|
||||
return (
|
||||
|
||||
@@ -1,8 +1,24 @@
|
||||
"use client"
|
||||
|
||||
import { useMemo, useState } from "react"
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/examples/radix/ui/avatar"
|
||||
import { Badge } from "@/examples/radix/ui/badge"
|
||||
import {
|
||||
IconApps,
|
||||
IconArrowUp,
|
||||
IconAt,
|
||||
IconBook,
|
||||
IconCircleDashedPlus,
|
||||
IconPaperclip,
|
||||
IconPlus,
|
||||
IconWorld,
|
||||
IconX,
|
||||
} from "@tabler/icons-react"
|
||||
|
||||
import {
|
||||
Avatar,
|
||||
AvatarFallback,
|
||||
AvatarImage,
|
||||
} from "@/registry/new-york-v4/ui/avatar"
|
||||
import { Badge } from "@/registry/new-york-v4/ui/badge"
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
@@ -10,7 +26,7 @@ import {
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@/examples/radix/ui/command"
|
||||
} from "@/registry/new-york-v4/ui/command"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuCheckboxItem,
|
||||
@@ -23,36 +39,25 @@ import {
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/examples/radix/ui/dropdown-menu"
|
||||
import { Field, FieldLabel } from "@/examples/radix/ui/field"
|
||||
} from "@/registry/new-york-v4/ui/dropdown-menu"
|
||||
import { Field, FieldLabel } from "@/registry/new-york-v4/ui/field"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupTextarea,
|
||||
} from "@/examples/radix/ui/input-group"
|
||||
} from "@/registry/new-york-v4/ui/input-group"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/examples/radix/ui/popover"
|
||||
import { Switch } from "@/examples/radix/ui/switch"
|
||||
} from "@/registry/new-york-v4/ui/popover"
|
||||
import { Switch } from "@/registry/new-york-v4/ui/switch"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/examples/radix/ui/tooltip"
|
||||
import {
|
||||
IconApps,
|
||||
IconArrowUp,
|
||||
IconAt,
|
||||
IconBook,
|
||||
IconCircleDashedPlus,
|
||||
IconPaperclip,
|
||||
IconPlus,
|
||||
IconWorld,
|
||||
IconX,
|
||||
} from "@tabler/icons-react"
|
||||
} from "@/registry/new-york-v4/ui/tooltip"
|
||||
|
||||
const SAMPLE_DATA = {
|
||||
mentionable: [
|
||||
@@ -185,7 +190,7 @@ export function NotionPromptForm() {
|
||||
const hasMentions = mentions.length > 0
|
||||
|
||||
return (
|
||||
<form>
|
||||
<form className="[--radius:1.2rem]">
|
||||
<Field>
|
||||
<FieldLabel htmlFor="notion-prompt" className="sr-only">
|
||||
Prompt
|
||||
@@ -217,7 +222,7 @@ export function NotionPromptForm() {
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Mention a person, page, or date</TooltipContent>
|
||||
</Tooltip>
|
||||
<PopoverContent className="p-0" align="start">
|
||||
<PopoverContent className="p-0 [--radius:1.2rem]" align="start">
|
||||
<Command>
|
||||
<CommandInput placeholder="Search pages..." />
|
||||
<CommandList>
|
||||
@@ -301,8 +306,12 @@ export function NotionPromptForm() {
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Select AI model</TooltipContent>
|
||||
</Tooltip>
|
||||
<DropdownMenuContent side="top" align="start" className="w-48">
|
||||
<DropdownMenuGroup className="w-48">
|
||||
<DropdownMenuContent
|
||||
side="top"
|
||||
align="start"
|
||||
className="[--radius:1rem]"
|
||||
>
|
||||
<DropdownMenuGroup className="w-42">
|
||||
<DropdownMenuLabel className="text-muted-foreground text-xs">
|
||||
Select Agent Mode
|
||||
</DropdownMenuLabel>
|
||||
@@ -337,7 +346,11 @@ export function NotionPromptForm() {
|
||||
<IconWorld /> All Sources
|
||||
</InputGroupButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent side="top" align="end" className="w-72">
|
||||
<DropdownMenuContent
|
||||
side="top"
|
||||
align="end"
|
||||
className="[--radius:1rem]"
|
||||
>
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem
|
||||
asChild
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Badge } from "@/examples/radix/ui/badge"
|
||||
import { Spinner } from "@/examples/radix/ui/spinner"
|
||||
import { Badge } from "@/registry/new-york-v4/ui/badge"
|
||||
import { Spinner } from "@/registry/new-york-v4/ui/spinner"
|
||||
|
||||
export function SpinnerBadge() {
|
||||
return (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Button } from "@/examples/radix/ui/button"
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import {
|
||||
Empty,
|
||||
EmptyContent,
|
||||
@@ -6,8 +6,8 @@ import {
|
||||
EmptyHeader,
|
||||
EmptyMedia,
|
||||
EmptyTitle,
|
||||
} from "@/examples/radix/ui/empty"
|
||||
import { Spinner } from "@/examples/radix/ui/spinner"
|
||||
} from "@/registry/new-york-v4/ui/empty"
|
||||
import { Spinner } from "@/registry/new-york-v4/ui/spinner"
|
||||
|
||||
export function SpinnerEmpty() {
|
||||
return (
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { type Metadata } from "next"
|
||||
import Image from "next/image"
|
||||
import Link from "next/link"
|
||||
import { PlusSignIcon } from "@hugeicons/core-free-icons"
|
||||
import { HugeiconsIcon } from "@hugeicons/react"
|
||||
|
||||
import { Announcement } from "@/components/announcement"
|
||||
import { ExamplesNav } from "@/components/examples-nav"
|
||||
@@ -56,7 +58,10 @@ export default function IndexPage() {
|
||||
<PageHeaderDescription>{description}</PageHeaderDescription>
|
||||
<PageActions>
|
||||
<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 asChild size="sm" variant="ghost" className="rounded-lg">
|
||||
<Link href="/docs/components">View Components</Link>
|
||||
|
||||
@@ -2,11 +2,7 @@ import * as React from "react"
|
||||
import { notFound } from "next/navigation"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import {
|
||||
ChartDisplay,
|
||||
getCachedRegistryItem,
|
||||
getChartHighlightedCode,
|
||||
} from "@/components/chart-display"
|
||||
import { ChartDisplay } from "@/components/chart-display"
|
||||
import { getActiveStyle } from "@/registry/_legacy-styles"
|
||||
import { charts } from "@/app/(app)/charts/charts"
|
||||
|
||||
@@ -48,26 +44,6 @@ export default async function ChartPage({ params }: ChartPageProps) {
|
||||
const chartList = charts[chartType]
|
||||
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 (
|
||||
<div className="grid flex-1 gap-12 lg:gap-24">
|
||||
<h2 className="sr-only">
|
||||
@@ -75,14 +51,16 @@ export default async function ChartPage({ params }: ChartPageProps) {
|
||||
</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">
|
||||
{Array.from({ length: 12 }).map((_, index) => {
|
||||
const chart = prefetchedCharts[index]
|
||||
const chart = chartList[index]
|
||||
return chart ? (
|
||||
<ChartDisplay
|
||||
key={chart.name}
|
||||
chart={chart}
|
||||
style={activeStyle.name}
|
||||
key={chart.id}
|
||||
name={chart.id}
|
||||
styleName={activeStyle.name}
|
||||
className={cn(chart.fullWidth && "md:col-span-2 lg:col-span-3")}
|
||||
/>
|
||||
>
|
||||
<chart.component />
|
||||
</ChartDisplay>
|
||||
) : (
|
||||
<div
|
||||
key={`empty-${index}`}
|
||||
|
||||
@@ -63,8 +63,9 @@ export default function ChartsLayout({
|
||||
</PageHeader>
|
||||
<PageNav id="charts">
|
||||
<ChartsNav />
|
||||
<ThemeSelector className="mr-4 hidden md:flex" />
|
||||
</PageNav>
|
||||
<div className="container-wrapper flex-1">
|
||||
<div className="container-wrapper section-soft flex-1">
|
||||
<div className="container pb-6">
|
||||
<section className="theme-container">{children}</section>
|
||||
</div>
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
import Link from "next/link"
|
||||
import { notFound } from "next/navigation"
|
||||
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 z from "zod"
|
||||
|
||||
import { source } from "@/lib/source"
|
||||
import { absoluteUrl } from "@/lib/utils"
|
||||
import { DocsBaseSwitcher } from "@/components/docs-base-switcher"
|
||||
import { DocsCopyPage } from "@/components/docs-copy-page"
|
||||
import { DocsTableOfContents } from "@/components/docs-toc"
|
||||
import { OpenInV0Cta } from "@/components/open-in-v0-cta"
|
||||
import { Badge } from "@/registry/new-york-v4/ui/badge"
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
|
||||
export const revalidate = false
|
||||
@@ -79,114 +85,127 @@ export default async function Page(props: {
|
||||
|
||||
const doc = page.data
|
||||
const MDX = doc.body
|
||||
const isChangelog = params.slug?.[0] === "changelog"
|
||||
const neighbours = isChangelog
|
||||
? { previous: null, next: null }
|
||||
: findNeighbour(source.pageTree, page.url)
|
||||
const neighbours = findNeighbour(source.pageTree, page.url)
|
||||
|
||||
const raw = await page.data.getText("raw")
|
||||
const { attributes } = fm(raw)
|
||||
const { links } = z
|
||||
.object({
|
||||
links: z
|
||||
.object({
|
||||
doc: z.string().optional(),
|
||||
api: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
.parse(attributes)
|
||||
|
||||
return (
|
||||
<div
|
||||
data-slot="docs"
|
||||
className="flex scroll-mt-24 items-stretch pb-8 text-[1.05rem] sm:text-[15px] xl:w-full"
|
||||
>
|
||||
<div className="flex items-stretch text-[1.05rem] sm:text-[15px] xl:w-full">
|
||||
<div className="flex min-w-0 flex-1 flex-col">
|
||||
<div className="h-(--top-spacing) shrink-0" />
|
||||
<div className="mx-auto flex w-full max-w-[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 items-start justify-between">
|
||||
<h1 className="scroll-m-24 text-4xl 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}
|
||||
</h1>
|
||||
<div className="docs-nav bg-background/80 border-border/50 fixed inset-x-0 bottom-0 isolate z-50 flex items-center gap-2 border-t px-6 py-4 backdrop-blur-sm sm:static sm:z-0 sm:border-t-0 sm:bg-transparent sm:px-0 sm:py-1.5 sm:backdrop-blur-none">
|
||||
<div className="docs-nav bg-background/80 border-border/50 fixed inset-x-0 bottom-0 isolate z-50 flex items-center gap-2 border-t px-6 py-4 backdrop-blur-sm sm:static sm:z-0 sm:border-t-0 sm:bg-transparent sm:px-0 sm:pt-1.5 sm:backdrop-blur-none">
|
||||
<DocsCopyPage page={raw} url={absoluteUrl(page.url)} />
|
||||
<div className="ml-auto flex gap-2">
|
||||
{neighbours.previous && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="icon"
|
||||
className="extend-touch-target size-8 shadow-none md:size-7"
|
||||
asChild
|
||||
>
|
||||
<Link href={neighbours.previous.url}>
|
||||
<IconArrowLeft />
|
||||
<span className="sr-only">Previous</span>
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
{neighbours.next && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="icon"
|
||||
className="extend-touch-target size-8 shadow-none md:size-7"
|
||||
asChild
|
||||
>
|
||||
<Link href={neighbours.next.url}>
|
||||
<span className="sr-only">Next</span>
|
||||
<IconArrowRight />
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{neighbours.previous && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="icon"
|
||||
className="extend-touch-target ml-auto size-8 shadow-none md:size-7"
|
||||
asChild
|
||||
>
|
||||
<Link href={neighbours.previous.url}>
|
||||
<IconArrowLeft />
|
||||
<span className="sr-only">Previous</span>
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
{neighbours.next && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="icon"
|
||||
className="extend-touch-target size-8 shadow-none md:size-7"
|
||||
asChild
|
||||
>
|
||||
<Link href={neighbours.next.url}>
|
||||
<span className="sr-only">Next</span>
|
||||
<IconArrowRight />
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{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}
|
||||
</p>
|
||||
)}
|
||||
</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 className="w-full flex-1 pb-16 *:data-[slot=alert]:first:mt-0 sm:pb-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"
|
||||
/>
|
||||
)}
|
||||
<div className="w-full flex-1 *:data-[slot=alert]:first:mt-0">
|
||||
<MDX components={mdxComponents} />
|
||||
</div>
|
||||
<div className="hidden h-16 w-full items-center gap-2 px-4 sm:flex sm:px-0">
|
||||
{neighbours.previous && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
asChild
|
||||
className="shadow-none"
|
||||
>
|
||||
<Link href={neighbours.previous.url}>
|
||||
<IconArrowLeft /> {neighbours.previous.name}
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
{neighbours.next && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
className="ml-auto shadow-none"
|
||||
asChild
|
||||
>
|
||||
<Link href={neighbours.next.url}>
|
||||
{neighbours.next.name} <IconArrowRight />
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx-auto hidden h-16 w-full max-w-2xl items-center gap-2 px-4 sm:flex md:px-0">
|
||||
{neighbours.previous && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
asChild
|
||||
className="shadow-none"
|
||||
>
|
||||
<Link href={neighbours.previous.url}>
|
||||
<IconArrowLeft /> {neighbours.previous.name}
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
{neighbours.next && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
className="ml-auto shadow-none"
|
||||
asChild
|
||||
>
|
||||
<Link href={neighbours.next.url}>
|
||||
{neighbours.next.name} <IconArrowRight />
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
</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="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" />
|
||||
{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} />
|
||||
<div className="h-12" />
|
||||
</div>
|
||||
) : 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 />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,140 +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>
|
||||
<ul className="flex flex-col gap-4">
|
||||
{olderPages.map((page) => {
|
||||
const data = page.data as ChangelogPageData
|
||||
return (
|
||||
<li key={page.url} className="flex items-center gap-3">
|
||||
<Link
|
||||
href={page.url}
|
||||
className="font-medium hover:underline"
|
||||
>
|
||||
{data.title}
|
||||
</Link>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</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>
|
||||
)
|
||||
}
|
||||
@@ -9,10 +9,7 @@ export default function DocsLayout({
|
||||
}) {
|
||||
return (
|
||||
<div className="container-wrapper flex flex-1 flex-col px-2">
|
||||
<SidebarProvider
|
||||
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": "220px" } as React.CSSProperties}
|
||||
>
|
||||
<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)]">
|
||||
<DocsSidebar tree={source.pageTree} />
|
||||
<div className="h-full w-full">{children}</div>
|
||||
</SidebarProvider>
|
||||
|
||||
@@ -3,23 +3,10 @@ import { NextResponse, type NextRequest } from "next/server"
|
||||
|
||||
import { processMdxForLLMs } from "@/lib/llm"
|
||||
import { source } from "@/lib/source"
|
||||
import { getActiveStyle, type Style } from "@/registry/_legacy-styles"
|
||||
import { getActiveStyle } from "@/registry/_legacy-styles"
|
||||
|
||||
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(
|
||||
_req: NextRequest,
|
||||
{ params }: { params: Promise<{ slug?: string[] }> }
|
||||
@@ -32,11 +19,9 @@ export async function GET(
|
||||
notFound()
|
||||
}
|
||||
|
||||
const effectiveStyle = getStyleFromSlug(slug, activeStyle.name)
|
||||
|
||||
const processedContent = processMdxForLLMs(
|
||||
await page.data.getText("raw"),
|
||||
effectiveStyle as Style["name"]
|
||||
activeStyle.name
|
||||
)
|
||||
|
||||
return new NextResponse(processedContent, {
|
||||
|
||||
@@ -26,8 +26,8 @@ export function MenuAccentPicker({
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="group/picker relative">
|
||||
<Picker>
|
||||
<Picker>
|
||||
<div className="group/picker relative">
|
||||
<PickerTrigger>
|
||||
<div className="flex flex-col justify-start text-left">
|
||||
<div className="text-muted-foreground text-xs">Menu Accent</div>
|
||||
@@ -35,7 +35,7 @@ export function MenuAccentPicker({
|
||||
{currentAccent?.label}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-foreground pointer-events-none absolute top-1/2 right-4 flex size-4 -translate-y-1/2 items-center justify-center text-base select-none">
|
||||
<div className="text-foreground absolute top-1/2 right-4 flex size-4 -translate-y-1/2 items-center justify-center text-base">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="128"
|
||||
@@ -65,31 +65,31 @@ export function MenuAccentPicker({
|
||||
</svg>
|
||||
</div>
|
||||
</PickerTrigger>
|
||||
<PickerContent
|
||||
anchor={isMobile ? anchorRef : undefined}
|
||||
side={isMobile ? "top" : "right"}
|
||||
align={isMobile ? "center" : "start"}
|
||||
<LockButton
|
||||
param="menuAccent"
|
||||
className="absolute top-1/2 right-10 -translate-y-1/2"
|
||||
/>
|
||||
</div>
|
||||
<PickerContent
|
||||
anchor={isMobile ? anchorRef : undefined}
|
||||
side={isMobile ? "top" : "right"}
|
||||
align={isMobile ? "center" : "start"}
|
||||
>
|
||||
<PickerRadioGroup
|
||||
value={currentAccent?.value}
|
||||
onValueChange={(value) => {
|
||||
setParams({ menuAccent: value as MenuAccentValue })
|
||||
}}
|
||||
>
|
||||
<PickerRadioGroup
|
||||
value={currentAccent?.value}
|
||||
onValueChange={(value) => {
|
||||
setParams({ menuAccent: value as MenuAccentValue })
|
||||
}}
|
||||
>
|
||||
<PickerGroup>
|
||||
{MENU_ACCENTS.map((accent) => (
|
||||
<PickerRadioItem key={accent.value} value={accent.value}>
|
||||
{accent.label}
|
||||
</PickerRadioItem>
|
||||
))}
|
||||
</PickerGroup>
|
||||
</PickerRadioGroup>
|
||||
</PickerContent>
|
||||
</Picker>
|
||||
<LockButton
|
||||
param="menuAccent"
|
||||
className="absolute top-1/2 right-10 -translate-y-1/2"
|
||||
/>
|
||||
</div>
|
||||
<PickerGroup>
|
||||
{MENU_ACCENTS.map((accent) => (
|
||||
<PickerRadioItem key={accent.value} value={accent.value}>
|
||||
{accent.label}
|
||||
</PickerRadioItem>
|
||||
))}
|
||||
</PickerGroup>
|
||||
</PickerRadioGroup>
|
||||
</PickerContent>
|
||||
</Picker>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -35,8 +35,8 @@ export function BaseColorPicker({
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="group/picker relative">
|
||||
<Picker>
|
||||
<Picker>
|
||||
<div className="group/picker relative">
|
||||
<PickerTrigger>
|
||||
<div className="flex flex-col justify-start text-left">
|
||||
<div className="text-muted-foreground text-xs">Base Color</div>
|
||||
@@ -54,72 +54,72 @@ export function BaseColorPicker({
|
||||
]?.["muted-foreground"],
|
||||
} as React.CSSProperties
|
||||
}
|
||||
className="pointer-events-none absolute top-1/2 right-4 size-4 -translate-y-1/2 rounded-full bg-(--color) select-none"
|
||||
className="absolute top-1/2 right-4 size-4 -translate-y-1/2 rounded-full bg-(--color)"
|
||||
/>
|
||||
)}
|
||||
</PickerTrigger>
|
||||
<PickerContent
|
||||
anchor={isMobile ? anchorRef : undefined}
|
||||
side={isMobile ? "top" : "right"}
|
||||
align={isMobile ? "center" : "start"}
|
||||
>
|
||||
<PickerRadioGroup
|
||||
value={currentBaseColor?.name}
|
||||
onValueChange={(value) => {
|
||||
if (value === "dark") {
|
||||
setTheme(resolvedTheme === "dark" ? "light" : "dark")
|
||||
return
|
||||
}
|
||||
<LockButton
|
||||
param="baseColor"
|
||||
className="absolute top-1/2 right-10 -translate-y-1/2"
|
||||
/>
|
||||
</div>
|
||||
<PickerContent
|
||||
anchor={isMobile ? anchorRef : undefined}
|
||||
side={isMobile ? "top" : "right"}
|
||||
align={isMobile ? "center" : "start"}
|
||||
>
|
||||
<PickerRadioGroup
|
||||
value={currentBaseColor?.name}
|
||||
onValueChange={(value) => {
|
||||
if (value === "dark") {
|
||||
setTheme(resolvedTheme === "dark" ? "light" : "dark")
|
||||
return
|
||||
}
|
||||
|
||||
setParams({ baseColor: value as BaseColorName })
|
||||
}}
|
||||
>
|
||||
<PickerGroup>
|
||||
{BASE_COLORS.map((baseColor) => (
|
||||
<PickerRadioItem key={baseColor.name} value={baseColor.name}>
|
||||
<div className="flex items-center gap-2">
|
||||
{mounted && resolvedTheme && (
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"--color":
|
||||
baseColor.cssVars?.[
|
||||
resolvedTheme as "light" | "dark"
|
||||
]?.["muted-foreground"],
|
||||
} as React.CSSProperties
|
||||
}
|
||||
className="size-4 rounded-full bg-(--color)"
|
||||
/>
|
||||
)}
|
||||
{baseColor.title}
|
||||
</div>
|
||||
</PickerRadioItem>
|
||||
))}
|
||||
</PickerGroup>
|
||||
<PickerSeparator />
|
||||
<PickerGroup>
|
||||
<PickerItem
|
||||
onClick={() => {
|
||||
setTheme(resolvedTheme === "dark" ? "light" : "dark")
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col justify-start pointer-coarse:gap-1">
|
||||
<div>
|
||||
Switch to {resolvedTheme === "dark" ? "Light" : "Dark"} Mode
|
||||
</div>
|
||||
<div className="text-muted-foreground text-xs pointer-coarse:text-sm">
|
||||
Base colors are easier to see in dark mode.
|
||||
</div>
|
||||
setParams({ baseColor: value as BaseColorName })
|
||||
}}
|
||||
>
|
||||
<PickerGroup>
|
||||
{BASE_COLORS.map((baseColor) => (
|
||||
<PickerRadioItem key={baseColor.name} value={baseColor.name}>
|
||||
<div className="flex items-center gap-2">
|
||||
{mounted && resolvedTheme && (
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"--color":
|
||||
baseColor.cssVars?.[
|
||||
resolvedTheme as "light" | "dark"
|
||||
]?.["muted-foreground"],
|
||||
} as React.CSSProperties
|
||||
}
|
||||
className="size-4 rounded-full bg-(--color)"
|
||||
/>
|
||||
)}
|
||||
{baseColor.title}
|
||||
</div>
|
||||
</PickerItem>
|
||||
</PickerGroup>
|
||||
</PickerRadioGroup>
|
||||
</PickerContent>
|
||||
</Picker>
|
||||
<LockButton
|
||||
param="baseColor"
|
||||
className="absolute top-1/2 right-10 -translate-y-1/2"
|
||||
/>
|
||||
</div>
|
||||
</PickerRadioItem>
|
||||
))}
|
||||
</PickerGroup>
|
||||
<PickerSeparator />
|
||||
<PickerGroup>
|
||||
<PickerItem
|
||||
onClick={() => {
|
||||
setTheme(resolvedTheme === "dark" ? "light" : "dark")
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col justify-start pointer-coarse:gap-1">
|
||||
<div>
|
||||
Switch to {resolvedTheme === "dark" ? "Light" : "Dark"} Mode
|
||||
</div>
|
||||
<div className="text-muted-foreground text-xs pointer-coarse:text-sm">
|
||||
Base colors are easier to see in dark mode.
|
||||
</div>
|
||||
</div>
|
||||
</PickerItem>
|
||||
</PickerGroup>
|
||||
</PickerRadioGroup>
|
||||
</PickerContent>
|
||||
</Picker>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ export function BasePicker({
|
||||
</div>
|
||||
{currentBase?.meta?.logo && (
|
||||
<div
|
||||
className="text-foreground *:[svg]:text-foreground! pointer-events-none absolute top-1/2 right-4 size-4 -translate-y-1/2 select-none *:[svg]:size-4"
|
||||
className="text-foreground *:[svg]:text-foreground! absolute top-1/2 right-4 size-4 -translate-y-1/2 *:[svg]:size-4"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: currentBase.meta.logo,
|
||||
}}
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
|
||||
import * as React from "react"
|
||||
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 { cn } from "@/lib/utils"
|
||||
import {
|
||||
BASE_COLORS,
|
||||
DEFAULT_CONFIG,
|
||||
getThemesForBaseColor,
|
||||
iconLibraries,
|
||||
MENU_ACCENTS,
|
||||
@@ -16,11 +18,6 @@ import {
|
||||
} from "@/registry/config"
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
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 { FONTS } from "@/app/(create)/lib/fonts"
|
||||
import {
|
||||
@@ -36,10 +33,26 @@ function randomItem<T>(array: readonly T[]): T {
|
||||
return array[Math.floor(Math.random() * array.length)]
|
||||
}
|
||||
|
||||
export function RandomButton() {
|
||||
export function CustomizerControls({ className }: { className?: string }) {
|
||||
const { locks } = useLocks()
|
||||
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(() => {
|
||||
// Use current value if locked, otherwise randomize.
|
||||
const baseColor = locks.has("baseColor")
|
||||
@@ -117,30 +130,33 @@ export function RandomButton() {
|
||||
}, [handleRandomize])
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
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!"
|
||||
>
|
||||
<div className="flex flex-col justify-start text-left">
|
||||
<div className="text-muted-foreground text-xs">Shuffle</div>
|
||||
<div className="text-foreground text-sm font-medium">
|
||||
Try Random
|
||||
</div>
|
||||
</div>
|
||||
<HugeiconsIcon icon={DiceFaces05Icon} className="size-5 md:hidden" />
|
||||
<Kbd className="bg-foreground/10 text-foreground hidden md:flex">
|
||||
R
|
||||
</Kbd>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="left">
|
||||
Use browser back/forward to navigate history
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<div className={cn("items-center gap-0", className)}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
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!"
|
||||
>
|
||||
<div className="flex flex-col justify-start text-left">
|
||||
<div className="text-muted-foreground text-xs">Shuffle</div>
|
||||
<div className="text-foreground text-sm font-medium">Try Random</div>
|
||||
</div>
|
||||
<HugeiconsIcon icon={DiceFaces05Icon} className="size-5 md:hidden" />
|
||||
<Kbd className="bg-foreground/10 text-foreground hidden md:flex">R</Kbd>
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleReset}
|
||||
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>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -7,18 +7,15 @@ import { HugeiconsIcon } from "@hugeicons/react"
|
||||
import { useIsMobile } from "@/hooks/use-mobile"
|
||||
import { getThemesForBaseColor, PRESETS, STYLES } from "@/registry/config"
|
||||
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 { BaseColorPicker } from "@/app/(create)/components/base-color-picker"
|
||||
import { BasePicker } from "@/app/(create)/components/base-picker"
|
||||
import { DirectionPicker } from "@/app/(create)/components/direction-picker"
|
||||
import { CustomizerControls } from "@/app/(create)/components/customizer-controls"
|
||||
import { FontPicker } from "@/app/(create)/components/font-picker"
|
||||
import { IconLibraryPicker } from "@/app/(create)/components/icon-library-picker"
|
||||
import { MenuColorPicker } from "@/app/(create)/components/menu-picker"
|
||||
import { PresetPicker } from "@/app/(create)/components/preset-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 { ThemePicker } from "@/app/(create)/components/theme-picker"
|
||||
import { FONTS } from "@/app/(create)/lib/fonts"
|
||||
@@ -39,7 +36,7 @@ export function Customizer() {
|
||||
className="no-scrollbar -mx-2.5 flex flex-col overflow-y-auto p-1 md:mx-0 md:h-[calc(100svh-var(--header-height)-2rem)] md:w-48 md:gap-0 md:py-0"
|
||||
ref={anchorRef}
|
||||
>
|
||||
<div className="--md:flex hidden items-center gap-2 px-[calc(--spacing(2.5))] pb-1 md:flex-col md:items-start">
|
||||
<div className="hidden items-center gap-2 px-[calc(--spacing(2.5))] pb-1 md:flex md:flex-col md:items-start">
|
||||
<HugeiconsIcon
|
||||
icon={Settings05Icon}
|
||||
className="size-4"
|
||||
@@ -76,13 +73,9 @@ export function Customizer() {
|
||||
<IconLibraryPicker isMobile={isMobile} anchorRef={anchorRef} />
|
||||
<FontPicker fonts={FONTS} isMobile={isMobile} anchorRef={anchorRef} />
|
||||
<RadiusPicker isMobile={isMobile} anchorRef={anchorRef} />
|
||||
<DirectionPicker isMobile={isMobile} anchorRef={anchorRef} />
|
||||
<MenuColorPicker isMobile={isMobile} anchorRef={anchorRef} />
|
||||
<MenuAccentPicker isMobile={isMobile} anchorRef={anchorRef} />
|
||||
<div className="mt-auto hidden w-full flex-col items-center gap-0 md:flex">
|
||||
<RandomButton />
|
||||
<ResetButton />
|
||||
</div>
|
||||
<CustomizerControls className="mt-auto hidden w-full flex-col md:flex" />
|
||||
</FieldGroup>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,23 +7,21 @@ import {
|
||||
DEFAULT_CONFIG,
|
||||
type DesignSystemConfig,
|
||||
} from "@/registry/config"
|
||||
import { useIframeMessageListener } from "@/app/(create)/hooks/use-iframe-sync"
|
||||
import { useDesignSystemParam } from "@/app/(create)/hooks/use-design-system"
|
||||
import { FONTS } from "@/app/(create)/lib/fonts"
|
||||
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
||||
|
||||
export function DesignSystemProvider({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
const [
|
||||
{ style, theme, font, baseColor, direction, menuAccent, menuColor, radius },
|
||||
setSearchParams,
|
||||
] = useDesignSystemSearchParams({
|
||||
shallow: true, // No need to go through the server…
|
||||
history: "replace", // …or push updates into the iframe history.
|
||||
})
|
||||
useIframeMessageListener("design-system-params", setSearchParams)
|
||||
const style = useDesignSystemParam("style")
|
||||
const theme = useDesignSystemParam("theme")
|
||||
const font = useDesignSystemParam("font")
|
||||
const baseColor = useDesignSystemParam("baseColor")
|
||||
const menuAccent = useDesignSystemParam("menuAccent")
|
||||
const menuColor = useDesignSystemParam("menuColor")
|
||||
const radius = useDesignSystemParam("radius")
|
||||
const [isReady, setIsReady] = React.useState(false)
|
||||
|
||||
// Use useLayoutEffect for synchronous style updates to prevent flash.
|
||||
@@ -166,13 +164,6 @@ export function DesignSystemProvider({
|
||||
}
|
||||
}, [menuColor])
|
||||
|
||||
// Set dir attribute on html element when direction changes.
|
||||
React.useLayoutEffect(() => {
|
||||
if (direction) {
|
||||
document.documentElement.dir = direction
|
||||
}
|
||||
}, [direction])
|
||||
|
||||
if (!isReady) {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -1,122 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import { type DirectionValue } from "@/registry/config"
|
||||
import { LockButton } from "@/app/(create)/components/lock-button"
|
||||
import {
|
||||
Picker,
|
||||
PickerContent,
|
||||
PickerGroup,
|
||||
PickerRadioGroup,
|
||||
PickerRadioItem,
|
||||
PickerTrigger,
|
||||
} from "@/app/(create)/components/picker"
|
||||
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
||||
|
||||
const DIRECTION_OPTIONS = [
|
||||
{
|
||||
value: "ltr" as const,
|
||||
label: "LTR",
|
||||
description: "Left-to-Right",
|
||||
icon: (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="128"
|
||||
height="128"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
role="img"
|
||||
stroke="currentColor"
|
||||
className="text-foreground"
|
||||
>
|
||||
<path
|
||||
d="M3 12h18M15 6l6 6-6 6"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
value: "rtl" as const,
|
||||
label: "RTL",
|
||||
description: "Right-to-Left",
|
||||
icon: (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="128"
|
||||
height="128"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
role="img"
|
||||
stroke="currentColor"
|
||||
className="text-foreground"
|
||||
>
|
||||
<path
|
||||
d="M21 12H3M9 18l-6-6 6-6"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
] as const
|
||||
|
||||
export function DirectionPicker({
|
||||
isMobile,
|
||||
anchorRef,
|
||||
}: {
|
||||
isMobile: boolean
|
||||
anchorRef: React.RefObject<HTMLDivElement | null>
|
||||
}) {
|
||||
const [params, setParams] = useDesignSystemSearchParams()
|
||||
const currentDirection = DIRECTION_OPTIONS.find(
|
||||
(direction) => direction.value === params.direction
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="group/picker relative">
|
||||
<Picker>
|
||||
<PickerTrigger>
|
||||
<div className="flex flex-col justify-start text-left">
|
||||
<div className="text-muted-foreground text-xs">Direction</div>
|
||||
<div className="text-foreground text-sm font-medium">
|
||||
{currentDirection?.label}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-foreground pointer-events-none absolute top-1/2 right-4 flex size-4 -translate-y-1/2 items-center justify-center text-base select-none">
|
||||
{currentDirection?.icon}
|
||||
</div>
|
||||
</PickerTrigger>
|
||||
<PickerContent
|
||||
anchor={isMobile ? anchorRef : undefined}
|
||||
side={isMobile ? "top" : "right"}
|
||||
align={isMobile ? "center" : "start"}
|
||||
>
|
||||
<PickerRadioGroup
|
||||
value={currentDirection?.value}
|
||||
onValueChange={(value) => {
|
||||
setParams({ direction: value as DirectionValue })
|
||||
}}
|
||||
>
|
||||
<PickerGroup>
|
||||
{DIRECTION_OPTIONS.map((direction) => (
|
||||
<PickerRadioItem key={direction.value} value={direction.value}>
|
||||
{direction.icon}
|
||||
{direction.label}
|
||||
</PickerRadioItem>
|
||||
))}
|
||||
</PickerGroup>
|
||||
</PickerRadioGroup>
|
||||
</PickerContent>
|
||||
</Picker>
|
||||
<LockButton
|
||||
param="direction"
|
||||
className="absolute top-1/2 right-10 -translate-y-1/2"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -39,8 +39,8 @@ export function FontPicker({
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="group/picker relative">
|
||||
<Picker>
|
||||
<Picker>
|
||||
<div className="group/picker relative">
|
||||
<PickerTrigger>
|
||||
<div className="flex flex-col justify-start text-left">
|
||||
<div className="text-muted-foreground text-xs">Font</div>
|
||||
@@ -49,55 +49,54 @@ export function FontPicker({
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="text-foreground pointer-events-none absolute top-1/2 right-4 flex size-4 -translate-y-1/2 items-center justify-center text-base select-none"
|
||||
className="text-foreground absolute top-1/2 right-4 flex size-4 -translate-y-1/2 items-center justify-center text-base"
|
||||
style={{ fontFamily: currentFont?.font.style.fontFamily }}
|
||||
>
|
||||
Aa
|
||||
</div>
|
||||
</PickerTrigger>
|
||||
<PickerContent
|
||||
anchor={isMobile ? anchorRef : undefined}
|
||||
side={isMobile ? "top" : "right"}
|
||||
align={isMobile ? "center" : "start"}
|
||||
className="max-h-80 md:w-72"
|
||||
<LockButton
|
||||
param="font"
|
||||
className="absolute top-1/2 right-10 -translate-y-1/2"
|
||||
/>
|
||||
</div>
|
||||
<PickerContent
|
||||
anchor={isMobile ? anchorRef : undefined}
|
||||
side={isMobile ? "top" : "right"}
|
||||
align={isMobile ? "center" : "start"}
|
||||
className="max-h-80 md:w-72"
|
||||
>
|
||||
<PickerRadioGroup
|
||||
value={currentFont?.value}
|
||||
onValueChange={(value) => {
|
||||
setParams({ font: value as FontValue })
|
||||
}}
|
||||
>
|
||||
<PickerRadioGroup
|
||||
value={currentFont?.value}
|
||||
onValueChange={(value) => {
|
||||
setParams({ font: value as FontValue })
|
||||
}}
|
||||
>
|
||||
<PickerGroup>
|
||||
{fonts.map((font, index) => (
|
||||
<React.Fragment key={font.value}>
|
||||
<PickerRadioItem value={font.value}>
|
||||
<Item size="xs">
|
||||
<ItemContent className="gap-1">
|
||||
<ItemTitle className="text-muted-foreground text-xs font-medium">
|
||||
{font.name}
|
||||
</ItemTitle>
|
||||
<ItemDescription
|
||||
style={{ fontFamily: font.font.style.fontFamily }}
|
||||
>
|
||||
Designers love packing quirky glyphs into test
|
||||
phrases.
|
||||
</ItemDescription>
|
||||
</ItemContent>
|
||||
</Item>
|
||||
</PickerRadioItem>
|
||||
{index < fonts.length - 1 && (
|
||||
<PickerSeparator className="opacity-50" />
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</PickerGroup>
|
||||
</PickerRadioGroup>
|
||||
</PickerContent>
|
||||
</Picker>
|
||||
<LockButton
|
||||
param="font"
|
||||
className="absolute top-1/2 right-10 -translate-y-1/2"
|
||||
/>
|
||||
</div>
|
||||
<PickerGroup>
|
||||
{fonts.map((font, index) => (
|
||||
<React.Fragment key={font.value}>
|
||||
<PickerRadioItem value={font.value}>
|
||||
<Item size="xs">
|
||||
<ItemContent className="gap-1">
|
||||
<ItemTitle className="text-muted-foreground text-xs font-medium">
|
||||
{font.name}
|
||||
</ItemTitle>
|
||||
<ItemDescription
|
||||
style={{ fontFamily: font.font.style.fontFamily }}
|
||||
>
|
||||
Designers love packing quirky glyphs into test phrases.
|
||||
</ItemDescription>
|
||||
</ItemContent>
|
||||
</Item>
|
||||
</PickerRadioItem>
|
||||
{index < fonts.length - 1 && (
|
||||
<PickerSeparator className="opacity-50" />
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</PickerGroup>
|
||||
</PickerRadioGroup>
|
||||
</PickerContent>
|
||||
</Picker>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -39,18 +39,6 @@ const IconHugeicons = lazy(() =>
|
||||
}))
|
||||
)
|
||||
|
||||
const IconPhosphor = lazy(() =>
|
||||
import("@/registry/icons/icon-phosphor").then((mod) => ({
|
||||
default: mod.IconPhosphor,
|
||||
}))
|
||||
)
|
||||
|
||||
const IconRemixicon = lazy(() =>
|
||||
import("@/registry/icons/icon-remixicon").then((mod) => ({
|
||||
default: mod.IconRemixicon,
|
||||
}))
|
||||
)
|
||||
|
||||
const PREVIEW_ICONS = {
|
||||
lucide: [
|
||||
"CopyIcon",
|
||||
@@ -100,38 +88,6 @@ const PREVIEW_ICONS = {
|
||||
"ArrowDown01Icon",
|
||||
"ArrowRight01Icon",
|
||||
],
|
||||
phosphor: [
|
||||
"CopyIcon",
|
||||
"WarningCircleIcon",
|
||||
"TrashIcon",
|
||||
"ShareIcon",
|
||||
"BagIcon",
|
||||
"DotsThreeIcon",
|
||||
"SpinnerIcon",
|
||||
"PlusIcon",
|
||||
"MinusIcon",
|
||||
"ArrowLeftIcon",
|
||||
"ArrowRightIcon",
|
||||
"CheckIcon",
|
||||
"CaretDownIcon",
|
||||
"CaretRightIcon",
|
||||
],
|
||||
remixicon: [
|
||||
"RiFileCopyLine",
|
||||
"RiErrorWarningLine",
|
||||
"RiDeleteBinLine",
|
||||
"RiShareLine",
|
||||
"RiShoppingBagLine",
|
||||
"RiMoreLine",
|
||||
"RiLoaderLine",
|
||||
"RiAddLine",
|
||||
"RiSubtractLine",
|
||||
"RiArrowLeftLine",
|
||||
"RiArrowRightLine",
|
||||
"RiCheckLine",
|
||||
"RiArrowDownSLine",
|
||||
"RiArrowRightSLine",
|
||||
],
|
||||
}
|
||||
|
||||
const logos = {
|
||||
@@ -198,35 +154,6 @@ const logos = {
|
||||
></path>
|
||||
</svg>
|
||||
),
|
||||
phosphor: (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 32 32"
|
||||
width="32"
|
||||
height="32"
|
||||
>
|
||||
<path fill="none" d="M0 0h32v32H0z" />
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M9 5h9v16H9zm9 16v9a9 9 0 0 1-9-9M9 5l9 16m0 0h1a8 8 0 0 0 0-16h-1"
|
||||
/>
|
||||
</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({
|
||||
@@ -244,8 +171,8 @@ export function IconLibraryPicker({
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="group/picker relative">
|
||||
<Picker>
|
||||
<Picker>
|
||||
<div className="group/picker relative">
|
||||
<PickerTrigger>
|
||||
<div className="flex flex-col justify-start text-left">
|
||||
<div className="text-muted-foreground text-xs">Icon Library</div>
|
||||
@@ -253,42 +180,42 @@ export function IconLibraryPicker({
|
||||
{currentIconLibrary?.title}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-foreground *:[svg]:text-foreground! pointer-events-none absolute top-1/2 right-4 flex size-4 -translate-y-1/2 items-center justify-center text-base select-none">
|
||||
<div className="text-foreground *:[svg]:text-foreground! absolute top-1/2 right-4 flex size-4 -translate-y-1/2 items-center justify-center text-base">
|
||||
{logos[currentIconLibrary?.name as keyof typeof logos]}
|
||||
</div>
|
||||
</PickerTrigger>
|
||||
<PickerContent
|
||||
anchor={isMobile ? anchorRef : undefined}
|
||||
side={isMobile ? "top" : "right"}
|
||||
align={isMobile ? "center" : "start"}
|
||||
<LockButton
|
||||
param="iconLibrary"
|
||||
className="absolute top-1/2 right-10 -translate-y-1/2"
|
||||
/>
|
||||
</div>
|
||||
<PickerContent
|
||||
anchor={isMobile ? anchorRef : undefined}
|
||||
side={isMobile ? "top" : "right"}
|
||||
align={isMobile ? "center" : "start"}
|
||||
>
|
||||
<PickerRadioGroup
|
||||
value={currentIconLibrary?.name}
|
||||
onValueChange={(value) => {
|
||||
setParams({ iconLibrary: value as IconLibraryName })
|
||||
}}
|
||||
>
|
||||
<PickerRadioGroup
|
||||
value={currentIconLibrary?.name}
|
||||
onValueChange={(value) => {
|
||||
setParams({ iconLibrary: value as IconLibraryName })
|
||||
}}
|
||||
>
|
||||
<PickerGroup>
|
||||
{Object.values(iconLibraries).map((iconLibrary, index) => (
|
||||
<React.Fragment key={iconLibrary.name}>
|
||||
<IconLibraryPickerItem
|
||||
iconLibrary={iconLibrary}
|
||||
value={iconLibrary.name}
|
||||
/>
|
||||
{index < Object.values(iconLibraries).length - 1 && (
|
||||
<PickerSeparator className="opacity-50" />
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</PickerGroup>
|
||||
</PickerRadioGroup>
|
||||
</PickerContent>
|
||||
</Picker>
|
||||
<LockButton
|
||||
param="iconLibrary"
|
||||
className="absolute top-1/2 right-10 -translate-y-1/2"
|
||||
/>
|
||||
</div>
|
||||
<PickerGroup>
|
||||
{Object.values(iconLibraries).map((iconLibrary, index) => (
|
||||
<React.Fragment key={iconLibrary.name}>
|
||||
<IconLibraryPickerItem
|
||||
iconLibrary={iconLibrary}
|
||||
value={iconLibrary.name}
|
||||
/>
|
||||
{index < Object.values(iconLibraries).length - 1 && (
|
||||
<PickerSeparator className="opacity-50" />
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</PickerGroup>
|
||||
</PickerRadioGroup>
|
||||
</PickerContent>
|
||||
</Picker>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -332,11 +259,7 @@ const IconLibraryPreview = memo(function IconLibraryPreview({
|
||||
? IconLucide
|
||||
: iconLibrary === "tabler"
|
||||
? IconTabler
|
||||
: iconLibrary === "hugeicons"
|
||||
? IconHugeicons
|
||||
: iconLibrary === "phosphor"
|
||||
? IconPhosphor
|
||||
: IconRemixicon
|
||||
: IconHugeicons
|
||||
|
||||
return (
|
||||
<Suspense
|
||||
|
||||
@@ -4,7 +4,7 @@ import { lazy, Suspense } from "react"
|
||||
import { SquareIcon } from "lucide-react"
|
||||
import type { IconLibraryName } from "shadcn/icons"
|
||||
|
||||
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
||||
import { useDesignSystemParam } from "@/app/(create)/hooks/use-design-system"
|
||||
|
||||
const IconLucide = lazy(() =>
|
||||
import("@/registry/icons/icon-lucide").then((mod) => ({
|
||||
@@ -24,24 +24,12 @@ const IconHugeicons = lazy(() =>
|
||||
}))
|
||||
)
|
||||
|
||||
const IconPhosphor = lazy(() =>
|
||||
import("@/registry/icons/icon-phosphor").then((mod) => ({
|
||||
default: mod.IconPhosphor,
|
||||
}))
|
||||
)
|
||||
|
||||
const IconRemixicon = lazy(() =>
|
||||
import("@/registry/icons/icon-remixicon").then((mod) => ({
|
||||
default: mod.IconRemixicon,
|
||||
}))
|
||||
)
|
||||
|
||||
export function IconPlaceholder({
|
||||
...props
|
||||
}: {
|
||||
[K in IconLibraryName]: string
|
||||
} & React.ComponentProps<"svg">) {
|
||||
const [{ iconLibrary }] = useDesignSystemSearchParams()
|
||||
const iconLibrary = useDesignSystemParam("iconLibrary")
|
||||
const iconName = props[iconLibrary]
|
||||
|
||||
if (!iconName) {
|
||||
@@ -55,12 +43,6 @@ export function IconPlaceholder({
|
||||
{iconLibrary === "hugeicons" && (
|
||||
<IconHugeicons name={iconName} {...props} />
|
||||
)}
|
||||
{iconLibrary === "phosphor" && (
|
||||
<IconPhosphor name={iconName} {...props} />
|
||||
)}
|
||||
{iconLibrary === "remixicon" && (
|
||||
<IconRemixicon name={iconName} {...props} />
|
||||
)}
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -99,7 +99,7 @@ export function ItemPicker({
|
||||
variant="outline"
|
||||
aria-label="Select item"
|
||||
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} />
|
||||
</ComboboxTrigger>
|
||||
<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"
|
||||
align="end"
|
||||
align="center"
|
||||
>
|
||||
<ComboboxInput
|
||||
showTrigger={false}
|
||||
|
||||
@@ -119,8 +119,8 @@ export function MenuColorPicker({
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="group/picker relative">
|
||||
<Picker>
|
||||
<Picker>
|
||||
<div className="group/picker relative">
|
||||
<PickerTrigger disabled={mounted && resolvedTheme === "dark"}>
|
||||
<div className="flex flex-col justify-start text-left">
|
||||
<div className="text-muted-foreground text-xs">Menu Color</div>
|
||||
@@ -128,36 +128,36 @@ export function MenuColorPicker({
|
||||
{currentMenu?.label}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-foreground pointer-events-none absolute top-1/2 right-4 flex size-4 -translate-y-1/2 items-center justify-center text-base select-none">
|
||||
<div className="text-foreground absolute top-1/2 right-4 flex size-4 -translate-y-1/2 items-center justify-center text-base">
|
||||
{currentMenu?.icon}
|
||||
</div>
|
||||
</PickerTrigger>
|
||||
<PickerContent
|
||||
anchor={isMobile ? anchorRef : undefined}
|
||||
side={isMobile ? "top" : "right"}
|
||||
align={isMobile ? "center" : "start"}
|
||||
<LockButton
|
||||
param="menuColor"
|
||||
className="absolute top-1/2 right-10 -translate-y-1/2"
|
||||
/>
|
||||
</div>
|
||||
<PickerContent
|
||||
anchor={isMobile ? anchorRef : undefined}
|
||||
side={isMobile ? "top" : "right"}
|
||||
align={isMobile ? "center" : "start"}
|
||||
>
|
||||
<PickerRadioGroup
|
||||
value={currentMenu?.value}
|
||||
onValueChange={(value) => {
|
||||
setParams({ menuColor: value as MenuColorValue })
|
||||
}}
|
||||
>
|
||||
<PickerRadioGroup
|
||||
value={currentMenu?.value}
|
||||
onValueChange={(value) => {
|
||||
setParams({ menuColor: value as MenuColorValue })
|
||||
}}
|
||||
>
|
||||
<PickerGroup>
|
||||
{MENU_OPTIONS.map((menu) => (
|
||||
<PickerRadioItem key={menu.value} value={menu.value}>
|
||||
{menu.icon}
|
||||
{menu.label}
|
||||
</PickerRadioItem>
|
||||
))}
|
||||
</PickerGroup>
|
||||
</PickerRadioGroup>
|
||||
</PickerContent>
|
||||
</Picker>
|
||||
<LockButton
|
||||
param="menuColor"
|
||||
className="absolute top-1/2 right-10 -translate-y-1/2"
|
||||
/>
|
||||
</div>
|
||||
<PickerGroup>
|
||||
{MENU_OPTIONS.map((menu) => (
|
||||
<PickerRadioItem key={menu.value} value={menu.value}>
|
||||
{menu.icon}
|
||||
{menu.label}
|
||||
</PickerRadioItem>
|
||||
))}
|
||||
</PickerGroup>
|
||||
</PickerRadioGroup>
|
||||
</PickerContent>
|
||||
</Picker>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -138,8 +138,6 @@ function PickerSubTrigger({
|
||||
lucide="ChevronRightIcon"
|
||||
tabler="IconChevronRight"
|
||||
hugeicons="ArrowRight01Icon"
|
||||
phosphor="CaretRightIcon"
|
||||
remixicon="RiArrowRightSLine"
|
||||
className="ml-auto"
|
||||
/>
|
||||
</MenuPrimitive.SubmenuTrigger>
|
||||
@@ -192,8 +190,6 @@ function PickerCheckboxItem({
|
||||
lucide="CheckIcon"
|
||||
tabler="IconCheck"
|
||||
hugeicons="Tick02Icon"
|
||||
phosphor="CheckIcon"
|
||||
remixicon="RiCheckLine"
|
||||
/>
|
||||
</MenuPrimitive.CheckboxItemIndicator>
|
||||
</span>
|
||||
@@ -234,8 +230,6 @@ function PickerRadioItem({
|
||||
lucide="CheckIcon"
|
||||
tabler="IconCheck"
|
||||
hugeicons="Tick02Icon"
|
||||
phosphor="CheckIcon"
|
||||
remixicon="RiCheckLine"
|
||||
className="size-4 pointer-coarse:size-5"
|
||||
/>
|
||||
</MenuPrimitive.RadioItemIndicator>
|
||||
|
||||
@@ -5,18 +5,18 @@ import { type ImperativePanelHandle } from "react-resizable-panels"
|
||||
|
||||
import { DARK_MODE_FORWARD_TYPE } from "@/components/mode-switcher"
|
||||
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 { RANDOMIZE_FORWARD_TYPE } from "@/app/(create)/components/random-button"
|
||||
import { sendToIframe } from "@/app/(create)/hooks/use-iframe-sync"
|
||||
import {
|
||||
serializeDesignSystemSearchParams,
|
||||
useDesignSystemSearchParams,
|
||||
} from "@/app/(create)/lib/search-params"
|
||||
import { useDesignSystemSync } from "@/app/(create)/hooks/use-design-system"
|
||||
|
||||
const MESSAGE_TYPE = "design-system-params"
|
||||
|
||||
export function Preview() {
|
||||
const [params] = useDesignSystemSearchParams()
|
||||
const params = useDesignSystemSync()
|
||||
const iframeRef = React.useRef<HTMLIFrameElement>(null)
|
||||
const resizablePanelRef = React.useRef<ImperativePanelHandle>(null)
|
||||
const [initialParams] = React.useState(params)
|
||||
const [iframeKey, setIframeKey] = React.useState(0)
|
||||
|
||||
// Sync resizable panel with URL param changes.
|
||||
React.useEffect(() => {
|
||||
@@ -32,7 +32,13 @@ export function Preview() {
|
||||
}
|
||||
|
||||
const sendParams = () => {
|
||||
sendToIframe(iframe, "design-system-params", params)
|
||||
iframe.contentWindow?.postMessage(
|
||||
{
|
||||
type: MESSAGE_TYPE,
|
||||
params,
|
||||
},
|
||||
"*"
|
||||
)
|
||||
}
|
||||
|
||||
if (iframe.contentWindow) {
|
||||
@@ -90,25 +96,18 @@ export function Preview() {
|
||||
}
|
||||
}, [])
|
||||
|
||||
const iframeSrc = React.useMemo(() => {
|
||||
// The iframe src needs to include the serialized design system params
|
||||
// for the initial load, but not be reactive to them as it would cause
|
||||
// full-iframe reloads on every param change (flashes & loss of state).
|
||||
// Further updates of the search params will be sent to the iframe
|
||||
// via a postMessage channel, for it to sync its own history onto the host's.
|
||||
return serializeDesignSystemSearchParams(
|
||||
`/preview/${params.base}/${params.item}`,
|
||||
params
|
||||
)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [params.base, params.item])
|
||||
if (!params.item || !params.base) {
|
||||
return null
|
||||
}
|
||||
|
||||
const iframeSrc = `/preview/${params.base}/${params.item}?theme=${initialParams.theme ?? "neutral"}&iconLibrary=${initialParams.iconLibrary ?? "lucide"}&style=${initialParams.style ?? "vega"}&font=${initialParams.font ?? "inter"}&baseColor=${initialParams.baseColor ?? "neutral"}`
|
||||
|
||||
return (
|
||||
<div className="relative -mx-1 flex flex-1 flex-col justify-center sm:mx-0">
|
||||
<div className="ring-foreground/15 3xl:max-h-[1200px] 3xl:max-w-[1800px] relative -z-0 mx-auto flex w-full flex-1 flex-col overflow-hidden rounded-2xl ring-1">
|
||||
<div className="bg-muted dark:bg-muted/30 absolute inset-0 rounded-2xl" />
|
||||
<iframe
|
||||
key={params.base + params.item}
|
||||
key={`${params.item}-${iframeKey}`}
|
||||
ref={iframeRef}
|
||||
src={iframeSrc}
|
||||
className="z-10 size-full flex-1"
|
||||
|
||||
@@ -27,8 +27,8 @@ export function RadiusPicker({
|
||||
const otherRadii = RADII.filter((radius) => radius.name !== "default")
|
||||
|
||||
return (
|
||||
<div className="group/picker relative">
|
||||
<Picker>
|
||||
<Picker>
|
||||
<div className="group/picker relative">
|
||||
<PickerTrigger>
|
||||
<div className="flex flex-col justify-start text-left">
|
||||
<div className="text-muted-foreground text-xs">Radius</div>
|
||||
@@ -36,7 +36,7 @@ export function RadiusPicker({
|
||||
{currentRadius?.label}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-foreground pointer-events-none absolute top-1/2 right-4 flex size-4 -translate-y-1/2 rotate-90 items-center justify-center text-base select-none">
|
||||
<div className="text-foreground absolute top-1/2 right-4 flex size-4 -translate-y-1/2 rotate-90 items-center justify-center text-base">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
@@ -55,47 +55,47 @@ export function RadiusPicker({
|
||||
</svg>
|
||||
</div>
|
||||
</PickerTrigger>
|
||||
<PickerContent
|
||||
anchor={isMobile ? anchorRef : undefined}
|
||||
side={isMobile ? "top" : "right"}
|
||||
align={isMobile ? "center" : "start"}
|
||||
<LockButton
|
||||
param="radius"
|
||||
className="absolute top-1/2 right-10 -translate-y-1/2"
|
||||
/>
|
||||
</div>
|
||||
<PickerContent
|
||||
anchor={isMobile ? anchorRef : undefined}
|
||||
side={isMobile ? "top" : "right"}
|
||||
align={isMobile ? "center" : "start"}
|
||||
>
|
||||
<PickerRadioGroup
|
||||
value={currentRadius?.name}
|
||||
onValueChange={(value) => {
|
||||
setParams({ radius: value as RadiusValue })
|
||||
}}
|
||||
>
|
||||
<PickerRadioGroup
|
||||
value={currentRadius?.name}
|
||||
onValueChange={(value) => {
|
||||
setParams({ radius: value as RadiusValue })
|
||||
}}
|
||||
>
|
||||
<PickerGroup>
|
||||
{defaultRadius && (
|
||||
<PickerRadioItem
|
||||
key={defaultRadius.name}
|
||||
value={defaultRadius.name}
|
||||
>
|
||||
<div className="flex flex-col justify-start pointer-coarse:gap-1">
|
||||
<div>{defaultRadius.label}</div>
|
||||
<div className="text-muted-foreground text-xs pointer-coarse:text-sm">
|
||||
Use radius from style
|
||||
</div>
|
||||
<PickerGroup>
|
||||
{defaultRadius && (
|
||||
<PickerRadioItem
|
||||
key={defaultRadius.name}
|
||||
value={defaultRadius.name}
|
||||
>
|
||||
<div className="flex flex-col justify-start pointer-coarse:gap-1">
|
||||
<div>{defaultRadius.label}</div>
|
||||
<div className="text-muted-foreground text-xs pointer-coarse:text-sm">
|
||||
Use radius from style
|
||||
</div>
|
||||
</PickerRadioItem>
|
||||
)}
|
||||
</PickerGroup>
|
||||
<PickerSeparator />
|
||||
<PickerGroup>
|
||||
{otherRadii.map((radius) => (
|
||||
<PickerRadioItem key={radius.name} value={radius.name}>
|
||||
{radius.label}
|
||||
</PickerRadioItem>
|
||||
))}
|
||||
</PickerGroup>
|
||||
</PickerRadioGroup>
|
||||
</PickerContent>
|
||||
</Picker>
|
||||
<LockButton
|
||||
param="radius"
|
||||
className="absolute top-1/2 right-10 -translate-y-1/2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</PickerRadioItem>
|
||||
)}
|
||||
</PickerGroup>
|
||||
<PickerSeparator />
|
||||
<PickerGroup>
|
||||
{otherRadii.map((radius) => (
|
||||
<PickerRadioItem key={radius.name} value={radius.name}>
|
||||
{radius.label}
|
||||
</PickerRadioItem>
|
||||
))}
|
||||
</PickerGroup>
|
||||
</PickerRadioGroup>
|
||||
</PickerContent>
|
||||
</Picker>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -56,7 +56,7 @@ export function ShareButton() {
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="rounded-lg shadow-none lg:w-8 xl:w-fit"
|
||||
className="rounded-lg shadow-none"
|
||||
onClick={handleCopy}
|
||||
>
|
||||
{hasCopied ? (
|
||||
@@ -64,7 +64,7 @@ export function ShareButton() {
|
||||
) : (
|
||||
<HugeiconsIcon icon={Share03Icon} strokeWidth={2} />
|
||||
)}
|
||||
<span className="lg:hidden xl:block">Share</span>
|
||||
Share
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Copy Link</TooltipContent>
|
||||
|
||||
@@ -29,8 +29,8 @@ export function StylePicker({
|
||||
const currentStyle = styles.find((style) => style.name === params.style)
|
||||
|
||||
return (
|
||||
<div className="group/picker relative">
|
||||
<Picker>
|
||||
<Picker>
|
||||
<div className="group/picker relative">
|
||||
<PickerTrigger>
|
||||
<div className="flex flex-col justify-start text-left">
|
||||
<div className="text-muted-foreground text-xs">Style</div>
|
||||
@@ -39,58 +39,58 @@ export function StylePicker({
|
||||
</div>
|
||||
</div>
|
||||
{currentStyle?.icon && (
|
||||
<div className="pointer-events-none absolute top-1/2 right-4 flex size-4 -translate-y-1/2 items-center justify-center select-none">
|
||||
<div className="absolute top-1/2 right-4 flex size-4 -translate-y-1/2 items-center justify-center">
|
||||
{React.cloneElement(currentStyle.icon, {
|
||||
className: "size-4",
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</PickerTrigger>
|
||||
<PickerContent
|
||||
anchor={isMobile ? anchorRef : undefined}
|
||||
side={isMobile ? "top" : "right"}
|
||||
align={isMobile ? "center" : "start"}
|
||||
className="md:w-64"
|
||||
<LockButton
|
||||
param="style"
|
||||
className="absolute top-1/2 right-10 -translate-y-1/2"
|
||||
/>
|
||||
</div>
|
||||
<PickerContent
|
||||
anchor={isMobile ? anchorRef : undefined}
|
||||
side={isMobile ? "top" : "right"}
|
||||
align={isMobile ? "center" : "start"}
|
||||
className="md:w-64"
|
||||
>
|
||||
<PickerRadioGroup
|
||||
value={currentStyle?.name}
|
||||
onValueChange={(value) => {
|
||||
setParams({ style: value as StyleName })
|
||||
}}
|
||||
>
|
||||
<PickerRadioGroup
|
||||
value={currentStyle?.name}
|
||||
onValueChange={(value) => {
|
||||
setParams({ style: value as StyleName })
|
||||
}}
|
||||
>
|
||||
<PickerGroup>
|
||||
{styles.map((style, index) => (
|
||||
<React.Fragment key={style.name}>
|
||||
<PickerRadioItem value={style.name}>
|
||||
<div className="flex items-start gap-2">
|
||||
{style.icon && (
|
||||
<div className="flex size-4 translate-y-0.5 items-center justify-center">
|
||||
{React.cloneElement(style.icon, {
|
||||
className: "size-4",
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col justify-start pointer-coarse:gap-1">
|
||||
<div>{style.title}</div>
|
||||
<div className="text-muted-foreground text-xs pointer-coarse:text-sm">
|
||||
{style.description}
|
||||
</div>
|
||||
<PickerGroup>
|
||||
{styles.map((style, index) => (
|
||||
<React.Fragment key={style.name}>
|
||||
<PickerRadioItem value={style.name}>
|
||||
<div className="flex items-start gap-2">
|
||||
{style.icon && (
|
||||
<div className="flex size-4 translate-y-0.5 items-center justify-center">
|
||||
{React.cloneElement(style.icon, {
|
||||
className: "size-4",
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col justify-start pointer-coarse:gap-1">
|
||||
<div>{style.title}</div>
|
||||
<div className="text-muted-foreground text-xs pointer-coarse:text-sm">
|
||||
{style.description}
|
||||
</div>
|
||||
</div>
|
||||
</PickerRadioItem>
|
||||
{index < styles.length - 1 && (
|
||||
<PickerSeparator className="opacity-50" />
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</PickerGroup>
|
||||
</PickerRadioGroup>
|
||||
</PickerContent>
|
||||
</Picker>
|
||||
<LockButton
|
||||
param="style"
|
||||
className="absolute top-1/2 right-10 -translate-y-1/2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</PickerRadioItem>
|
||||
{index < styles.length - 1 && (
|
||||
<PickerSeparator className="opacity-50" />
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</PickerGroup>
|
||||
</PickerRadioGroup>
|
||||
</PickerContent>
|
||||
</Picker>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ export function TemplatePicker({
|
||||
</div>
|
||||
{currentTemplate?.logo && (
|
||||
<div
|
||||
className="text-foreground *:[svg]:text-foreground! pointer-events-none absolute top-1/2 right-4 size-4 -translate-y-1/2 select-none *:[svg]:size-4"
|
||||
className="text-foreground *:[svg]:text-foreground! absolute top-1/2 right-4 size-4 -translate-y-1/2 *:[svg]:size-4"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: currentTemplate.logo,
|
||||
}}
|
||||
|
||||
@@ -47,8 +47,8 @@ export function ThemePicker({
|
||||
}, [currentTheme, themes, setParams])
|
||||
|
||||
return (
|
||||
<div className="group/picker relative">
|
||||
<Picker>
|
||||
<Picker>
|
||||
<div className="group/picker relative">
|
||||
<PickerTrigger>
|
||||
<div className="flex flex-col justify-start text-left">
|
||||
<div className="text-muted-foreground text-xs">Theme</div>
|
||||
@@ -68,99 +68,99 @@ export function ThemePicker({
|
||||
],
|
||||
} as React.CSSProperties
|
||||
}
|
||||
className="pointer-events-none absolute top-1/2 right-4 size-4 -translate-y-1/2 rounded-full bg-(--color) select-none"
|
||||
className="absolute top-1/2 right-4 size-4 -translate-y-1/2 rounded-full bg-(--color)"
|
||||
/>
|
||||
)}
|
||||
</PickerTrigger>
|
||||
<PickerContent
|
||||
anchor={isMobile ? anchorRef : undefined}
|
||||
side={isMobile ? "top" : "right"}
|
||||
align={isMobile ? "center" : "start"}
|
||||
className="max-h-96"
|
||||
<LockButton
|
||||
param="theme"
|
||||
className="absolute top-1/2 right-10 -translate-y-1/2"
|
||||
/>
|
||||
</div>
|
||||
<PickerContent
|
||||
anchor={isMobile ? anchorRef : undefined}
|
||||
side={isMobile ? "top" : "right"}
|
||||
align={isMobile ? "center" : "start"}
|
||||
className="max-h-96"
|
||||
>
|
||||
<PickerRadioGroup
|
||||
value={currentTheme?.name}
|
||||
onValueChange={(value) => {
|
||||
setParams({ theme: value as ThemeName })
|
||||
}}
|
||||
>
|
||||
<PickerRadioGroup
|
||||
value={currentTheme?.name}
|
||||
onValueChange={(value) => {
|
||||
setParams({ theme: value as ThemeName })
|
||||
}}
|
||||
>
|
||||
<PickerGroup>
|
||||
{themes
|
||||
.filter((theme) =>
|
||||
BASE_COLORS.find((baseColor) => baseColor.name === theme.name)
|
||||
<PickerGroup>
|
||||
{themes
|
||||
.filter((theme) =>
|
||||
BASE_COLORS.find((baseColor) => baseColor.name === theme.name)
|
||||
)
|
||||
.map((theme) => {
|
||||
const isBaseColor = BASE_COLORS.find(
|
||||
(baseColor) => baseColor.name === theme.name
|
||||
)
|
||||
.map((theme) => {
|
||||
const isBaseColor = BASE_COLORS.find(
|
||||
(baseColor) => baseColor.name === theme.name
|
||||
)
|
||||
return (
|
||||
<PickerRadioItem key={theme.name} value={theme.name}>
|
||||
<div className="flex items-start gap-2">
|
||||
{mounted && resolvedTheme && (
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"--color":
|
||||
theme.cssVars?.[
|
||||
resolvedTheme as "light" | "dark"
|
||||
]?.[
|
||||
isBaseColor ? "muted-foreground" : "primary"
|
||||
],
|
||||
} as React.CSSProperties
|
||||
}
|
||||
className="size-4 translate-y-1 rounded-full bg-(--color)"
|
||||
/>
|
||||
)}
|
||||
<div className="flex flex-col justify-start pointer-coarse:gap-1">
|
||||
<div>{theme.title}</div>
|
||||
<div className="text-muted-foreground text-xs pointer-coarse:text-sm">
|
||||
Match base color
|
||||
</div>
|
||||
return (
|
||||
<PickerRadioItem key={theme.name} value={theme.name}>
|
||||
<div className="flex items-start gap-2">
|
||||
{mounted && resolvedTheme && (
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"--color":
|
||||
theme.cssVars?.[
|
||||
resolvedTheme as "light" | "dark"
|
||||
]?.[
|
||||
isBaseColor ? "muted-foreground" : "primary"
|
||||
],
|
||||
} as React.CSSProperties
|
||||
}
|
||||
className="size-4 translate-y-1 rounded-full bg-(--color)"
|
||||
/>
|
||||
)}
|
||||
<div className="flex flex-col justify-start pointer-coarse:gap-1">
|
||||
<div>{theme.title}</div>
|
||||
<div className="text-muted-foreground text-xs pointer-coarse:text-sm">
|
||||
Match base color
|
||||
</div>
|
||||
</div>
|
||||
</PickerRadioItem>
|
||||
)
|
||||
})}
|
||||
</PickerGroup>
|
||||
<PickerSeparator />
|
||||
<PickerGroup>
|
||||
{themes
|
||||
.filter(
|
||||
(theme) =>
|
||||
!BASE_COLORS.find(
|
||||
(baseColor) => baseColor.name === theme.name
|
||||
)
|
||||
</div>
|
||||
</PickerRadioItem>
|
||||
)
|
||||
.map((theme) => {
|
||||
return (
|
||||
<PickerRadioItem key={theme.name} value={theme.name}>
|
||||
<div className="flex items-center gap-2">
|
||||
{mounted && resolvedTheme && (
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"--color":
|
||||
theme.cssVars?.[
|
||||
resolvedTheme as "light" | "dark"
|
||||
]?.["primary"],
|
||||
} as React.CSSProperties
|
||||
}
|
||||
className="size-4 rounded-full bg-(--color)"
|
||||
/>
|
||||
)}
|
||||
{theme.title}
|
||||
</div>
|
||||
</PickerRadioItem>
|
||||
})}
|
||||
</PickerGroup>
|
||||
<PickerSeparator />
|
||||
<PickerGroup>
|
||||
{themes
|
||||
.filter(
|
||||
(theme) =>
|
||||
!BASE_COLORS.find(
|
||||
(baseColor) => baseColor.name === theme.name
|
||||
)
|
||||
})}
|
||||
</PickerGroup>
|
||||
</PickerRadioGroup>
|
||||
</PickerContent>
|
||||
</Picker>
|
||||
<LockButton
|
||||
param="theme"
|
||||
className="absolute top-1/2 right-10 -translate-y-1/2"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
.map((theme) => {
|
||||
return (
|
||||
<PickerRadioItem key={theme.name} value={theme.name}>
|
||||
<div className="flex items-center gap-2">
|
||||
{mounted && resolvedTheme && (
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"--color":
|
||||
theme.cssVars?.[
|
||||
resolvedTheme as "light" | "dark"
|
||||
]?.["primary"],
|
||||
} as React.CSSProperties
|
||||
}
|
||||
className="size-4 rounded-full bg-(--color)"
|
||||
/>
|
||||
)}
|
||||
{theme.title}
|
||||
</div>
|
||||
</PickerRadioItem>
|
||||
)
|
||||
})}
|
||||
</PickerGroup>
|
||||
</PickerRadioGroup>
|
||||
</PickerContent>
|
||||
</Picker>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -14,12 +14,14 @@ import {
|
||||
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
||||
|
||||
export function V0Button({ className }: { className?: string }) {
|
||||
const [params] = useDesignSystemSearchParams()
|
||||
const [params, setParams] = useDesignSystemSearchParams()
|
||||
const isMobile = useIsMobile()
|
||||
const isMounted = useMounted()
|
||||
|
||||
const url = `${process.env.NEXT_PUBLIC_APP_URL}/create/v0?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}&item=${params.item}`
|
||||
|
||||
console.log(url)
|
||||
|
||||
if (!isMounted) {
|
||||
return <Skeleton className="h-8 w-24 rounded-lg" />
|
||||
}
|
||||
@@ -32,7 +34,7 @@ export function V0Button({ className }: { className?: string }) {
|
||||
size="sm"
|
||||
variant={isMobile ? "default" : "outline"}
|
||||
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
|
||||
)}
|
||||
asChild
|
||||
@@ -41,8 +43,7 @@ export function V0Button({ className }: { className?: string }) {
|
||||
href={`${process.env.NEXT_PUBLIC_V0_URL}/chat/api/open?url=${encodeURIComponent(url)}&title=${params.item}`}
|
||||
target="_blank"
|
||||
>
|
||||
<span className="lg:hidden xl:block">Open in</span>
|
||||
<Icons.v0 className="size-5" />
|
||||
Open in <Icons.v0 className="size-5" />
|
||||
</a>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
|
||||
@@ -4,11 +4,7 @@ import { ArrowLeftIcon } from "lucide-react"
|
||||
import type { SearchParams } from "nuqs/server"
|
||||
|
||||
import { siteConfig } from "@/lib/config"
|
||||
import { source } from "@/lib/source"
|
||||
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 { SiteConfig } from "@/components/site-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 { SidebarProvider } from "@/registry/new-york-v4/ui/sidebar"
|
||||
import { Customizer } from "@/app/(create)/components/customizer"
|
||||
import { CustomizerControls } from "@/app/(create)/components/customizer-controls"
|
||||
import { ItemExplorer } from "@/app/(create)/components/item-explorer"
|
||||
import { ItemPicker } from "@/app/(create)/components/item-picker"
|
||||
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 { ToolbarControls } from "@/app/(create)/components/toolbar-controls"
|
||||
import { V0Button } from "@/app/(create)/components/v0-button"
|
||||
@@ -68,7 +63,6 @@ export default async function CreatePage({
|
||||
const params = await loadDesignSystemSearchParams(searchParams)
|
||||
const base = BASES.find((b) => b.name === params.base) ?? BASES[0]
|
||||
|
||||
const pageTree = source.pageTree
|
||||
const items = await getItemsForBase(base.name)
|
||||
|
||||
const filteredItems = items
|
||||
@@ -87,34 +81,35 @@ export default async function CreatePage({
|
||||
<header className="sticky top-0 z-50 w-full">
|
||||
<div className="container-wrapper 3xl:fixed:px-0 px-6">
|
||||
<div className="3xl:fixed:container flex h-(--header-height) items-center **:data-[slot=separator]:!h-4">
|
||||
<div className="3xl:fixed:container flex h-(--header-height) items-center **:data-[slot=separator]:!h-4">
|
||||
<MobileNav
|
||||
tree={pageTree}
|
||||
items={siteConfig.navItems}
|
||||
className="flex lg:hidden"
|
||||
/>
|
||||
<div className="flex items-center xl:w-1/3">
|
||||
<Button
|
||||
asChild
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="hidden size-8 lg:flex"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="rounded-lg shadow-none"
|
||||
>
|
||||
<Link href="/">
|
||||
<Icons.logo className="size-5" />
|
||||
<span className="sr-only">{siteConfig.name}</span>
|
||||
<ArrowLeftIcon />
|
||||
Back
|
||||
</Link>
|
||||
</Button>
|
||||
<MainNav items={siteConfig.navItems} className="hidden lg:flex" />
|
||||
</div>
|
||||
<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">
|
||||
<ItemPicker items={filteredItems} />
|
||||
<div className="items-center gap-0 sm:hidden">
|
||||
<RandomButton />
|
||||
<ResetButton />
|
||||
<Separator
|
||||
orientation="vertical"
|
||||
className="mx-2 hidden sm:mx-4 lg:flex"
|
||||
/>
|
||||
<div className="text-muted-foreground hidden text-sm font-medium lg:flex">
|
||||
New Project
|
||||
</div>
|
||||
<Separator orientation="vertical" className="mr-2 flex" />
|
||||
</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" />
|
||||
<Separator orientation="vertical" className="3xl:flex hidden" />
|
||||
<ModeSwitcher />
|
||||
|
||||
@@ -45,6 +45,18 @@ export async function GET(request: NextRequest) {
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
@@ -63,23 +75,28 @@ export async function GET(request: NextRequest) {
|
||||
}
|
||||
|
||||
async function buildV0Payload(designSystemConfig: DesignSystemConfig) {
|
||||
const registryBase = buildRegistryBase(designSystemConfig)
|
||||
const files: z.infer<typeof registryItemFileSchema>[] = []
|
||||
|
||||
// Build all files in parallel.
|
||||
const [globalsCss, layoutFile, componentFiles] = await Promise.all([
|
||||
buildGlobalsCss(registryBase),
|
||||
buildLayoutFile(designSystemConfig),
|
||||
buildComponentFiles(designSystemConfig),
|
||||
])
|
||||
// Build globals.css file.
|
||||
files.push(buildGlobalsCss(designSystemConfig))
|
||||
|
||||
// Build layout.tsx file.
|
||||
files.push(buildLayoutFile(designSystemConfig))
|
||||
|
||||
// Build component files.
|
||||
const componentFiles = await buildComponentFiles(designSystemConfig)
|
||||
files.push(...componentFiles)
|
||||
|
||||
return registryItemSchema.parse({
|
||||
name: designSystemConfig.item ?? "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 ?? {})
|
||||
.map(([key, value]) => ` --${key}: ${value};`)
|
||||
.join("\n")
|
||||
|
||||
41
apps/v4/app/(create)/hooks/use-canva.tsx
Normal file
41
apps/v4/app/(create)/hooks/use-canva.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import {
|
||||
sendToIframe,
|
||||
sendToParent,
|
||||
useParentMessageListener,
|
||||
} from "@/app/(create)/hooks/use-iframe-sync"
|
||||
|
||||
const MESSAGE_TYPE = "canva-zoom"
|
||||
|
||||
export type ZoomCommand =
|
||||
| { type: "ZOOM_IN" }
|
||||
| { type: "ZOOM_OUT" }
|
||||
| { type: "ZOOM_SET"; value: number }
|
||||
| { type: "ZOOM_FIT" }
|
||||
| { type: "RESET" }
|
||||
|
||||
export function sendCanvaZoomCommand(
|
||||
iframe: HTMLIFrameElement | null,
|
||||
command: ZoomCommand
|
||||
) {
|
||||
sendToIframe(iframe, MESSAGE_TYPE, { command })
|
||||
}
|
||||
|
||||
export function sendCanvaZoomUpdate(zoom: number) {
|
||||
sendToParent(MESSAGE_TYPE, { zoom })
|
||||
}
|
||||
|
||||
export function useCanvaZoomSync() {
|
||||
const [zoom, setZoom] = React.useState(1)
|
||||
|
||||
useParentMessageListener<{ zoom: number }>(MESSAGE_TYPE, (data) => {
|
||||
if (typeof data.zoom === "number") {
|
||||
setZoom(data.zoom)
|
||||
}
|
||||
})
|
||||
|
||||
return zoom
|
||||
}
|
||||
44
apps/v4/app/(create)/hooks/use-design-system.tsx
Normal file
44
apps/v4/app/(create)/hooks/use-design-system.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
"use client"
|
||||
|
||||
import {
|
||||
createIframeSyncStore,
|
||||
useIframeSyncAll,
|
||||
useIframeSyncValue,
|
||||
} from "@/app/(create)/hooks/use-iframe-sync"
|
||||
import {
|
||||
loadDesignSystemSearchParams,
|
||||
useDesignSystemSearchParams,
|
||||
type DesignSystemSearchParams,
|
||||
} from "@/app/(create)/lib/search-params"
|
||||
|
||||
const MESSAGE_TYPE = "design-system-params"
|
||||
|
||||
const getInitialValues = (): DesignSystemSearchParams => {
|
||||
const initialValues = loadDesignSystemSearchParams(
|
||||
typeof window === "undefined" ? "" : location.search
|
||||
)
|
||||
// Override defaults for iframe use case
|
||||
initialValues.item = "cover-example"
|
||||
return initialValues
|
||||
}
|
||||
|
||||
const designSystemStore = createIframeSyncStore(
|
||||
MESSAGE_TYPE,
|
||||
getInitialValues()
|
||||
)
|
||||
|
||||
export function useDesignSystemSync() {
|
||||
const [urlParams] = useDesignSystemSearchParams()
|
||||
|
||||
const keys = Object.keys(urlParams) as (keyof DesignSystemSearchParams)[]
|
||||
|
||||
return useIframeSyncAll(designSystemStore, keys, urlParams)
|
||||
}
|
||||
|
||||
export function useDesignSystemParam<K extends keyof DesignSystemSearchParams>(
|
||||
key: K
|
||||
) {
|
||||
const [urlParams] = useDesignSystemSearchParams()
|
||||
|
||||
return useIframeSyncValue(designSystemStore, key, urlParams[key])
|
||||
}
|
||||
@@ -1,14 +1,8 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import type { DesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
||||
|
||||
type ParentToIframeMessage = {
|
||||
type: "design-system-params"
|
||||
data: DesignSystemSearchParams
|
||||
}
|
||||
|
||||
export const isInIframe = () => {
|
||||
if (typeof window === "undefined") {
|
||||
return false
|
||||
@@ -16,21 +10,157 @@ export const isInIframe = () => {
|
||||
return window.self !== window.top
|
||||
}
|
||||
|
||||
export function useIframeMessageListener<
|
||||
Message extends ParentToIframeMessage,
|
||||
MessageType extends Message["type"],
|
||||
>(
|
||||
messageType: MessageType,
|
||||
onMessage: (data: Extract<Message, { type: MessageType }>["data"]) => void
|
||||
export function createIframeSyncStore<T extends Record<string, any>>(
|
||||
messageType: string,
|
||||
defaultValues: T
|
||||
) {
|
||||
const store = new Map<keyof T, any>()
|
||||
const listeners = new Map<keyof T, Set<() => void>>()
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
Object.entries(defaultValues).forEach(([key, value]) => {
|
||||
store.set(key as keyof T, value)
|
||||
})
|
||||
}
|
||||
|
||||
if (typeof window !== "undefined" && isInIframe()) {
|
||||
window.addEventListener("message", (event: MessageEvent) => {
|
||||
if (event.data.type === messageType && event.data.params) {
|
||||
Object.keys(event.data.params).forEach((key) => {
|
||||
const newValue = event.data.params[key]
|
||||
const oldValue = store.get(key as keyof T)
|
||||
|
||||
if (newValue !== oldValue) {
|
||||
store.set(key as keyof T, newValue)
|
||||
|
||||
const keyListeners = listeners.get(key as keyof T)
|
||||
if (keyListeners) {
|
||||
keyListeners.forEach((listener) => listener())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
store,
|
||||
listeners,
|
||||
subscribe(key: keyof T, callback: () => void) {
|
||||
if (!isInIframe()) return () => {}
|
||||
|
||||
if (!listeners.has(key)) {
|
||||
listeners.set(key, new Set())
|
||||
}
|
||||
const keyListeners = listeners.get(key)!
|
||||
keyListeners.add(callback)
|
||||
|
||||
return () => {
|
||||
keyListeners.delete(callback)
|
||||
if (keyListeners.size === 0) {
|
||||
listeners.delete(key)
|
||||
}
|
||||
}
|
||||
},
|
||||
subscribeAll(keys: (keyof T)[], callback: () => void) {
|
||||
if (!isInIframe()) return () => {}
|
||||
|
||||
keys.forEach((key) => {
|
||||
if (!listeners.has(key)) {
|
||||
listeners.set(key, new Set())
|
||||
}
|
||||
listeners.get(key)!.add(callback)
|
||||
})
|
||||
|
||||
return () => {
|
||||
keys.forEach((key) => {
|
||||
const keyListeners = listeners.get(key)
|
||||
if (keyListeners) {
|
||||
keyListeners.delete(callback)
|
||||
if (keyListeners.size === 0) {
|
||||
listeners.delete(key)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
get(key: keyof T) {
|
||||
return store.get(key) as T[keyof T]
|
||||
},
|
||||
getAll() {
|
||||
const result = {} as T
|
||||
store.forEach((value, key) => {
|
||||
result[key as keyof T] = value
|
||||
})
|
||||
return result
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function useIframeSyncValue<T>(
|
||||
store: ReturnType<typeof createIframeSyncStore<any>>,
|
||||
key: string,
|
||||
urlValue: T
|
||||
) {
|
||||
const subscribe = React.useCallback(
|
||||
(callback: () => void) => {
|
||||
return store.subscribe(key, callback)
|
||||
},
|
||||
[store, key]
|
||||
)
|
||||
|
||||
const getSnapshot = React.useCallback(() => {
|
||||
if (!isInIframe()) {
|
||||
return urlValue
|
||||
}
|
||||
return store.get(key) as T
|
||||
}, [store, key, urlValue])
|
||||
|
||||
const getServerSnapshot = React.useCallback(() => {
|
||||
return urlValue
|
||||
}, [urlValue])
|
||||
|
||||
return React.useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)
|
||||
}
|
||||
|
||||
export function useIframeSyncAll<T extends Record<string, any>>(
|
||||
store: ReturnType<typeof createIframeSyncStore<T>>,
|
||||
keys: (keyof T)[],
|
||||
urlValues: T
|
||||
) {
|
||||
const subscribe = React.useCallback(
|
||||
(callback: () => void) => {
|
||||
return store.subscribeAll(keys, callback)
|
||||
},
|
||||
[store, keys]
|
||||
)
|
||||
|
||||
const getSnapshot = React.useCallback(() => {
|
||||
if (!isInIframe()) {
|
||||
return urlValues
|
||||
}
|
||||
return store.getAll()
|
||||
}, [store, urlValues])
|
||||
|
||||
const getServerSnapshot = React.useCallback(() => {
|
||||
return urlValues
|
||||
}, [urlValues])
|
||||
|
||||
return React.useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)
|
||||
}
|
||||
|
||||
export function useParentMessageListener<T>(
|
||||
messageType: string,
|
||||
onMessage: (data: T) => void
|
||||
) {
|
||||
React.useEffect(() => {
|
||||
if (!isInIframe()) {
|
||||
if (isInIframe()) {
|
||||
return
|
||||
}
|
||||
|
||||
const handleMessage = (event: MessageEvent) => {
|
||||
if (event.data.type === messageType) {
|
||||
onMessage(event.data.data)
|
||||
onMessage(event.data)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,22 +171,33 @@ export function useIframeMessageListener<
|
||||
}, [messageType, onMessage])
|
||||
}
|
||||
|
||||
export function sendToIframe<
|
||||
Message extends ParentToIframeMessage,
|
||||
MessageType extends Message["type"],
|
||||
>(
|
||||
export function sendToIframe(
|
||||
iframe: HTMLIFrameElement | null,
|
||||
messageType: MessageType,
|
||||
data: Extract<Message, { type: MessageType }>["data"]
|
||||
messageType: string,
|
||||
data: any
|
||||
) {
|
||||
if (!iframe?.contentWindow) {
|
||||
if (!iframe || !iframe.contentWindow) {
|
||||
return
|
||||
}
|
||||
|
||||
iframe.contentWindow.postMessage(
|
||||
{
|
||||
type: messageType,
|
||||
data,
|
||||
...data,
|
||||
},
|
||||
"*"
|
||||
)
|
||||
}
|
||||
|
||||
export function sendToParent(messageType: string, data: any) {
|
||||
if (!isInIframe()) {
|
||||
return
|
||||
}
|
||||
|
||||
window.parent.postMessage(
|
||||
{
|
||||
type: messageType,
|
||||
...data,
|
||||
},
|
||||
"*"
|
||||
)
|
||||
|
||||
@@ -11,7 +11,6 @@ export type LockableParam =
|
||||
| "menuAccent"
|
||||
| "menuColor"
|
||||
| "radius"
|
||||
| "direction"
|
||||
|
||||
type LocksContextValue = {
|
||||
locks: Set<LockableParam>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useQueryStates } from "nuqs"
|
||||
import {
|
||||
createLoader,
|
||||
createSerializer,
|
||||
parseAsBoolean,
|
||||
parseAsInteger,
|
||||
parseAsString,
|
||||
@@ -14,7 +13,6 @@ import {
|
||||
BASE_COLORS,
|
||||
BASES,
|
||||
DEFAULT_CONFIG,
|
||||
DIRECTIONS,
|
||||
iconLibraries,
|
||||
MENU_ACCENTS,
|
||||
MENU_COLORS,
|
||||
@@ -23,7 +21,6 @@ import {
|
||||
THEMES,
|
||||
type BaseColorName,
|
||||
type BaseName,
|
||||
type DirectionValue,
|
||||
type FontValue,
|
||||
type IconLibraryName,
|
||||
type MenuAccentValue,
|
||||
@@ -38,7 +35,7 @@ const designSystemSearchParams = {
|
||||
base: parseAsStringLiteral<BaseName>(BASES.map((b) => b.name)).withDefault(
|
||||
DEFAULT_CONFIG.base
|
||||
),
|
||||
item: parseAsString.withDefault("preview").withOptions({ shallow: true }),
|
||||
item: parseAsString.withDefault("preview"),
|
||||
iconLibrary: parseAsStringLiteral<IconLibraryName>(
|
||||
Object.values(iconLibraries).map((i) => i.name)
|
||||
).withDefault(DEFAULT_CONFIG.iconLibrary),
|
||||
@@ -54,9 +51,6 @@ const designSystemSearchParams = {
|
||||
baseColor: parseAsStringLiteral<BaseColorName>(
|
||||
BASE_COLORS.map((b) => b.name)
|
||||
).withDefault(DEFAULT_CONFIG.baseColor),
|
||||
direction: parseAsStringLiteral<DirectionValue>(
|
||||
DIRECTIONS.map((d) => d.value)
|
||||
).withDefault(DEFAULT_CONFIG.direction),
|
||||
menuAccent: parseAsStringLiteral<MenuAccentValue>(
|
||||
MENU_ACCENTS.map((a) => a.value)
|
||||
).withDefault(DEFAULT_CONFIG.menuAccent),
|
||||
@@ -79,10 +73,6 @@ export const loadDesignSystemSearchParams = createLoader(
|
||||
designSystemSearchParams
|
||||
)
|
||||
|
||||
export const serializeDesignSystemSearchParams = createSerializer(
|
||||
designSystemSearchParams
|
||||
)
|
||||
|
||||
export const useDesignSystemSearchParams = (options: Options = {}) =>
|
||||
useQueryStates(designSystemSearchParams, {
|
||||
shallow: false,
|
||||
|
||||
@@ -7,10 +7,10 @@ import { absoluteUrl } from "@/lib/utils"
|
||||
import { DarkModeScript } from "@/components/mode-switcher"
|
||||
import { TailwindIndicator } from "@/components/tailwind-indicator"
|
||||
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 { ItemPickerScript } from "@/app/(create)/components/item-picker"
|
||||
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 { ALLOWED_ITEM_TYPES } from "@/app/(create)/lib/constants"
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { TrashIcon } from "lucide-react"
|
||||
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
@@ -8,7 +6,6 @@ import {
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogMedia,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from "@/registry/new-york-v4/ui/alert-dialog"
|
||||
@@ -16,66 +13,23 @@ import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
|
||||
export function AlertDialogDemo() {
|
||||
return (
|
||||
<div className="flex flex-wrap gap-4">
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="outline">Default</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This action cannot be undone. This will permanently delete your
|
||||
account and remove your data from our servers.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction>Continue</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</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>
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="outline">Show Dialog</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This action cannot be undone. This will permanently delete your
|
||||
account and remove your data from our servers.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction>Continue</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,173 +1,89 @@
|
||||
import { PlusIcon } from "lucide-react"
|
||||
|
||||
import {
|
||||
Avatar,
|
||||
AvatarBadge,
|
||||
AvatarFallback,
|
||||
AvatarGroup,
|
||||
AvatarGroupCount,
|
||||
AvatarImage,
|
||||
} from "@/registry/new-york-v4/ui/avatar"
|
||||
|
||||
export function AvatarDemo() {
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
{/* Sizes. */}
|
||||
<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>
|
||||
</Avatar>
|
||||
<div className="flex flex-row flex-wrap items-center gap-4">
|
||||
<Avatar>
|
||||
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
|
||||
<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>
|
||||
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
|
||||
<AvatarFallback>CN</AvatarFallback>
|
||||
</Avatar>
|
||||
<Avatar size="lg">
|
||||
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
|
||||
<AvatarFallback>CN</AvatarFallback>
|
||||
</Avatar>
|
||||
</div>
|
||||
{/* Fallback. */}
|
||||
<div className="flex flex-row flex-wrap items-center gap-4">
|
||||
<Avatar size="sm">
|
||||
<AvatarFallback>CN</AvatarFallback>
|
||||
<Avatar>
|
||||
<AvatarImage
|
||||
src="https://github.com/maxleiter.png"
|
||||
alt="@maxleiter"
|
||||
/>
|
||||
<AvatarFallback>LR</AvatarFallback>
|
||||
</Avatar>
|
||||
<Avatar>
|
||||
<AvatarFallback>CN</AvatarFallback>
|
||||
</Avatar>
|
||||
<Avatar size="lg">
|
||||
<AvatarFallback>CN</AvatarFallback>
|
||||
<AvatarImage
|
||||
src="https://github.com/evilrabbit.png"
|
||||
alt="@evilrabbit"
|
||||
/>
|
||||
<AvatarFallback>ER</AvatarFallback>
|
||||
</Avatar>
|
||||
</div>
|
||||
{/* With badge. */}
|
||||
<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>
|
||||
<div className="*:data-[slot=avatar]:ring-background flex -space-x-2 *:data-[slot=avatar]:size-12 *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:grayscale">
|
||||
<Avatar>
|
||||
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
|
||||
<AvatarFallback>CN</AvatarFallback>
|
||||
<AvatarBadge />
|
||||
</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" />
|
||||
<AvatarFallback>CN</AvatarFallback>
|
||||
<AvatarBadge>
|
||||
<PlusIcon />
|
||||
</AvatarBadge>
|
||||
</Avatar>
|
||||
</div>
|
||||
{/* Avatar group. */}
|
||||
<div className="flex flex-row flex-wrap items-center gap-4">
|
||||
<AvatarGroup>
|
||||
<Avatar size="sm">
|
||||
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
|
||||
<AvatarFallback>CN</AvatarFallback>
|
||||
</Avatar>
|
||||
<Avatar size="sm">
|
||||
<AvatarImage
|
||||
src="https://github.com/maxleiter.png"
|
||||
alt="@maxleiter"
|
||||
/>
|
||||
<AvatarFallback>ML</AvatarFallback>
|
||||
</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>
|
||||
<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>
|
||||
)
|
||||
|
||||
@@ -10,10 +10,6 @@ export function BadgeDemo() {
|
||||
<Badge variant="secondary">Secondary</Badge>
|
||||
<Badge variant="destructive">Destructive</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">
|
||||
<CheckIcon />
|
||||
Badge
|
||||
@@ -59,16 +55,6 @@ export function BadgeDemo() {
|
||||
Link <ArrowRightIcon />
|
||||
</a>
|
||||
</Badge>
|
||||
<Badge asChild variant="ghost">
|
||||
<a href="#">
|
||||
Link <ArrowRightIcon />
|
||||
</a>
|
||||
</Badge>
|
||||
<Badge asChild variant="link">
|
||||
<a href="#">
|
||||
Link <ArrowRightIcon />
|
||||
</a>
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -6,25 +6,22 @@ export function ButtonDemo() {
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex flex-wrap items-center gap-2 md:flex-row">
|
||||
<Button size="xs">Extra Small</Button>
|
||||
<Button variant="outline" size="xs">
|
||||
Outline
|
||||
</Button>
|
||||
<Button variant="ghost" size="xs">
|
||||
Ghost
|
||||
</Button>
|
||||
<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">
|
||||
<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 />
|
||||
</Button>
|
||||
<Button disabled variant="outline">
|
||||
<Loader2Icon className="animate-spin" />
|
||||
Please wait
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2 md:flex-row">
|
||||
<Button size="sm">Small</Button>
|
||||
@@ -46,21 +43,10 @@ export function ButtonDemo() {
|
||||
<Button variant="outline" size="sm">
|
||||
<SendIcon /> Send
|
||||
</Button>
|
||||
</div>
|
||||
<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">
|
||||
<Button variant="outline" size="sm">
|
||||
Learn More <ArrowRightIcon />
|
||||
</Button>
|
||||
<Button disabled variant="outline">
|
||||
<Button disabled size="sm" variant="outline">
|
||||
<Loader2Icon className="animate-spin" />
|
||||
Please wait
|
||||
</Button>
|
||||
@@ -85,19 +71,12 @@ export function ButtonDemo() {
|
||||
<Button variant="outline" size="lg">
|
||||
<SendIcon /> Send
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2 md:flex-row">
|
||||
<Button size="icon-xs" variant="outline">
|
||||
<PlusIcon />
|
||||
<Button variant="outline" size="lg">
|
||||
Learn More <ArrowRightIcon />
|
||||
</Button>
|
||||
<Button size="icon-sm" variant="outline">
|
||||
<PlusIcon />
|
||||
</Button>
|
||||
<Button size="icon" variant="outline">
|
||||
<PlusIcon />
|
||||
</Button>
|
||||
<Button size="icon-lg" variant="outline">
|
||||
<PlusIcon />
|
||||
<Button disabled size="lg" variant="outline">
|
||||
<Loader2Icon className="animate-spin" />
|
||||
Please wait
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,183 +1,405 @@
|
||||
"use client"
|
||||
|
||||
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 {
|
||||
Combobox,
|
||||
ComboboxChip,
|
||||
ComboboxChips,
|
||||
ComboboxChipsInput,
|
||||
ComboboxCollection,
|
||||
ComboboxContent,
|
||||
ComboboxEmpty,
|
||||
ComboboxGroup,
|
||||
ComboboxInput,
|
||||
ComboboxItem,
|
||||
ComboboxLabel,
|
||||
ComboboxList,
|
||||
ComboboxTrigger,
|
||||
ComboboxValue,
|
||||
useComboboxAnchor,
|
||||
} from "@/registry/new-york-v4/ui/combobox"
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
CommandSeparator,
|
||||
} from "@/registry/new-york-v4/ui/command"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/registry/new-york-v4/ui/popover"
|
||||
|
||||
const frameworks = [
|
||||
"Next.js",
|
||||
"SvelteKit",
|
||||
"Nuxt.js",
|
||||
"Remix",
|
||||
"Astro",
|
||||
{
|
||||
value: "next.js",
|
||||
label: "Next.js",
|
||||
},
|
||||
{
|
||||
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
|
||||
|
||||
type User = (typeof users)[number]
|
||||
|
||||
const timezones = [
|
||||
{
|
||||
value: "Americas",
|
||||
items: ["(GMT-5) New York", "(GMT-8) Los Angeles", "(GMT-6) Chicago"],
|
||||
label: "Americas",
|
||||
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",
|
||||
items: ["(GMT+0) London", "(GMT+1) Paris", "(GMT+1) Berlin"],
|
||||
label: "Europe",
|
||||
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",
|
||||
items: ["(GMT+9) Tokyo", "(GMT+8) Shanghai", "(GMT+8) Singapore"],
|
||||
label: "Asia/Pacific",
|
||||
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
|
||||
|
||||
const countries = [
|
||||
{ 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" },
|
||||
]
|
||||
type Timezone = (typeof timezones)[number]
|
||||
|
||||
export function ComboboxDemo() {
|
||||
return (
|
||||
<div className="flex w-full flex-col gap-6">
|
||||
{/* Basic combobox. */}
|
||||
<div className="flex flex-wrap items-start gap-4">
|
||||
<Combobox items={frameworks}>
|
||||
<ComboboxInput placeholder="Select a framework" />
|
||||
<ComboboxContent>
|
||||
<ComboboxEmpty>No items found.</ComboboxEmpty>
|
||||
<ComboboxList>
|
||||
{(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 className="flex w-full flex-wrap items-start gap-4">
|
||||
<FrameworkCombobox frameworks={[...frameworks]} />
|
||||
<UserCombobox users={[...users]} selectedUserId={users[0].id} />
|
||||
<TimezoneCombobox
|
||||
timezones={[...timezones]}
|
||||
selectedTimezone={timezones[0].timezones[0]}
|
||||
/>
|
||||
<ComboboxWithCheckbox frameworks={[...frameworks]} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ComboboxMultiple() {
|
||||
const anchor = useComboboxAnchor()
|
||||
function FrameworkCombobox({ frameworks }: { frameworks: Framework[] }) {
|
||||
const [open, setOpen] = React.useState(false)
|
||||
const [value, setValue] = React.useState("")
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap items-start gap-4">
|
||||
<Combobox
|
||||
multiple
|
||||
autoHighlight
|
||||
items={frameworks}
|
||||
defaultValue={[frameworks[0]]}
|
||||
>
|
||||
<ComboboxChips ref={anchor}>
|
||||
<ComboboxValue>
|
||||
{(values) => (
|
||||
<React.Fragment>
|
||||
{values.map((value: string) => (
|
||||
<ComboboxChip key={value}>{value}</ComboboxChip>
|
||||
))}
|
||||
<ComboboxChipsInput placeholder="Add framework..." />
|
||||
</React.Fragment>
|
||||
)}
|
||||
</ComboboxValue>
|
||||
</ComboboxChips>
|
||||
<ComboboxContent anchor={anchor}>
|
||||
<ComboboxEmpty>No items found.</ComboboxEmpty>
|
||||
<ComboboxList>
|
||||
{(item) => (
|
||||
<ComboboxItem key={item} value={item}>
|
||||
{item}
|
||||
</ComboboxItem>
|
||||
)}
|
||||
</ComboboxList>
|
||||
</ComboboxContent>
|
||||
</Combobox>
|
||||
</div>
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={open}
|
||||
className="w-full justify-between md:max-w-[200px]"
|
||||
>
|
||||
{value
|
||||
? frameworks.find((framework) => framework.value === value)?.label
|
||||
: "Select framework..."}
|
||||
<ChevronsUpDown className="text-muted-foreground" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-(--radix-popover-trigger-width) p-0">
|
||||
<Command>
|
||||
<CommandInput placeholder="Search framework..." />
|
||||
<CommandList>
|
||||
<CommandEmpty>No framework found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{frameworks.map((framework) => (
|
||||
<CommandItem
|
||||
key={framework.value}
|
||||
value={framework.value}
|
||||
onSelect={(currentValue) => {
|
||||
setValue(currentValue === value ? "" : currentValue)
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
{framework.label}
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -117,8 +117,6 @@ function DropdownMenuCheckboxes() {
|
||||
lucide="UserIcon"
|
||||
tabler="IconUser"
|
||||
hugeicons="UserIcon"
|
||||
phosphor="UserIcon"
|
||||
remixicon="RiUserLine"
|
||||
/>
|
||||
Profile
|
||||
</DropdownMenuItem>
|
||||
@@ -127,8 +125,6 @@ function DropdownMenuCheckboxes() {
|
||||
lucide="CreditCardIcon"
|
||||
tabler="IconCreditCard"
|
||||
hugeicons="CreditCardIcon"
|
||||
phosphor="CreditCardIcon"
|
||||
remixicon="RiBankCardLine"
|
||||
/>
|
||||
Billing
|
||||
</DropdownMenuItem>
|
||||
@@ -137,8 +133,6 @@ function DropdownMenuCheckboxes() {
|
||||
lucide="SettingsIcon"
|
||||
tabler="IconSettings"
|
||||
hugeicons="SettingsIcon"
|
||||
phosphor="GearIcon"
|
||||
remixicon="RiSettings3Line"
|
||||
/>
|
||||
Settings
|
||||
</DropdownMenuItem>
|
||||
@@ -173,8 +167,6 @@ function DropdownMenuCheckboxes() {
|
||||
lucide="LogOutIcon"
|
||||
tabler="IconLogout"
|
||||
hugeicons="LogoutIcon"
|
||||
phosphor="SignOutIcon"
|
||||
remixicon="RiLogoutBoxLine"
|
||||
/>
|
||||
Sign Out
|
||||
</DropdownMenuItem>
|
||||
@@ -230,8 +222,6 @@ function DropdownMenuWithAvatar() {
|
||||
lucide="ChevronsUpDownIcon"
|
||||
tabler="IconChevronsUpDown"
|
||||
hugeicons="ChevronUpDownIcon"
|
||||
phosphor="CaretUpDownIcon"
|
||||
remixicon="RiExpandUpDownLine"
|
||||
className="text-muted-foreground ml-auto"
|
||||
/>
|
||||
</Button>
|
||||
@@ -261,8 +251,6 @@ function DropdownMenuWithAvatar() {
|
||||
lucide="SparklesIcon"
|
||||
tabler="IconSparkles"
|
||||
hugeicons="SparklesIcon"
|
||||
phosphor="SparklesIcon"
|
||||
remixicon="RiSparklingLine"
|
||||
/>
|
||||
Upgrade to Pro
|
||||
</DropdownMenuItem>
|
||||
@@ -274,8 +262,6 @@ function DropdownMenuWithAvatar() {
|
||||
lucide="BadgeCheckIcon"
|
||||
tabler="IconBadgeCheck"
|
||||
hugeicons="BadgeCheckIcon"
|
||||
phosphor="CheckCircleIcon"
|
||||
remixicon="RiVerifiedBadgeLine"
|
||||
/>
|
||||
Account
|
||||
</DropdownMenuItem>
|
||||
@@ -284,8 +270,6 @@ function DropdownMenuWithAvatar() {
|
||||
lucide="CreditCardIcon"
|
||||
tabler="IconCreditCard"
|
||||
hugeicons="CreditCardIcon"
|
||||
phosphor="CreditCardIcon"
|
||||
remixicon="RiBankCardLine"
|
||||
/>
|
||||
Billing
|
||||
</DropdownMenuItem>
|
||||
@@ -294,8 +278,6 @@ function DropdownMenuWithAvatar() {
|
||||
lucide="BellIcon"
|
||||
tabler="IconBell"
|
||||
hugeicons="BellIcon"
|
||||
phosphor="BellIcon"
|
||||
remixicon="RiNotification3Line"
|
||||
/>
|
||||
Notifications
|
||||
</DropdownMenuItem>
|
||||
@@ -306,8 +288,6 @@ function DropdownMenuWithAvatar() {
|
||||
lucide="LogOutIcon"
|
||||
tabler="IconLogout"
|
||||
hugeicons="LogoutIcon"
|
||||
phosphor="SignOutIcon"
|
||||
remixicon="RiLogoutBoxLine"
|
||||
/>
|
||||
Sign Out
|
||||
</DropdownMenuItem>
|
||||
@@ -361,8 +341,6 @@ function DropdownMenuAvatarOnly() {
|
||||
lucide="SparklesIcon"
|
||||
tabler="IconSparkles"
|
||||
hugeicons="SparklesIcon"
|
||||
phosphor="SparklesIcon"
|
||||
remixicon="RiSparklingLine"
|
||||
/>
|
||||
Upgrade to Pro
|
||||
</DropdownMenuItem>
|
||||
@@ -374,8 +352,6 @@ function DropdownMenuAvatarOnly() {
|
||||
lucide="BadgeCheckIcon"
|
||||
tabler="IconBadgeCheck"
|
||||
hugeicons="BadgeCheckIcon"
|
||||
phosphor="CheckCircleIcon"
|
||||
remixicon="RiVerifiedBadgeLine"
|
||||
/>
|
||||
Account
|
||||
</DropdownMenuItem>
|
||||
@@ -384,8 +360,6 @@ function DropdownMenuAvatarOnly() {
|
||||
lucide="CreditCardIcon"
|
||||
tabler="IconCreditCard"
|
||||
hugeicons="CreditCardIcon"
|
||||
phosphor="CreditCardIcon"
|
||||
remixicon="RiBankCardLine"
|
||||
/>
|
||||
Billing
|
||||
</DropdownMenuItem>
|
||||
@@ -394,8 +368,6 @@ function DropdownMenuAvatarOnly() {
|
||||
lucide="BellIcon"
|
||||
tabler="IconBell"
|
||||
hugeicons="BellIcon"
|
||||
phosphor="BellIcon"
|
||||
remixicon="RiNotification3Line"
|
||||
/>
|
||||
Notifications
|
||||
</DropdownMenuItem>
|
||||
@@ -406,8 +378,6 @@ function DropdownMenuAvatarOnly() {
|
||||
lucide="LogOutIcon"
|
||||
tabler="IconLogout"
|
||||
hugeicons="LogoutIcon"
|
||||
phosphor="SignOutIcon"
|
||||
remixicon="RiLogoutBoxLine"
|
||||
/>
|
||||
Sign Out
|
||||
</DropdownMenuItem>
|
||||
@@ -425,8 +395,6 @@ function DropdownMenuIconColor() {
|
||||
lucide="MoreHorizontalIcon"
|
||||
tabler="IconDots"
|
||||
hugeicons="MoreHorizontalCircle01Icon"
|
||||
phosphor="DotsThreeOutlineIcon"
|
||||
remixicon="RiMoreLine"
|
||||
/>
|
||||
<span className="sr-only">Toggle menu</span>
|
||||
</Button>
|
||||
@@ -438,8 +406,6 @@ function DropdownMenuIconColor() {
|
||||
lucide="PencilIcon"
|
||||
tabler="IconPencil"
|
||||
hugeicons="EditIcon"
|
||||
phosphor="PencilIcon"
|
||||
remixicon="RiPencilLine"
|
||||
/>
|
||||
Edit
|
||||
</DropdownMenuItem>
|
||||
@@ -448,8 +414,6 @@ function DropdownMenuIconColor() {
|
||||
lucide="ShareIcon"
|
||||
tabler="IconShare"
|
||||
hugeicons="ShareIcon"
|
||||
phosphor="ShareIcon"
|
||||
remixicon="RiShareLine"
|
||||
/>
|
||||
Share
|
||||
</DropdownMenuItem>
|
||||
@@ -459,8 +423,6 @@ function DropdownMenuIconColor() {
|
||||
lucide="TrashIcon"
|
||||
tabler="IconTrash"
|
||||
hugeicons="DeleteIcon"
|
||||
phosphor="TrashIcon"
|
||||
remixicon="RiDeleteBinLine"
|
||||
/>
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
|
||||
@@ -4,64 +4,59 @@ import { Label } from "@/registry/new-york-v4/ui/label"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverDescription,
|
||||
PopoverHeader,
|
||||
PopoverTitle,
|
||||
PopoverTrigger,
|
||||
} from "@/registry/new-york-v4/ui/popover"
|
||||
|
||||
export function PopoverDemo() {
|
||||
return (
|
||||
<div className="flex gap-4">
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline">Open popover</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-80" align="start">
|
||||
<div className="grid gap-4">
|
||||
<PopoverHeader>
|
||||
<PopoverTitle>Dimensions</PopoverTitle>
|
||||
<PopoverDescription>
|
||||
Set the dimensions for the layer.
|
||||
</PopoverDescription>
|
||||
</PopoverHeader>
|
||||
<div className="grid gap-2">
|
||||
<div className="grid grid-cols-3 items-center gap-4">
|
||||
<Label htmlFor="width">Width</Label>
|
||||
<Input
|
||||
id="width"
|
||||
defaultValue="100%"
|
||||
className="col-span-2 h-8"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 items-center gap-4">
|
||||
<Label htmlFor="maxWidth">Max. width</Label>
|
||||
<Input
|
||||
id="maxWidth"
|
||||
defaultValue="300px"
|
||||
className="col-span-2 h-8"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 items-center gap-4">
|
||||
<Label htmlFor="height">Height</Label>
|
||||
<Input
|
||||
id="height"
|
||||
defaultValue="25px"
|
||||
className="col-span-2 h-8"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 items-center gap-4">
|
||||
<Label htmlFor="maxHeight">Max. height</Label>
|
||||
<Input
|
||||
id="maxHeight"
|
||||
defaultValue="none"
|
||||
className="col-span-2 h-8"
|
||||
/>
|
||||
</div>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline">Open popover</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-80" align="start">
|
||||
<div className="grid gap-4">
|
||||
<div className="grid gap-1.5">
|
||||
<h4 className="leading-none font-medium">Dimensions</h4>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Set the dimensions for the layer.
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<div className="grid grid-cols-3 items-center gap-4">
|
||||
<Label htmlFor="width">Width</Label>
|
||||
<Input
|
||||
id="width"
|
||||
defaultValue="100%"
|
||||
className="col-span-2 h-8"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 items-center gap-4">
|
||||
<Label htmlFor="maxWidth">Max. width</Label>
|
||||
<Input
|
||||
id="maxWidth"
|
||||
defaultValue="300px"
|
||||
className="col-span-2 h-8"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 items-center gap-4">
|
||||
<Label htmlFor="height">Height</Label>
|
||||
<Input
|
||||
id="height"
|
||||
defaultValue="25px"
|
||||
className="col-span-2 h-8"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 items-center gap-4">
|
||||
<Label htmlFor="maxHeight">Max. height</Label>
|
||||
<Input
|
||||
id="maxHeight"
|
||||
defaultValue="none"
|
||||
className="col-span-2 h-8"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -47,27 +47,6 @@ export function SheetDemo() {
|
||||
</SheetFooter>
|
||||
</SheetContent>
|
||||
</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">
|
||||
{SHEET_SIDES.map((side) => (
|
||||
<Sheet key={side}>
|
||||
|
||||
@@ -4,17 +4,6 @@ import { Switch } from "@/registry/new-york-v4/ui/switch"
|
||||
export function SwitchDemo() {
|
||||
return (
|
||||
<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">
|
||||
<Switch id="switch-demo-airplane-mode" />
|
||||
<Label htmlFor="switch-demo-airplane-mode">Airplane Mode</Label>
|
||||
|
||||
@@ -101,45 +101,6 @@ export function TabsDemo() {
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,13 +1,5 @@
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
export function ComponentPreview({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"bg-background *:data-[slot=card]:has-[[data-slot=chart]]:shadow-none"
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
return <div className={cn("bg-background")}>{children}</div>
|
||||
}
|
||||
|
||||
@@ -4,16 +4,10 @@ import { type Metadata } from "next"
|
||||
import { notFound } from "next/navigation"
|
||||
|
||||
import { siteConfig } from "@/lib/config"
|
||||
import {
|
||||
getDemoItem,
|
||||
getRegistryComponent,
|
||||
getRegistryItem,
|
||||
} from "@/lib/registry"
|
||||
import { getRegistryComponent, getRegistryItem } from "@/lib/registry"
|
||||
import { absoluteUrl } from "@/lib/utils"
|
||||
import { getStyle, legacyStyles, type Style } from "@/registry/_legacy-styles"
|
||||
|
||||
import "@/styles/legacy-themes.css"
|
||||
|
||||
import { ComponentPreview } from "./component-preview"
|
||||
|
||||
export const revalidate = false
|
||||
@@ -22,12 +16,7 @@ export const dynamicParams = false
|
||||
|
||||
const getCachedRegistryItem = React.cache(
|
||||
async (name: string, styleName: Style["name"]) => {
|
||||
// Try registry item first, then fallback to demo item (for examples).
|
||||
const item = await getRegistryItem(name, styleName)
|
||||
if (item) {
|
||||
return item
|
||||
}
|
||||
return await getDemoItem(name, styleName)
|
||||
return await getRegistryItem(name, styleName)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -84,54 +73,9 @@ export async function generateMetadata({
|
||||
|
||||
export async function generateStaticParams() {
|
||||
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 }> = []
|
||||
|
||||
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]) {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -57,11 +57,6 @@ export const metadata: Metadata = {
|
||||
apple: "/apple-touch-icon.png",
|
||||
},
|
||||
manifest: `${siteConfig.url}/site.webmanifest`,
|
||||
alternates: {
|
||||
types: {
|
||||
"application/rss+xml": `${siteConfig.url}/rss.xml`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default function RootLayout({
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -3,27 +3,12 @@ import { ArrowRightIcon } from "lucide-react"
|
||||
|
||||
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() {
|
||||
return (
|
||||
<Badge asChild variant="secondary" className="bg-transparent">
|
||||
<Link href="/docs/changelog">
|
||||
<BaseUILogo />
|
||||
Base UI Documentation <ArrowRightIcon />
|
||||
<span className="flex size-2 rounded-full bg-blue-500" title="New" />
|
||||
npx shadcn create <ArrowRightIcon />
|
||||
</Link>
|
||||
</Badge>
|
||||
)
|
||||
|
||||
@@ -5,7 +5,6 @@ import { type z } from "zod"
|
||||
import { highlightCode } from "@/lib/highlight-code"
|
||||
import { getRegistryItem } from "@/lib/registry"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { ChartIframe } from "@/components/chart-iframe"
|
||||
import { ChartToolbar } from "@/components/chart-toolbar"
|
||||
import { type Style } from "@/registry/_legacy-styles"
|
||||
|
||||
@@ -13,43 +12,50 @@ export type Chart = z.infer<typeof registryItemSchema> & {
|
||||
highlightedCode: string
|
||||
}
|
||||
|
||||
export function ChartDisplay({
|
||||
chart,
|
||||
style,
|
||||
export async function ChartDisplay({
|
||||
name,
|
||||
styleName,
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
chart: Chart
|
||||
style: string
|
||||
name: string
|
||||
styleName: Style["name"]
|
||||
} & React.ComponentProps<"div">) {
|
||||
const chart = await getCachedRegistryItem(name, styleName)
|
||||
const highlightedCode = await getChartHighlightedCode(
|
||||
chart?.files?.[0]?.content ?? ""
|
||||
)
|
||||
|
||||
if (!chart || !highlightedCode) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
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
|
||||
)}
|
||||
>
|
||||
<ChartToolbar
|
||||
chart={chart}
|
||||
className="relative z-20 flex justify-end px-3 py-2.5"
|
||||
/>
|
||||
<div className="bg-background relative z-10 overflow-hidden rounded-xl">
|
||||
<ChartIframe
|
||||
src={`/view/${style}/${chart.name}`}
|
||||
height={460}
|
||||
title={chart.name}
|
||||
/>
|
||||
chart={{ ...chart, highlightedCode }}
|
||||
className="bg-card text-card-foreground relative z-20 flex justify-end border-b px-3 py-2.5"
|
||||
>
|
||||
{children}
|
||||
</ChartToolbar>
|
||||
<div className="relative z-10 [&>div]:rounded-none [&>div]:border-none [&>div]:shadow-none">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Exported for parallel prefetching in page components.
|
||||
export const getCachedRegistryItem = React.cache(
|
||||
const getCachedRegistryItem = React.cache(
|
||||
async (name: string, styleName: Style["name"]) => {
|
||||
return await getRegistryItem(name, styleName)
|
||||
}
|
||||
)
|
||||
|
||||
export const getChartHighlightedCode = React.cache(async (content: string) => {
|
||||
const getChartHighlightedCode = React.cache(async (content: string) => {
|
||||
return await highlightCode(content)
|
||||
})
|
||||
|
||||
@@ -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)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -46,7 +46,7 @@ export function ChartToolbar({
|
||||
}
|
||||
|
||||
function ChartTitle({ chart }: { chart: Chart }) {
|
||||
if (chart.name.includes("chart-line")) {
|
||||
if (chart.name.includes("charts-line")) {
|
||||
return (
|
||||
<>
|
||||
<LineChartIcon /> Line Chart
|
||||
|
||||
@@ -18,7 +18,7 @@ export function CodeTabs({ children }: React.ComponentProps<typeof Tabs>) {
|
||||
onValueChange={(value) =>
|
||||
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}
|
||||
</Tabs>
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
"use client"
|
||||
|
||||
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 * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||
import { IconArrowRight } from "@tabler/icons-react"
|
||||
import { useDocsSearch } from "fumadocs-core/search/client"
|
||||
import { CornerDownLeftIcon, SquareDashedIcon, XIcon } from "lucide-react"
|
||||
import { CornerDownLeftIcon, SquareDashedIcon } from "lucide-react"
|
||||
|
||||
import { type Color, type ColorPalette } from "@/lib/colors"
|
||||
import { trackEvent } from "@/lib/events"
|
||||
import { showMcpDocs } from "@/lib/flags"
|
||||
import { getCurrentBase, getPagesFromFolder } from "@/lib/page-tree"
|
||||
import { type source } from "@/lib/source"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { useConfig } from "@/hooks/use-config"
|
||||
@@ -28,14 +26,13 @@ import {
|
||||
} from "@/registry/new-york-v4/ui/command"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogOverlay,
|
||||
DialogPortal,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/registry/new-york-v4/ui/dialog"
|
||||
import { Kbd } from "@/registry/new-york-v4/ui/kbd"
|
||||
import { Kbd, KbdGroup } from "@/registry/new-york-v4/ui/kbd"
|
||||
import { Separator } from "@/registry/new-york-v4/ui/separator"
|
||||
import { Spinner } from "@/registry/new-york-v4/ui/spinner"
|
||||
|
||||
@@ -52,9 +49,7 @@ export function CommandMenu({
|
||||
navItems?: { href: string; label: string }[]
|
||||
}) {
|
||||
const router = useRouter()
|
||||
const pathname = usePathname()
|
||||
const [config] = useConfig()
|
||||
const currentBase = getCurrentBase(pathname)
|
||||
const [open, setOpen] = React.useState(false)
|
||||
const [selectedType, setSelectedType] = React.useState<
|
||||
"color" | "page" | "component" | "block" | null
|
||||
@@ -143,13 +138,10 @@ export function CommandMenu({
|
||||
[setSelectedType, setCopyPayload, packageManager]
|
||||
)
|
||||
|
||||
const runCommand = React.useCallback(
|
||||
(command: () => unknown) => {
|
||||
setOpen(false)
|
||||
command()
|
||||
},
|
||||
[setOpen]
|
||||
)
|
||||
const runCommand = React.useCallback((command: () => unknown) => {
|
||||
setOpen(false)
|
||||
command()
|
||||
}, [])
|
||||
|
||||
React.useEffect(() => {
|
||||
const down = (e: KeyboardEvent) => {
|
||||
@@ -215,7 +207,10 @@ export function CommandMenu({
|
||||
</div>
|
||||
</Button>
|
||||
</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">
|
||||
<DialogTitle>Search documentation...</DialogTitle>
|
||||
<DialogDescription>Search for a command to run...</DialogDescription>
|
||||
@@ -274,37 +269,40 @@ export function CommandMenu({
|
||||
className="!p-0 [&_[cmdk-group-heading]]:scroll-mt-16 [&_[cmdk-group-heading]]:!p-3 [&_[cmdk-group-heading]]:!pb-1"
|
||||
>
|
||||
{group.type === "folder" &&
|
||||
getPagesFromFolder(group, currentBase).map((item) => {
|
||||
const isComponent = item.url.includes("/components/")
|
||||
group.children.map((item) => {
|
||||
if (item.type === "page") {
|
||||
const isComponent = item.url.includes("/components/")
|
||||
|
||||
if (!showMcpDocs && item.url.includes("/mcp")) {
|
||||
return null
|
||||
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 (
|
||||
<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>
|
||||
))}
|
||||
@@ -525,27 +523,3 @@ function SearchResults({
|
||||
</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-[50%] 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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,199 +3,49 @@
|
||||
import * as React from "react"
|
||||
|
||||
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"
|
||||
|
||||
export function ComponentPreviewTabs({
|
||||
className,
|
||||
previewClassName,
|
||||
align = "center",
|
||||
hideCode = false,
|
||||
chromeLessOnMobile = false,
|
||||
component,
|
||||
source,
|
||||
sourcePreview,
|
||||
direction = "ltr",
|
||||
styleName,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & {
|
||||
previewClassName?: string
|
||||
align?: "center" | "start" | "end"
|
||||
hideCode?: boolean
|
||||
chromeLessOnMobile?: boolean
|
||||
component: 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 (
|
||||
<div
|
||||
data-slot="component-preview"
|
||||
className={cn(
|
||||
"group relative mt-4 mb-12 flex flex-col gap-2 overflow-hidden rounded-xl border",
|
||||
"group relative mt-4 mb-12 flex flex-col gap-2 rounded-lg border",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{direction === "rtl" ? (
|
||||
<LanguageProvider defaultLanguage="ar">
|
||||
<RtlLanguageSelector />
|
||||
<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 data-slot="preview">
|
||||
<div
|
||||
data-slot="code"
|
||||
data-mobile-code-visible={isMobileCodeVisible}
|
||||
className="relative 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-72"
|
||||
>
|
||||
{isMobileCodeVisible ? (
|
||||
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"
|
||||
onClick={() => {
|
||||
setIsMobileCodeVisible(true)
|
||||
}}
|
||||
>
|
||||
View Code
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
data-align={align}
|
||||
className={cn(
|
||||
"preview flex w-full justify-center data-[align=center]:items-center data-[align=end]:items-end data-[align=start]:items-start",
|
||||
chromeLessOnMobile ? "sm:p-10" : "h-[450px] p-10"
|
||||
)}
|
||||
>
|
||||
{component}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const directionTranslations: Translations<Record<string, never>> = {
|
||||
en: {
|
||||
dir: "ltr",
|
||||
values: {},
|
||||
},
|
||||
ar: {
|
||||
dir: "rtl",
|
||||
values: {},
|
||||
},
|
||||
he: {
|
||||
dir: "rtl",
|
||||
values: {},
|
||||
},
|
||||
}
|
||||
|
||||
function RtlLanguageSelector() {
|
||||
const context = useLanguageContext()
|
||||
// This component is always rendered inside LanguageProvider when direction === "rtl".
|
||||
// so context should always be available.
|
||||
if (!context) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<LanguageSelector
|
||||
value={context.language}
|
||||
onValueChange={context.setLanguage}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
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}>
|
||||
<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
|
||||
{!hideCode && (
|
||||
<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]"
|
||||
>
|
||||
{source}
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</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>
|
||||
}
|
||||
|
||||
@@ -1,44 +1,45 @@
|
||||
import * as React from "react"
|
||||
import Image from "next/image"
|
||||
|
||||
import { getRegistryComponent } from "@/lib/registry"
|
||||
import { ComponentPreviewTabs } from "@/components/component-preview-tabs"
|
||||
import { ComponentSource } from "@/components/component-source"
|
||||
import { Index } from "@/registry/__index__"
|
||||
import { type Style } from "@/registry/_legacy-styles"
|
||||
|
||||
export function ComponentPreview({
|
||||
name,
|
||||
styleName = "new-york-v4",
|
||||
type,
|
||||
className,
|
||||
previewClassName,
|
||||
align = "center",
|
||||
hideCode = false,
|
||||
chromeLessOnMobile = false,
|
||||
styleName = "new-york-v4",
|
||||
direction = "ltr",
|
||||
caption,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & {
|
||||
name: string
|
||||
styleName?: string
|
||||
styleName?: Style["name"]
|
||||
align?: "center" | "start" | "end"
|
||||
description?: string
|
||||
hideCode?: boolean
|
||||
type?: "block" | "component" | "example"
|
||||
chromeLessOnMobile?: boolean
|
||||
previewClassName?: string
|
||||
direction?: "ltr" | "rtl"
|
||||
caption?: string
|
||||
}) {
|
||||
const translationDisclaimer =
|
||||
direction === "rtl"
|
||||
? "Automatic translation may contain errors."
|
||||
: undefined
|
||||
const finalCaption =
|
||||
[caption, translationDisclaimer].filter(Boolean).join(" ") || undefined
|
||||
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") {
|
||||
const content = (
|
||||
<div className="relative aspect-[4/2.5] w-full overflow-hidden rounded-xl border md:-mx-1">
|
||||
return (
|
||||
<div className="relative aspect-[4/2.5] w-full overflow-hidden rounded-md border md:-mx-1">
|
||||
<Image
|
||||
src={`/r/styles/new-york-v4/${name}-light.png`}
|
||||
alt={name}
|
||||
@@ -58,42 +59,14 @@ export function ComponentPreview({
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
if (finalCaption) {
|
||||
return (
|
||||
<figure className="flex flex-col gap-4">
|
||||
{content}
|
||||
<figcaption className="text-muted-foreground text-center text-sm">
|
||||
{finalCaption}
|
||||
</figcaption>
|
||||
</figure>
|
||||
)
|
||||
}
|
||||
|
||||
return content
|
||||
}
|
||||
|
||||
const Component = getRegistryComponent(name, styleName)
|
||||
|
||||
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 = (
|
||||
return (
|
||||
<ComponentPreviewTabs
|
||||
className={className}
|
||||
previewClassName={previewClassName}
|
||||
align={align}
|
||||
hideCode={hideCode}
|
||||
component={<DynamicComponent name={name} styleName={styleName} />}
|
||||
component={<Component />}
|
||||
source={
|
||||
<ComponentSource
|
||||
name={name}
|
||||
@@ -101,53 +74,8 @@ export function ComponentPreview({
|
||||
styleName={styleName}
|
||||
/>
|
||||
}
|
||||
sourcePreview={
|
||||
<ComponentSource
|
||||
name={name}
|
||||
collapsible={false}
|
||||
styleName={styleName}
|
||||
maxLines={3}
|
||||
/>
|
||||
}
|
||||
chromeLessOnMobile={chromeLessOnMobile}
|
||||
direction={direction}
|
||||
styleName={styleName}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
||||
if (finalCaption) {
|
||||
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">
|
||||
{finalCaption}
|
||||
</figcaption>
|
||||
</figure>
|
||||
)
|
||||
}
|
||||
|
||||
return content
|
||||
}
|
||||
|
||||
function DynamicComponent({
|
||||
name,
|
||||
styleName,
|
||||
}: {
|
||||
name: string
|
||||
styleName: string
|
||||
}) {
|
||||
const Component = React.useMemo(
|
||||
() => getRegistryComponent(name, styleName),
|
||||
[name, styleName]
|
||||
)
|
||||
|
||||
if (!Component) {
|
||||
return null
|
||||
}
|
||||
|
||||
return React.createElement(Component)
|
||||
}
|
||||
|
||||
@@ -3,12 +3,12 @@ import path from "node:path"
|
||||
import * as React from "react"
|
||||
|
||||
import { highlightCode } from "@/lib/highlight-code"
|
||||
import { getDemoItem, getRegistryItem } from "@/lib/registry"
|
||||
import { formatCode } from "@/lib/rehype"
|
||||
import { getRegistryItem } from "@/lib/registry"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { CodeCollapsibleWrapper } from "@/components/code-collapsible-wrapper"
|
||||
import { CopyButton } from "@/components/copy-button"
|
||||
import { getIconForLanguageExtension } from "@/components/icons"
|
||||
import { type Style } from "@/registry/_legacy-styles"
|
||||
|
||||
export async function ComponentSource({
|
||||
name,
|
||||
@@ -18,15 +18,13 @@ export async function ComponentSource({
|
||||
collapsible = true,
|
||||
className,
|
||||
styleName = "new-york-v4",
|
||||
maxLines,
|
||||
}: React.ComponentProps<"div"> & {
|
||||
name?: string
|
||||
src?: string
|
||||
title?: string
|
||||
language?: string
|
||||
collapsible?: boolean
|
||||
styleName?: string
|
||||
maxLines?: number
|
||||
styleName?: Style["name"]
|
||||
}) {
|
||||
if (!name && !src) {
|
||||
return null
|
||||
@@ -35,9 +33,7 @@ export async function ComponentSource({
|
||||
let code: string | undefined
|
||||
|
||||
if (name) {
|
||||
const item =
|
||||
(await getDemoItem(name, styleName)) ??
|
||||
(await getRegistryItem(name, styleName))
|
||||
const item = await getRegistryItem(name, styleName)
|
||||
code = item?.files?.[0]?.content
|
||||
}
|
||||
|
||||
@@ -50,13 +46,13 @@ export async function ComponentSource({
|
||||
return null
|
||||
}
|
||||
|
||||
code = await formatCode(code, styleName)
|
||||
code = code.replaceAll("/* eslint-disable react/no-children-prop */\n", "")
|
||||
// Fix imports.
|
||||
// Replace @/registry/${style}/ with @/components/.
|
||||
code = code.replaceAll(`@/registry/${styleName}/`, "@/components/")
|
||||
|
||||
// Truncate code if maxLines is set.
|
||||
if (maxLines) {
|
||||
code = code.split("\n").slice(0, maxLines).join("\n")
|
||||
}
|
||||
// Replace export default with export.
|
||||
code = code.replaceAll("export default", "export")
|
||||
code = code.replaceAll("/* eslint-disable react/no-children-prop */\n", "")
|
||||
|
||||
const lang = language ?? title?.split(".").pop() ?? "tsx"
|
||||
const highlightedCode = await highlightCode(code, lang)
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
import Link from "next/link"
|
||||
|
||||
import { PAGES_NEW } from "@/lib/docs"
|
||||
import { getPagesFromFolder, type PageTreeFolder } from "@/lib/page-tree"
|
||||
import { source } from "@/lib/source"
|
||||
|
||||
export function ComponentsList({
|
||||
componentsFolder,
|
||||
currentBase,
|
||||
}: {
|
||||
componentsFolder: PageTreeFolder
|
||||
currentBase: string
|
||||
}) {
|
||||
const list = getPagesFromFolder(componentsFolder, currentBase)
|
||||
export function ComponentsList() {
|
||||
const components = source.pageTree.children.find(
|
||||
(page) => page.$id === "components"
|
||||
)
|
||||
|
||||
if (components?.type !== "folder") {
|
||||
return
|
||||
}
|
||||
|
||||
const list = components.children.filter(
|
||||
(component) => component.type === "page"
|
||||
)
|
||||
|
||||
return (
|
||||
<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">
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
"use client"
|
||||
|
||||
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 { copyToClipboardWithMeta } from "@/components/copy-button"
|
||||
import { CopyButton } from "@/components/copy-button"
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import {
|
||||
Dialog,
|
||||
@@ -27,114 +29,77 @@ import {
|
||||
DrawerTitle,
|
||||
DrawerTrigger,
|
||||
} 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({
|
||||
registry,
|
||||
}: {
|
||||
registry: { name: string }
|
||||
registry: {
|
||||
name: string
|
||||
url: string
|
||||
}
|
||||
}) {
|
||||
const [config, setConfig] = useConfig()
|
||||
const [hasCopied, setHasCopied] = React.useState(false)
|
||||
const [open, setOpen] = React.useState(false)
|
||||
const { copyToClipboard, isCopied } = useCopyToClipboard()
|
||||
const isMobile = useIsMobile()
|
||||
const [open, setOpen] = React.useState(false)
|
||||
|
||||
const packageManager = config.packageManager || "pnpm"
|
||||
|
||||
const commands = React.useMemo(() => {
|
||||
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 jsonValue = `{
|
||||
"registries": {
|
||||
"${registry.name}": "${registry.url}"
|
||||
}
|
||||
}`
|
||||
|
||||
const Trigger = (
|
||||
<Button size="sm" variant="outline" className="relative z-10">
|
||||
Add <IconPlus />
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="relative z-10"
|
||||
onClick={() => setOpen(true)}
|
||||
>
|
||||
{isCopied ? (
|
||||
<IconCheck />
|
||||
) : (
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>Model Context Protocol</title>
|
||||
<path
|
||||
d="M13.85 0a4.16 4.16 0 0 0-2.95 1.217L1.456 10.66a.835.835 0 0 0 0 1.18.835.835 0 0 0 1.18 0l9.442-9.442a2.49 2.49 0 0 1 3.541 0 2.49 2.49 0 0 1 0 3.541L8.59 12.97l-.1.1a.835.835 0 0 0 0 1.18.835.835 0 0 0 1.18 0l.1-.098 7.03-7.034a2.49 2.49 0 0 1 3.542 0l.049.05a2.49 2.49 0 0 1 0 3.54l-8.54 8.54a1.96 1.96 0 0 0 0 2.755l1.753 1.753a.835.835 0 0 0 1.18 0 .835.835 0 0 0 0-1.18l-1.753-1.753a.266.266 0 0 1 0-.394l8.54-8.54a4.185 4.185 0 0 0 0-5.9l-.05-.05a4.16 4.16 0 0 0-2.95-1.218c-.2 0-.401.02-.6.048a4.17 4.17 0 0 0-1.17-3.552A4.16 4.16 0 0 0 13.85 0m0 3.333a.84.84 0 0 0-.59.245L6.275 10.56a4.186 4.186 0 0 0 0 5.902 4.186 4.186 0 0 0 5.902 0L19.16 9.48a.835.835 0 0 0 0-1.18.835.835 0 0 0-1.18 0l-6.985 6.984a2.49 2.49 0 0 1-3.54 0 2.49 2.49 0 0 1 0-3.54l6.983-6.985a.835.835 0 0 0 0-1.18.84.84 0 0 0-.59-.245"
|
||||
className="fill-foreground"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
MCP
|
||||
</Button>
|
||||
)
|
||||
|
||||
const Content = (
|
||||
<Tabs
|
||||
value={packageManager}
|
||||
onValueChange={(value) => {
|
||||
setConfig({
|
||||
...config,
|
||||
packageManager: value as "pnpm" | "npm" | "yarn" | "bun",
|
||||
})
|
||||
}}
|
||||
className="gap-0 overflow-hidden rounded-lg border"
|
||||
>
|
||||
<div className="flex items-center gap-2 border-b 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={handleCopy}
|
||||
>
|
||||
{hasCopied ? (
|
||||
<IconCheck className="size-4" />
|
||||
) : (
|
||||
<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>
|
||||
<>
|
||||
<figure
|
||||
data-rehype-pretty-code-figure
|
||||
className={cn(
|
||||
"group relative mt-0",
|
||||
!isMobile &&
|
||||
"dark:bg-background dark:[&_[data-line]:not([data-highlighted-line]):before]:bg-background!"
|
||||
)}
|
||||
>
|
||||
<CopyButton
|
||||
value={jsonValue}
|
||||
className="top-3 right-2"
|
||||
tooltip="Copy Code"
|
||||
/>
|
||||
<div data-rehype-pretty-code-title>components.json</div>
|
||||
<pre className="no-scrollbar min-w-0 overflow-x-auto px-4 py-3.5 outline-none has-[[data-highlighted-line]]:px-0 has-[[data-line-numbers]]:px-0 has-[[data-slot=tabs]]:p-0">
|
||||
<code data-line-numbers data-language="json">
|
||||
<span data-line>{"{"}</span>
|
||||
<span data-line>{' "registries": {'}</span>
|
||||
<span
|
||||
data-line
|
||||
data-highlighted-line
|
||||
>{` "${registry.name}": "${registry.url}"`}</span>
|
||||
<span data-line>{" }"}</span>
|
||||
<span data-line>{"}"}</span>
|
||||
</code>
|
||||
</pre>
|
||||
</figure>
|
||||
</>
|
||||
)
|
||||
|
||||
if (isMobile) {
|
||||
@@ -143,16 +108,20 @@ export function DirectoryAddButton({
|
||||
<DrawerTrigger asChild>{Trigger}</DrawerTrigger>
|
||||
<DrawerContent>
|
||||
<DrawerHeader>
|
||||
<DrawerTitle>Add Registry</DrawerTitle>
|
||||
<DrawerTitle>Configure MCP</DrawerTitle>
|
||||
<DrawerDescription>
|
||||
Run this command to add {registry.name} to your project.
|
||||
Copy and paste the following code into your project's
|
||||
components.json.
|
||||
</DrawerDescription>
|
||||
</DrawerHeader>
|
||||
<div className="px-4">{Content}</div>
|
||||
<div className="px-6">{Content}</div>
|
||||
<DrawerFooter>
|
||||
<DrawerClose asChild>
|
||||
<Button size="sm">Done</Button>
|
||||
<Button size="sm">Close</Button>
|
||||
</DrawerClose>
|
||||
<Button size="sm" asChild variant="outline">
|
||||
<Link href="/docs/mcp">Read the docs</Link>
|
||||
</Button>
|
||||
</DrawerFooter>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
@@ -162,15 +131,22 @@ export function DirectoryAddButton({
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<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>
|
||||
<DialogTitle>Add Registry</DialogTitle>
|
||||
<DialogTitle>Configure MCP</DialogTitle>
|
||||
<DialogDescription>
|
||||
Run this command to add {registry.name} to your project.
|
||||
Copy and paste the following code into your project's
|
||||
components.json.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{Content}
|
||||
<DialogFooter>
|
||||
<DialogFooter className="justify-between!">
|
||||
<Button size="sm" asChild variant="ghost">
|
||||
<Link href="/docs/mcp">Read the docs</Link>
|
||||
</Button>
|
||||
<DialogClose asChild>
|
||||
<Button size="sm">Done</Button>
|
||||
</DialogClose>
|
||||
|
||||
@@ -50,10 +50,8 @@ export function DirectoryList() {
|
||||
href={getHomepageUrl(registry.homepage)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer external"
|
||||
className="group flex items-center gap-1"
|
||||
>
|
||||
{registry.name}{" "}
|
||||
<IconArrowUpRight className="size-4 opacity-0 group-hover:opacity-100" />
|
||||
{registry.name}
|
||||
</a>
|
||||
</ItemTitle>
|
||||
{registry.description && (
|
||||
@@ -63,6 +61,15 @@ export function DirectoryList() {
|
||||
)}
|
||||
</ItemContent>
|
||||
<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} />
|
||||
</ItemActions>
|
||||
<ItemFooter className="justify-start pl-16 sm:hidden">
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
import Link from "next/link"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { BASES } from "@/registry/bases"
|
||||
|
||||
export function DocsBaseSwitcher({
|
||||
base,
|
||||
component,
|
||||
className,
|
||||
}: {
|
||||
base: string
|
||||
component: string
|
||||
className?: string
|
||||
}) {
|
||||
const activeBase = BASES.find((baseItem) => base === baseItem.name)
|
||||
|
||||
return (
|
||||
<div className={cn("inline-flex w-full items-center gap-6", className)}>
|
||||
{BASES.map((baseItem) => (
|
||||
<Link
|
||||
key={baseItem.name}
|
||||
href={`/docs/components/${baseItem.name}/${component}`}
|
||||
data-active={base === baseItem.name}
|
||||
className="text-muted-foreground hover:text-foreground data-[active=true]:text-foreground after:bg-foreground relative inline-flex items-center justify-center gap-1 pt-1 pb-0.5 text-base font-medium transition-colors after:absolute after:inset-x-0 after:bottom-[-4px] after:h-0.5 after:opacity-0 after:transition-opacity data-[active=true]:after:opacity-100"
|
||||
>
|
||||
{baseItem.title}
|
||||
</Link>
|
||||
))}
|
||||
{activeBase?.meta?.logo && (
|
||||
<div
|
||||
className="text-muted-foreground ml-auto size-4 shrink-0 opacity-80 [&_svg]:size-4"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: activeBase.meta.logo,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -87,70 +87,6 @@ const menuItems = {
|
||||
Open in Claude
|
||||
</a>
|
||||
),
|
||||
scira: (url: string) => (
|
||||
<a
|
||||
href={getPromptUrl("https://scira.ai/", url)}
|
||||
target="_blank"
|
||||
className="m-0 p-0"
|
||||
>
|
||||
<svg
|
||||
width="910"
|
||||
height="934"
|
||||
viewBox="0 0 910 934"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M647.664 197.775C569.13 189.049 525.5 145.419 516.774 66.8849C508.048 145.419 464.418 189.049 385.884 197.775C464.418 206.501 508.048 250.131 516.774 328.665C525.5 250.131 569.13 206.501 647.664 197.775Z"
|
||||
fill="currentColor"
|
||||
stroke="currentColor"
|
||||
strokeWidth="8"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M516.774 304.217C510.299 275.491 498.208 252.087 480.335 234.214C462.462 216.341 439.058 204.251 410.333 197.775C439.059 191.3 462.462 179.209 480.335 161.336C498.208 143.463 510.299 120.06 516.774 91.334C523.25 120.059 535.34 143.463 553.213 161.336C571.086 179.209 594.49 191.3 623.216 197.775C594.49 204.251 571.086 216.341 553.213 234.214C535.34 252.087 523.25 275.491 516.774 304.217Z"
|
||||
fill="currentColor"
|
||||
stroke="currentColor"
|
||||
strokeWidth="8"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M857.5 508.116C763.259 497.644 710.903 445.288 700.432 351.047C689.961 445.288 637.605 497.644 543.364 508.116C637.605 518.587 689.961 570.943 700.432 665.184C710.903 570.943 763.259 518.587 857.5 508.116Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="20"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M700.432 615.957C691.848 589.05 678.575 566.357 660.383 548.165C642.191 529.973 619.499 516.7 592.593 508.116C619.499 499.533 642.191 486.258 660.383 468.066C678.575 449.874 691.848 427.181 700.432 400.274C709.015 427.181 722.289 449.874 740.481 468.066C758.673 486.258 781.365 499.533 808.271 508.116C781.365 516.7 758.673 529.973 740.481 548.165C722.289 566.357 709.015 589.05 700.432 615.957Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="20"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M889.949 121.237C831.049 114.692 798.326 81.9698 791.782 23.0692C785.237 81.9698 752.515 114.692 693.614 121.237C752.515 127.781 785.237 160.504 791.782 219.404C798.326 160.504 831.049 127.781 889.949 121.237Z"
|
||||
fill="currentColor"
|
||||
stroke="currentColor"
|
||||
strokeWidth="8"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M791.782 196.795C786.697 176.937 777.869 160.567 765.16 147.858C752.452 135.15 736.082 126.322 716.226 121.237C736.082 116.152 752.452 107.324 765.16 94.6152C777.869 81.9065 786.697 65.5368 791.782 45.6797C796.867 65.5367 805.695 81.9066 818.403 94.6152C831.112 107.324 847.481 116.152 867.338 121.237C847.481 126.322 831.112 135.15 818.403 147.858C805.694 160.567 796.867 176.937 791.782 196.795Z"
|
||||
fill="currentColor"
|
||||
stroke="currentColor"
|
||||
strokeWidth="8"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M760.632 764.337C720.719 814.616 669.835 855.1 611.872 882.692C553.91 910.285 490.404 924.255 426.213 923.533C362.022 922.812 298.846 907.419 241.518 878.531C184.19 849.643 134.228 808.026 95.4548 756.863C56.6815 705.7 30.1238 646.346 17.8129 583.343C5.50207 520.339 7.76433 455.354 24.4266 393.359C41.089 331.364 71.7099 274.001 113.947 225.658C156.184 177.315 208.919 139.273 268.117 114.442"
|
||||
stroke="currentColor"
|
||||
strokeWidth="30"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
Open in Scira
|
||||
</a>
|
||||
),
|
||||
}
|
||||
|
||||
export function DocsCopyPage({ page, url }: { page: string; url: string }) {
|
||||
@@ -183,10 +119,7 @@ export function DocsCopyPage({ page, url }: { page: string; url: string }) {
|
||||
<DropdownMenuTrigger asChild className="hidden sm:flex">
|
||||
{trigger}
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="end"
|
||||
className="animate-none! rounded-lg shadow-none"
|
||||
>
|
||||
<DropdownMenuContent align="end" className="shadow-none">
|
||||
{Object.entries(menuItems).map(([key, value]) => (
|
||||
<DropdownMenuItem key={key} asChild>
|
||||
{value(url)}
|
||||
@@ -196,13 +129,13 @@ export function DocsCopyPage({ page, url }: { page: string; url: string }) {
|
||||
</DropdownMenu>
|
||||
<Separator
|
||||
orientation="vertical"
|
||||
className="!bg-foreground/5 absolute top-1 right-8 z-0 !h-6 peer-focus-visible:opacity-0 sm:right-7 sm:!h-5"
|
||||
className="!bg-foreground/10 absolute top-0 right-8 z-0 !h-8 peer-focus-visible:opacity-0 sm:right-7 sm:!h-7"
|
||||
/>
|
||||
<PopoverTrigger asChild className="flex sm:hidden">
|
||||
{trigger}
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className="bg-background/70 dark:bg-background/60 w-52 !origin-center rounded-lg p-1 shadow-none backdrop-blur-sm"
|
||||
className="bg-background/70 dark:bg-background/60 w-52 !origin-center rounded-lg p-1 shadow-sm backdrop-blur-sm"
|
||||
align="start"
|
||||
>
|
||||
{Object.entries(menuItems).map(([key, value]) => (
|
||||
|
||||
@@ -1,179 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import { IconCheck, IconCopy } from "@tabler/icons-react"
|
||||
|
||||
import { useCopyToClipboard } from "@/hooks/use-copy-to-clipboard"
|
||||
|
||||
function getPromptUrl(baseURL: string, url: string) {
|
||||
return `${baseURL}?q=${encodeURIComponent(
|
||||
`I'm looking at this shadcn/ui documentation: ${url}.
|
||||
Help me understand how to use it. Be ready to explain concepts, give examples, or help debug based on it.
|
||||
`
|
||||
)}`
|
||||
}
|
||||
|
||||
export function DocsPageLinks({ page, url }: { page: string; url: string }) {
|
||||
const { copyToClipboard, isCopied } = useCopyToClipboard()
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3 px-6">
|
||||
<ul className="text-muted-foreground flex flex-col gap-2 text-[0.8rem]">
|
||||
<li>
|
||||
<button
|
||||
onClick={() => copyToClipboard(page)}
|
||||
className="hover:text-foreground inline-flex items-center gap-2 transition-colors"
|
||||
>
|
||||
{isCopied ? (
|
||||
<IconCheck className="size-4" />
|
||||
) : (
|
||||
<IconCopy className="size-4" />
|
||||
)}
|
||||
Copy page
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href={`${url}.md`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hover:text-foreground inline-flex items-center gap-2 transition-colors"
|
||||
>
|
||||
<svg strokeLinejoin="round" viewBox="0 0 22 16" className="size-4">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M19.5 2.25H2.5C1.80964 2.25 1.25 2.80964 1.25 3.5V12.5C1.25 13.1904 1.80964 13.75 2.5 13.75H19.5C20.1904 13.75 20.75 13.1904 20.75 12.5V3.5C20.75 2.80964 20.1904 2.25 19.5 2.25ZM2.5 1C1.11929 1 0 2.11929 0 3.5V12.5C0 13.8807 1.11929 15 2.5 15H19.5C20.8807 15 22 13.8807 22 12.5V3.5C22 2.11929 20.8807 1 19.5 1H2.5ZM3 4.5H4H4.25H4.6899L4.98715 4.82428L7 7.02011L9.01285 4.82428L9.3101 4.5H9.75H10H11V5.5V11.5H9V7.79807L7.73715 9.17572L7 9.97989L6.26285 9.17572L5 7.79807V11.5H3V5.5V4.5ZM15 8V4.5H17V8H19.5L17 10.5L16 11.5L15 10.5L12.5 8H15Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
View as Markdown
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href={getPromptUrl("https://v0.dev", url)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hover:text-foreground inline-flex items-center gap-2 transition-colors"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 147 70"
|
||||
className="size-4"
|
||||
>
|
||||
<path d="M56 50.203V14h14v46.156C70 65.593 65.593 70 60.156 70c-2.596 0-5.158-1-7-2.843L0 14h19.797L56 50.203ZM147 56h-14V23.953L100.953 56H133v14H96.687C85.814 70 77 61.186 77 50.312V14h14v32.156L123.156 14H91V0h36.312C138.186 0 147 8.814 147 19.688V56Z" />
|
||||
</svg>
|
||||
Open in v0
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href={getPromptUrl("https://chatgpt.com", url)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hover:text-foreground inline-flex items-center gap-2 transition-colors"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
className="size-4"
|
||||
>
|
||||
<path
|
||||
d="M22.282 9.821a5.985 5.985 0 0 0-.516-4.91 6.046 6.046 0 0 0-6.51-2.9A6.065 6.065 0 0 0 4.981 4.18a5.985 5.985 0 0 0-3.998 2.9 6.046 6.046 0 0 0 .743 7.097 5.98 5.98 0 0 0 .51 4.911 6.051 6.051 0 0 0 6.515 2.9A5.985 5.985 0 0 0 13.26 24a6.056 6.056 0 0 0 5.772-4.206 5.99 5.99 0 0 0 3.997-2.9 6.056 6.056 0 0 0-.747-7.073zM13.26 22.43a4.476 4.476 0 0 1-2.876-1.04l.141-.081 4.779-2.758a.795.795 0 0 0 .392-.681v-6.737l2.02 1.168a.071.071 0 0 1 .038.052v5.583a4.504 4.504 0 0 1-4.494 4.494zM3.6 18.304a4.47 4.47 0 0 1-.535-3.014l.142.085 4.783 2.759a.771.771 0 0 0 .78 0l5.843-3.369v2.332a.08.08 0 0 1-.033.062L9.74 19.95a4.5 4.5 0 0 1-6.14-1.646zM2.34 7.896a4.485 4.485 0 0 1 2.366-1.973V11.6a.766.766 0 0 0 .388.676l5.815 3.355-2.02 1.168a.076.076 0 0 1-.071 0l-4.83-2.786A4.504 4.504 0 0 1 2.34 7.872zm16.597 3.855-5.833-3.387L15.119 7.2a.076.076 0 0 1 .071 0l4.83 2.791a4.494 4.494 0 0 1-.676 8.105v-5.678a.79.79 0 0 0-.407-.667zm2.01-3.023-.141-.085-4.774-2.782a.776.776 0 0 0-.785 0L9.409 9.23V6.897a.066.066 0 0 1 .028-.061l4.83-2.787a4.5 4.5 0 0 1 6.68 4.66zm-12.64 4.135-2.02-1.164a.08.08 0 0 1-.038-.057V6.075a4.5 4.5 0 0 1 7.375-3.453l-.142.08-4.778 2.758a.795.795 0 0 0-.393.681zm1.097-2.365 2.602-1.5 2.607 1.5v2.999l-2.597 1.5-2.607-1.5Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
Open in ChatGPT
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href={getPromptUrl("https://claude.ai/new", url)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hover:text-foreground inline-flex items-center gap-2 transition-colors"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
className="size-4"
|
||||
>
|
||||
<path
|
||||
d="m4.714 15.956 4.718-2.648.079-.23-.08-.128h-.23l-.79-.048-2.695-.073-2.337-.097-2.265-.122-.57-.121-.535-.704.055-.353.48-.321.685.06 1.518.104 2.277.157 1.651.098 2.447.255h.389l.054-.158-.133-.097-.103-.098-2.356-1.596-2.55-1.688-1.336-.972-.722-.491L2 6.223l-.158-1.008.655-.722.88.06.225.061.893.686 1.906 1.476 2.49 1.833.364.304.146-.104.018-.072-.164-.274-1.354-2.446-1.445-2.49-.644-1.032-.17-.619a2.972 2.972 0 0 1-.103-.729L6.287.133 6.7 0l.995.134.42.364.619 1.415L9.735 4.14l1.555 3.03.455.898.243.832.09.255h.159V9.01l.127-1.706.237-2.095.23-2.695.08-.76.376-.91.747-.492.583.28.48.685-.067.444-.286 1.851-.558 2.903-.365 1.942h.213l.243-.242.983-1.306 1.652-2.064.728-.82.85-.904.547-.431h1.032l.759 1.129-.34 1.166-1.063 1.347-.88 1.142-1.263 1.7-.79 1.36.074.11.188-.02 2.853-.606 1.542-.28 1.84-.315.832.388.09.395-.327.807-1.967.486-2.307.462-3.436.813-.043.03.049.061 1.548.146.662.036h1.62l3.018.225.79.522.473.638-.08.485-1.213.62-1.64-.389-3.825-.91-1.31-.329h-.183v.11l1.093 1.068 2.003 1.81 2.508 2.33.127.578-.321.455-.34-.049-2.204-1.657-.85-.747-1.925-1.62h-.127v.17l.443.649 2.343 3.521.122 1.08-.17.353-.607.213-.668-.122-1.372-1.924-1.415-2.168-1.141-1.943-.14.08-.674 7.254-.316.37-.728.28-.607-.461-.322-.747.322-1.476.388-1.924.316-1.53.285-1.9.17-.632-.012-.042-.14.018-1.432 1.967-2.18 2.945-1.724 1.845-.413.164-.716-.37.066-.662.401-.589 2.386-3.036 1.439-1.882.929-1.086-.006-.158h-.055L4.138 18.56l-1.13.146-.485-.456.06-.746.231-.243 1.907-1.312Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
Open in Claude
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href={getPromptUrl("https://scira.ai/", url)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hover:text-foreground inline-flex items-center gap-2 transition-colors"
|
||||
>
|
||||
<svg
|
||||
width="910"
|
||||
height="934"
|
||||
viewBox="0 0 910 934"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="size-4"
|
||||
>
|
||||
<path
|
||||
d="M647.664 197.775C569.13 189.049 525.5 145.419 516.774 66.8849C508.048 145.419 464.418 189.049 385.884 197.775C464.418 206.501 508.048 250.131 516.774 328.665C525.5 250.131 569.13 206.501 647.664 197.775Z"
|
||||
fill="currentColor"
|
||||
stroke="currentColor"
|
||||
strokeWidth="8"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M516.774 304.217C510.299 275.491 498.208 252.087 480.335 234.214C462.462 216.341 439.058 204.251 410.333 197.775C439.059 191.3 462.462 179.209 480.335 161.336C498.208 143.463 510.299 120.06 516.774 91.334C523.25 120.059 535.34 143.463 553.213 161.336C571.086 179.209 594.49 191.3 623.216 197.775C594.49 204.251 571.086 216.341 553.213 234.214C535.34 252.087 523.25 275.491 516.774 304.217Z"
|
||||
fill="currentColor"
|
||||
stroke="currentColor"
|
||||
strokeWidth="8"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M857.5 508.116C763.259 497.644 710.903 445.288 700.432 351.047C689.961 445.288 637.605 497.644 543.364 508.116C637.605 518.587 689.961 570.943 700.432 665.184C710.903 570.943 763.259 518.587 857.5 508.116Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="20"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M700.432 615.957C691.848 589.05 678.575 566.357 660.383 548.165C642.191 529.973 619.499 516.7 592.593 508.116C619.499 499.533 642.191 486.258 660.383 468.066C678.575 449.874 691.848 427.181 700.432 400.274C709.015 427.181 722.289 449.874 740.481 468.066C758.673 486.258 781.365 499.533 808.271 508.116C781.365 516.7 758.673 529.973 740.481 548.165C722.289 566.357 709.015 589.05 700.432 615.957Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="20"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M889.949 121.237C831.049 114.692 798.326 81.9698 791.782 23.0692C785.237 81.9698 752.515 114.692 693.614 121.237C752.515 127.781 785.237 160.504 791.782 219.404C798.326 160.504 831.049 127.781 889.949 121.237Z"
|
||||
fill="currentColor"
|
||||
stroke="currentColor"
|
||||
strokeWidth="8"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M791.782 196.795C786.697 176.937 777.869 160.567 765.16 147.858C752.452 135.15 736.082 126.322 716.226 121.237C736.082 116.152 752.452 107.324 765.16 94.6152C777.869 81.9065 786.697 65.5368 791.782 45.6797C796.867 65.5367 805.695 81.9066 818.403 94.6152C831.112 107.324 847.481 116.152 867.338 121.237C847.481 126.322 831.112 135.15 818.403 147.858C805.694 160.567 796.867 176.937 791.782 196.795Z"
|
||||
fill="currentColor"
|
||||
stroke="currentColor"
|
||||
strokeWidth="8"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M760.632 764.337C720.719 814.616 669.835 855.1 611.872 882.692C553.91 910.285 490.404 924.255 426.213 923.533C362.022 922.812 298.846 907.419 241.518 878.531C184.19 849.643 134.228 808.026 95.4548 756.863C56.6815 705.7 30.1238 646.346 17.8129 583.343C5.50207 520.339 7.76433 455.354 24.4266 393.359C41.089 331.364 71.7099 274.001 113.947 225.658C156.184 177.315 208.919 139.273 268.117 114.442"
|
||||
stroke="currentColor"
|
||||
strokeWidth="30"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
Open in Scira
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -5,7 +5,6 @@ import { usePathname } from "next/navigation"
|
||||
|
||||
import { PAGES_NEW } from "@/lib/docs"
|
||||
import { showMcpDocs } from "@/lib/flags"
|
||||
import { getCurrentBase, getPagesFromFolder } from "@/lib/page-tree"
|
||||
import type { source } from "@/lib/source"
|
||||
import {
|
||||
Sidebar,
|
||||
@@ -19,31 +18,19 @@ import {
|
||||
} from "@/registry/new-york-v4/ui/sidebar"
|
||||
|
||||
const TOP_LEVEL_SECTIONS = [
|
||||
{ name: "Introduction", href: "/docs" },
|
||||
{ name: "Get Started", href: "/docs" },
|
||||
{
|
||||
name: "Components",
|
||||
href: "/docs/components",
|
||||
},
|
||||
{
|
||||
name: "Installation",
|
||||
href: "/docs/installation",
|
||||
},
|
||||
{
|
||||
name: "Directory",
|
||||
href: "/docs/directory",
|
||||
},
|
||||
{
|
||||
name: "RTL",
|
||||
href: "/docs/rtl",
|
||||
},
|
||||
{
|
||||
name: "MCP Server",
|
||||
href: "/docs/mcp",
|
||||
},
|
||||
{
|
||||
name: "Registry",
|
||||
href: "/docs/registry",
|
||||
},
|
||||
{
|
||||
name: "Forms",
|
||||
href: "/docs/forms",
|
||||
@@ -53,7 +40,7 @@ const TOP_LEVEL_SECTIONS = [
|
||||
href: "/docs/changelog",
|
||||
},
|
||||
]
|
||||
const EXCLUDED_SECTIONS = ["installation", "dark-mode", "changelog"]
|
||||
const EXCLUDED_SECTIONS = ["installation", "dark-mode"]
|
||||
const EXCLUDED_PAGES = ["/docs", "/docs/changelog"]
|
||||
|
||||
export function DocsSidebar({
|
||||
@@ -61,11 +48,10 @@ export function DocsSidebar({
|
||||
...props
|
||||
}: React.ComponentProps<typeof Sidebar> & { tree: typeof source.pageTree }) {
|
||||
const pathname = usePathname()
|
||||
const currentBase = getCurrentBase(pathname)
|
||||
|
||||
return (
|
||||
<Sidebar
|
||||
className="sticky top-[calc(var(--header-height)+1px)] z-30 hidden h-[calc(100svh-6rem)] overscroll-none bg-transparent lg:flex"
|
||||
className="sticky top-[calc(var(--header-height)+1px)] z-30 hidden h-[calc(100svh-var(--footer-height)-4rem)] overscroll-none bg-transparent lg:flex"
|
||||
collapsible="none"
|
||||
{...props}
|
||||
>
|
||||
@@ -95,12 +81,6 @@ export function DocsSidebar({
|
||||
<Link href={href}>
|
||||
<span className="absolute inset-0 flex w-(--sidebar-width) bg-transparent" />
|
||||
{name}
|
||||
{PAGES_NEW.includes(href) && (
|
||||
<span
|
||||
className="flex size-2 rounded-full bg-blue-500"
|
||||
title="New"
|
||||
/>
|
||||
)}
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
@@ -122,34 +102,37 @@ export function DocsSidebar({
|
||||
<SidebarGroupContent>
|
||||
{item.type === "folder" && (
|
||||
<SidebarMenu className="gap-0.5">
|
||||
{getPagesFromFolder(item, currentBase).map((page) => {
|
||||
if (!showMcpDocs && page.url.includes("/mcp")) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (EXCLUDED_PAGES.includes(page.url)) {
|
||||
{item.children.map((item) => {
|
||||
if (
|
||||
!showMcpDocs &&
|
||||
item.type === "page" &&
|
||||
item.url?.includes("/mcp")
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<SidebarMenuItem key={page.url}>
|
||||
<SidebarMenuButton
|
||||
asChild
|
||||
isActive={page.url === pathname}
|
||||
className="data-[active=true]:bg-accent data-[active=true]:border-accent 3xl:fixed:w-full 3xl:fixed:max-w-48 relative h-[30px] w-fit overflow-visible border border-transparent text-[0.8rem] font-medium after:absolute after:inset-x-0 after:-inset-y-1 after:z-0 after:rounded-md"
|
||||
>
|
||||
<Link href={page.url}>
|
||||
<span className="absolute inset-0 flex w-(--sidebar-width) bg-transparent" />
|
||||
{page.name}
|
||||
{PAGES_NEW.includes(page.url) && (
|
||||
<span
|
||||
className="flex size-2 rounded-full bg-blue-500"
|
||||
title="New"
|
||||
/>
|
||||
)}
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
item.type === "page" &&
|
||||
!EXCLUDED_PAGES.includes(item.url) && (
|
||||
<SidebarMenuItem key={item.url}>
|
||||
<SidebarMenuButton
|
||||
asChild
|
||||
isActive={item.url === pathname}
|
||||
className="data-[active=true]:bg-accent data-[active=true]:border-accent 3xl:fixed:w-full 3xl:fixed:max-w-48 relative h-[30px] w-fit overflow-visible border border-transparent text-[0.8rem] font-medium after:absolute after:inset-x-0 after:-inset-y-1 after:z-0 after:rounded-md"
|
||||
>
|
||||
<Link href={item.url}>
|
||||
<span className="absolute inset-0 flex w-(--sidebar-width) bg-transparent" />
|
||||
{item.name}
|
||||
{PAGES_NEW.includes(item.url) && (
|
||||
<span
|
||||
className="flex size-2 rounded-full bg-blue-500"
|
||||
title="New"
|
||||
/>
|
||||
)}
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
)
|
||||
)
|
||||
})}
|
||||
</SidebarMenu>
|
||||
|
||||
@@ -107,7 +107,7 @@ export function DocsTableOfContents({
|
||||
|
||||
return (
|
||||
<div className={cn("flex flex-col gap-2 p-4 pt-0 text-sm", className)}>
|
||||
<p className="text-muted-foreground bg-background sticky top-0 h-6 text-xs font-medium">
|
||||
<p className="text-muted-foreground bg-background sticky top-0 h-6 text-xs">
|
||||
On This Page
|
||||
</p>
|
||||
{toc.map((item) => (
|
||||
|
||||
@@ -1,119 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/examples/base/ui/select"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
export type Language = "en" | "ar" | "he"
|
||||
|
||||
export type Direction = "ltr" | "rtl"
|
||||
|
||||
export type Translations<
|
||||
T extends Record<string, string> = Record<string, string>,
|
||||
> = Record<
|
||||
Language,
|
||||
{
|
||||
dir: Direction
|
||||
values: T
|
||||
}
|
||||
>
|
||||
|
||||
export const languageOptions = [
|
||||
{ value: "en", label: "English" },
|
||||
{ value: "ar", label: "Arabic (العربية)" },
|
||||
{ value: "he", label: "Hebrew (עברית)" },
|
||||
] as const
|
||||
|
||||
type LanguageContextType = {
|
||||
language: Language
|
||||
setLanguage: (language: Language) => void
|
||||
}
|
||||
|
||||
const LanguageContext = React.createContext<LanguageContextType | undefined>(
|
||||
undefined
|
||||
)
|
||||
|
||||
export function LanguageProvider({
|
||||
children,
|
||||
defaultLanguage = "ar",
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
defaultLanguage?: Language
|
||||
}) {
|
||||
const [language, setLanguage] = React.useState<Language>(defaultLanguage)
|
||||
|
||||
return (
|
||||
<LanguageContext.Provider value={{ language, setLanguage }}>
|
||||
{children}
|
||||
</LanguageContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function useLanguageContext() {
|
||||
const context = React.useContext(LanguageContext)
|
||||
return context
|
||||
}
|
||||
|
||||
export function useTranslation<T extends Record<string, string>>(
|
||||
translations: Translations<T>,
|
||||
defaultLanguage: Language = "ar"
|
||||
) {
|
||||
const context = useLanguageContext()
|
||||
const [localLanguage, setLocalLanguage] =
|
||||
React.useState<Language>(defaultLanguage)
|
||||
|
||||
const language = context?.language ?? localLanguage
|
||||
const setLanguage = context?.setLanguage ?? setLocalLanguage
|
||||
|
||||
const { dir, values: t } = translations[language]
|
||||
return { language, setLanguage, dir, t }
|
||||
}
|
||||
|
||||
export interface LanguageSelectorProps {
|
||||
value: Language
|
||||
onValueChange: (value: Language) => void
|
||||
}
|
||||
|
||||
export function LanguageSelector({
|
||||
value,
|
||||
onValueChange,
|
||||
className,
|
||||
}: LanguageSelectorProps & {
|
||||
className?: string
|
||||
}) {
|
||||
return (
|
||||
<Select
|
||||
items={languageOptions}
|
||||
value={value}
|
||||
onValueChange={(value) => onValueChange(value as Language)}
|
||||
>
|
||||
<SelectTrigger
|
||||
size="sm"
|
||||
className={cn("absolute top-4 right-4 z-50 w-36", className)}
|
||||
dir="ltr"
|
||||
>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent
|
||||
dir="ltr"
|
||||
className="data-closed:animate-none data-open:animate-none"
|
||||
>
|
||||
<SelectGroup>
|
||||
{languageOptions.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)
|
||||
}
|
||||
@@ -2,11 +2,10 @@
|
||||
|
||||
import * as React from "react"
|
||||
import Link, { type LinkProps } from "next/link"
|
||||
import { usePathname, useRouter } from "next/navigation"
|
||||
import { useRouter } from "next/navigation"
|
||||
|
||||
import { PAGES_NEW } from "@/lib/docs"
|
||||
import { showMcpDocs } from "@/lib/flags"
|
||||
import { getCurrentBase, getPagesFromFolder } from "@/lib/page-tree"
|
||||
import { type source } from "@/lib/source"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
@@ -17,31 +16,19 @@ import {
|
||||
} from "@/registry/new-york-v4/ui/popover"
|
||||
|
||||
const TOP_LEVEL_SECTIONS = [
|
||||
{ name: "Introduction", href: "/docs" },
|
||||
{ name: "Get Started", href: "/docs" },
|
||||
{
|
||||
name: "Components",
|
||||
href: "/docs/components",
|
||||
},
|
||||
{
|
||||
name: "Installation",
|
||||
href: "/docs/installation",
|
||||
},
|
||||
{
|
||||
name: "Directory",
|
||||
href: "/docs/directory",
|
||||
},
|
||||
{
|
||||
name: "RTL",
|
||||
href: "/docs/rtl",
|
||||
},
|
||||
{
|
||||
name: "MCP Server",
|
||||
href: "/docs/mcp",
|
||||
},
|
||||
{
|
||||
name: "Registry",
|
||||
href: "/docs/registry",
|
||||
},
|
||||
{
|
||||
name: "Forms",
|
||||
href: "/docs/forms",
|
||||
@@ -62,8 +49,6 @@ export function MobileNav({
|
||||
className?: string
|
||||
}) {
|
||||
const [open, setOpen] = React.useState(false)
|
||||
const pathname = usePathname()
|
||||
const currentBase = getCurrentBase(pathname)
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
@@ -140,30 +125,31 @@ export function MobileNav({
|
||||
<div className="flex flex-col gap-8">
|
||||
{tree?.children?.map((group, index) => {
|
||||
if (group.type === "folder") {
|
||||
const pages = getPagesFromFolder(group, currentBase)
|
||||
return (
|
||||
<div key={index} className="flex flex-col gap-4">
|
||||
<div className="text-muted-foreground text-sm font-medium">
|
||||
{group.name}
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
{pages.map((item) => {
|
||||
if (!showMcpDocs && item.url.includes("/mcp")) {
|
||||
return null
|
||||
{group.children.map((item) => {
|
||||
if (item.type === "page") {
|
||||
if (!showMcpDocs && item.url.includes("/mcp")) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<MobileLink
|
||||
key={`${item.url}-${index}`}
|
||||
href={item.url}
|
||||
onOpenChange={setOpen}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
{item.name}{" "}
|
||||
{PAGES_NEW.includes(item.url) && (
|
||||
<span className="flex size-2 rounded-full bg-blue-500" />
|
||||
)}
|
||||
</MobileLink>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<MobileLink
|
||||
key={`${item.url}-${index}`}
|
||||
href={item.url}
|
||||
onOpenChange={setOpen}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
{item.name}{" "}
|
||||
{PAGES_NEW.includes(item.url) && (
|
||||
<span className="flex size-2 rounded-full bg-blue-500" />
|
||||
)}
|
||||
</MobileLink>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -23,7 +23,7 @@ function PageHeaderHeading({
|
||||
return (
|
||||
<h1
|
||||
className={cn(
|
||||
"text-primary leading-tighter max-w-2xl text-4xl font-semibold tracking-tight text-balance lg:leading-[1.1] lg:font-semibold xl:text-5xl xl:tracking-tighter",
|
||||
"text-primary leading-tighter max-w-2xl text-4xl font-semibold tracking-tight text-balance lg:leading-[1.1] lg:font-semibold xl:text-5xl xl:tracking-tight",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
} from "@/registry/new-york-v4/ui/input-group"
|
||||
|
||||
export const SearchDirectory = () => {
|
||||
const { query, registries, setQuery } = useSearchRegistry()
|
||||
const { query, setQuery } = useSearchRegistry()
|
||||
|
||||
const onQueryChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = e.target.value
|
||||
@@ -29,12 +29,6 @@ export const SearchDirectory = () => {
|
||||
value={query}
|
||||
onChange={onQueryChange}
|
||||
/>
|
||||
<InputGroupAddon align="inline-end">
|
||||
<span className="text-muted-foreground tabular-nums sm:text-xs">
|
||||
{registries.length}{" "}
|
||||
{registries.length === 1 ? "registry" : "registries"}
|
||||
</span>
|
||||
</InputGroupAddon>
|
||||
<InputGroupAddon
|
||||
align="inline-end"
|
||||
data-disabled={!query.length}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { siteConfig } from "@/lib/config"
|
||||
|
||||
export function SiteFooter() {
|
||||
return (
|
||||
<footer className="group-has-[.section-soft]/body:bg-surface/40 3xl:fixed:bg-transparent dark:group-has-[.section-soft]/body:bg-surface/40 group-has-[.docs-nav]/body:pb-20 group-has-[[data-slot=designer]]/body:hidden group-has-[[data-slot=docs]]/body:hidden group-has-[.docs-nav]/body:sm:pb-0 dark:bg-transparent">
|
||||
<footer className="group-has-[.section-soft]/body:bg-surface/40 3xl:fixed:bg-transparent group-has-[.docs-nav]/body:pb-20 group-has-[[data-slot=designer]]/body:hidden group-has-[.docs-nav]/body:sm:pb-0 dark:bg-transparent">
|
||||
<div className="container-wrapper px-4 xl:px-6">
|
||||
<div className="flex h-(--footer-height) items-center justify-between">
|
||||
<div className="text-muted-foreground w-full px-1 text-center text-xs leading-loose sm:text-sm">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const SHOW = true
|
||||
const SHOW = false
|
||||
|
||||
export function TailwindIndicator({
|
||||
forceMount = false,
|
||||
|
||||
1629
apps/v4/content/docs/(root)/changelog.mdx
Normal file
1629
apps/v4/content/docs/(root)/changelog.mdx
Normal file
File diff suppressed because it is too large
Load Diff
@@ -3,22 +3,12 @@ title: Registry Directory
|
||||
description: Discover community registries for shadcn/ui components and blocks.
|
||||
---
|
||||
|
||||
import { TriangleAlertIcon } from "lucide-react"
|
||||
|
||||
These registries are built into the CLI with no additional configuration required. To add a component, run: `npx shadcn add @<registry>/<component>`.
|
||||
|
||||
<Callout
|
||||
type="warning"
|
||||
className="border-amber-200 bg-amber-50 font-semibold dark:border-amber-900 dark:bg-amber-950"
|
||||
>
|
||||
Community registries are maintained by third-party developers. Always review
|
||||
code on installation to ensure it meets your security and quality standards.
|
||||
</Callout>
|
||||
<DirectoryList />
|
||||
|
||||
Don't see a registry? Learn how to [add it here](/docs/registry/registry-index).
|
||||
|
||||
<DirectoryList />
|
||||
|
||||
## Documentation
|
||||
|
||||
You can use the `shadcn` CLI to run your own code registry. Running your own registry allows you to distribute your custom components, hooks, pages, config, rules and other files to any project.
|
||||
|
||||
@@ -29,7 +29,6 @@ Select your MCP client and follow the instructions to configure the shadcn MCP s
|
||||
<TabsTrigger value="cursor">Cursor</TabsTrigger>
|
||||
<TabsTrigger value="vscode">VS Code</TabsTrigger>
|
||||
<TabsTrigger value="codex">Codex</TabsTrigger>
|
||||
<TabsTrigger value="opencode">OpenCode</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="claude" className="mt-4">
|
||||
**Run the following command** in your project:
|
||||
@@ -96,19 +95,6 @@ Select your MCP client and follow the instructions to configure the shadcn MCP s
|
||||
- Create a contact form using components from the shadcn registry
|
||||
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="opencode" className="mt-4">
|
||||
**Run the following command** in your project:
|
||||
```bash
|
||||
npx shadcn@latest mcp init --client opencode
|
||||
```
|
||||
|
||||
**Restart OpenCode** and try the following prompts:
|
||||
- Show me all available components in the shadcn registry
|
||||
- Add the button, dialog and card components to my project
|
||||
- Create a contact form using components from the shadcn registry
|
||||
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
---
|
||||
@@ -167,7 +153,7 @@ To use the shadcn MCP server with Claude Code, add the following configuration t
|
||||
|
||||
After adding the configuration, restart Claude Code and run `/mcp` to see the shadcn MCP server in the list. If you see `Connected`, you're good to go.
|
||||
|
||||
See the [Claude Code MCP documentation](https://code.claude.com/docs/en/mcp) for more details.
|
||||
See the [Claude Code MCP documentation](https://docs.anthropic.com/en/docs/claude-code/mcp) for more details.
|
||||
|
||||
### Cursor
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
"components-json",
|
||||
"theming",
|
||||
"[Dark Mode](/docs/dark-mode)",
|
||||
"[RTL](/docs/rtl)",
|
||||
"[CLI](/docs/cli)",
|
||||
"monorepo",
|
||||
"typography",
|
||||
@@ -14,7 +13,7 @@
|
||||
"javascript",
|
||||
"blocks",
|
||||
"figma",
|
||||
"[Changelog](/docs/changelog)",
|
||||
"changelog",
|
||||
"[llms.txt](/llms.txt)",
|
||||
"legacy"
|
||||
]
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
---
|
||||
title: "RTL"
|
||||
description: "RTL documentation"
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
RTL is a feature that allows you to display content from right to left.
|
||||
|
||||
## Sidebar
|
||||
|
||||
If you have an existing `sidebar.tsx` component and want to make it RTL compatible, you'll need to make the following changes:
|
||||
|
||||
### 1. Update the side prop type and default
|
||||
|
||||
```tsx
|
||||
function Sidebar({
|
||||
side = "inline-start", // [!code highlight]
|
||||
variant = "sidebar",
|
||||
collapsible = "offcanvas",
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & {
|
||||
side?: "left" | "right" | "inline-start" | "inline-end" // [!code highlight]
|
||||
variant?: "sidebar" | "floating" | "inset"
|
||||
collapsible?: "offcanvas" | "icon" | "none"
|
||||
}) {
|
||||
```
|
||||
|
||||
### 2. Update the mobile Sheet side prop
|
||||
|
||||
The Sheet component only accepts physical `left` or `right` values, so map the logical values:
|
||||
|
||||
```tsx
|
||||
<SheetContent
|
||||
side={side === "inline-start" || side === "left" ? "left" : "right"}
|
||||
// ...
|
||||
>
|
||||
```
|
||||
|
||||
### 3. Normalize the data-side attribute
|
||||
|
||||
The `data-side` attribute is used by CSS selectors, so normalize it to physical values:
|
||||
|
||||
```tsx
|
||||
<div
|
||||
data-side={side === "inline-start" || side === "left" ? "left" : "right"}
|
||||
// ...
|
||||
>
|
||||
```
|
||||
|
||||
### 4. Update the sidebar container positioning
|
||||
|
||||
Change the conditional logic to include `inline-start` and use logical positioning classes:
|
||||
|
||||
```tsx
|
||||
className={cn(
|
||||
"fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) md:flex",
|
||||
side === "left" || side === "inline-start"
|
||||
? "start-0 group-data-[collapsible=offcanvas]:start-[calc(var(--sidebar-width)*-1)]"
|
||||
: "end-0 group-data-[collapsible=offcanvas]:end-[calc(var(--sidebar-width)*-1)]",
|
||||
// ...
|
||||
)}
|
||||
```
|
||||
|
||||
### 5. Update SidebarRail classes
|
||||
|
||||
Replace physical positioning classes with logical equivalents:
|
||||
|
||||
| Before | After |
|
||||
| --------------------------------- | --------------------------------- |
|
||||
| `group-data-[side=left]:-right-4` | `group-data-[side=left]:-end-4` |
|
||||
| `group-data-[side=right]:left-0` | `group-data-[side=right]:start-0` |
|
||||
| `after:left-1/2` | `after:start-1/2` |
|
||||
| `after:left-full` | `after:start-full` |
|
||||
| `-right-2` | `-end-2` |
|
||||
| `-left-2` | `-start-2` |
|
||||
|
||||
### Usage
|
||||
|
||||
After these changes, you can use the sidebar with logical side values:
|
||||
|
||||
```tsx
|
||||
// LTR: sidebar on the left, RTL: sidebar on the right
|
||||
<Sidebar side="inline-start" />
|
||||
|
||||
// LTR: sidebar on the right, RTL: sidebar on the left
|
||||
<Sidebar side="inline-end" />
|
||||
|
||||
// Physical values still work for explicit positioning
|
||||
<Sidebar side="left" />
|
||||
<Sidebar side="right" />
|
||||
```
|
||||
@@ -108,7 +108,6 @@ Here's the list of variables available for customization:
|
||||
--accent: oklch(0.97 0 0);
|
||||
--accent-foreground: oklch(0.205 0 0);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--destructive-foreground: oklch(0.985 0 0);
|
||||
--border: oklch(0.922 0 0);
|
||||
--input: oklch(0.922 0 0);
|
||||
--ring: oklch(0.708 0 0);
|
||||
@@ -143,7 +142,6 @@ Here's the list of variables available for customization:
|
||||
--accent: oklch(0.371 0 0);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--destructive-foreground: oklch(0.985 0 0);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.556 0 0);
|
||||
@@ -220,7 +218,6 @@ For reference, here's a list of the base colors that are available.
|
||||
--accent: oklch(0.97 0 0);
|
||||
--accent-foreground: oklch(0.205 0 0);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--destructive-foreground: oklch(0.985 0 0);
|
||||
--border: oklch(0.922 0 0);
|
||||
--input: oklch(0.922 0 0);
|
||||
--ring: oklch(0.708 0 0);
|
||||
@@ -255,7 +252,6 @@ For reference, here's a list of the base colors that are available.
|
||||
--accent: oklch(0.269 0 0);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--destructive-foreground: oklch(0.985 0 0);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.556 0 0);
|
||||
@@ -299,7 +295,6 @@ For reference, here's a list of the base colors that are available.
|
||||
--accent: oklch(0.97 0.001 106.424);
|
||||
--accent-foreground: oklch(0.216 0.006 56.043);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--destructive-foreground: oklch(0.985 0 0);
|
||||
--border: oklch(0.923 0.003 48.717);
|
||||
--input: oklch(0.923 0.003 48.717);
|
||||
--ring: oklch(0.709 0.01 56.259);
|
||||
@@ -334,7 +329,6 @@ For reference, here's a list of the base colors that are available.
|
||||
--accent: oklch(0.268 0.007 34.298);
|
||||
--accent-foreground: oklch(0.985 0.001 106.423);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--destructive-foreground: oklch(0.985 0 0);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.553 0.013 58.071);
|
||||
@@ -378,7 +372,6 @@ For reference, here's a list of the base colors that are available.
|
||||
--accent: oklch(0.967 0.001 286.375);
|
||||
--accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--destructive-foreground: oklch(0.985 0 0);
|
||||
--border: oklch(0.92 0.004 286.32);
|
||||
--input: oklch(0.92 0.004 286.32);
|
||||
--ring: oklch(0.705 0.015 286.067);
|
||||
@@ -413,7 +406,6 @@ For reference, here's a list of the base colors that are available.
|
||||
--accent: oklch(0.274 0.006 286.033);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--destructive-foreground: oklch(0.985 0 0);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.552 0.016 285.938);
|
||||
@@ -457,7 +449,6 @@ For reference, here's a list of the base colors that are available.
|
||||
--accent: oklch(0.967 0.003 264.542);
|
||||
--accent-foreground: oklch(0.21 0.034 264.665);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--destructive-foreground: oklch(0.985 0 0);
|
||||
--border: oklch(0.928 0.006 264.531);
|
||||
--input: oklch(0.928 0.006 264.531);
|
||||
--ring: oklch(0.707 0.022 261.325);
|
||||
@@ -492,7 +483,6 @@ For reference, here's a list of the base colors that are available.
|
||||
--accent: oklch(0.278 0.033 256.848);
|
||||
--accent-foreground: oklch(0.985 0.002 247.839);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--destructive-foreground: oklch(0.985 0 0);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.551 0.027 264.364);
|
||||
@@ -536,7 +526,6 @@ For reference, here's a list of the base colors that are available.
|
||||
--accent: oklch(0.968 0.007 247.896);
|
||||
--accent-foreground: oklch(0.208 0.042 265.755);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--destructive-foreground: oklch(0.985 0 0);
|
||||
--border: oklch(0.929 0.013 255.508);
|
||||
--input: oklch(0.929 0.013 255.508);
|
||||
--ring: oklch(0.704 0.04 256.788);
|
||||
@@ -571,7 +560,6 @@ For reference, here's a list of the base colors that are available.
|
||||
--accent: oklch(0.279 0.041 260.031);
|
||||
--accent-foreground: oklch(0.984 0.003 247.858);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--destructive-foreground: oklch(0.985 0 0);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.551 0.027 264.364);
|
||||
|
||||
@@ -1,478 +0,0 @@
|
||||
---
|
||||
title: June 2023 - New CLI, Styles and more
|
||||
description: Complete CLI rewrite with new styles, theming options, and more.
|
||||
date: 2023-06-22
|
||||
---
|
||||
|
||||
I have a lot of updates to share with you today:
|
||||
|
||||
- [**New CLI**](#new-cli) - Rewrote the CLI from scratch. You can now add components, dependencies and configure import paths.
|
||||
- [**Theming**](#theming-with-css-variables-or-tailwind-colors) - Choose between using CSS variables or Tailwind CSS utility classes for theming.
|
||||
- [**Base color**](#base-color) - Configure the base color for your project. This will be used to generate the default color palette for your components.
|
||||
- [**React Server Components**](#react-server-components) - Opt out of using React Server Components. The CLI will automatically append or remove the `use client` directive.
|
||||
- [**Styles**](#styles) - Introducing a new concept called _Style_. A style comes with its own set of components, animations, icons and more.
|
||||
- [**Exit animations**](#exit-animations) - Added exit animations to all components.
|
||||
- [**Other updates**](#other-updates) - New `icon` button size, updated `sheet` component and more.
|
||||
- [**Updating your project**](#updating-your-project) - How to update your project to get the latest changes.
|
||||
|
||||
---
|
||||
|
||||
### New CLI
|
||||
|
||||
I've been working on a new CLI for the past few weeks. It's a complete rewrite. It comes with a lot of new features and improvements.
|
||||
|
||||
### `init`
|
||||
|
||||
```bash
|
||||
npx shadcn@latest init
|
||||
```
|
||||
|
||||
When you run the `init` command, you will be asked a few questions to configure `components.json`:
|
||||
|
||||
```txt showLineNumbers
|
||||
Which style would you like to use? › Default
|
||||
Which color would you like to use as base color? › Slate
|
||||
Where is your global CSS file? › › app/globals.css
|
||||
Do you want to use CSS variables for colors? › no / yes
|
||||
Where is your tailwind.config.js located? › tailwind.config.js
|
||||
Configure the import alias for components: › @/components
|
||||
Configure the import alias for utils: › @/lib/utils
|
||||
Are you using React Server Components? › no / yes
|
||||
```
|
||||
|
||||
This file contains all the information about your components: where to install them, the import paths, how they are styled...etc.
|
||||
|
||||
You can use this file to change the import path of a component, set a baseColor or change the styling method.
|
||||
|
||||
```json title="components.json" showLineNumbers
|
||||
{
|
||||
"style": "default",
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.ts",
|
||||
"css": "src/app/globals.css",
|
||||
"baseColor": "zinc",
|
||||
"cssVariables": true
|
||||
},
|
||||
"rsc": false,
|
||||
"aliases": {
|
||||
"utils": "~/lib/utils",
|
||||
"components": "~/components"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This means you can now use the CLI with any directory structure including `src` and `app` directories.
|
||||
|
||||
### `add`
|
||||
|
||||
```bash
|
||||
npx shadcn@latest add
|
||||
```
|
||||
|
||||
The `add` command is now much more capable. You can now add UI components but also import more complex components (coming soon).
|
||||
|
||||
The CLI will automatically resolve all components and dependencies, format them based on your custom config and add them to your project.
|
||||
|
||||
### `diff` (experimental)
|
||||
|
||||
```bash
|
||||
npx shadcn diff
|
||||
```
|
||||
|
||||
We're also introducing a new `diff` command to help you keep track of upstream updates.
|
||||
|
||||
You can use this command to see what has changed in the upstream repository and update your project accordingly.
|
||||
|
||||
Run the `diff` command to get a list of components that have updates available:
|
||||
|
||||
```bash
|
||||
npx shadcn diff
|
||||
```
|
||||
|
||||
```txt
|
||||
The following components have updates available:
|
||||
- button
|
||||
- /path/to/my-app/components/ui/button.tsx
|
||||
- toast
|
||||
- /path/to/my-app/components/ui/use-toast.ts
|
||||
- /path/to/my-app/components/ui/toaster.tsx
|
||||
```
|
||||
|
||||
Then run `diff [component]` to see the changes:
|
||||
|
||||
```bash
|
||||
npx shadcn diff alert
|
||||
```
|
||||
|
||||
```diff /pl-12/
|
||||
const alertVariants = cva(
|
||||
- "relative w-full rounded-lg border",
|
||||
+ "relative w-full pl-12 rounded-lg border"
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Theming with CSS Variables or Tailwind Colors
|
||||
|
||||
You can choose between using CSS variables or Tailwind CSS utility classes for theming.
|
||||
|
||||
When you add new components, the CLI will automatically use the correct theming methods based on your `components.json` configuration.
|
||||
|
||||
#### Utility classes
|
||||
|
||||
```tsx /bg-zinc-950/ /text-zinc-50/ /dark:bg-white/ /dark:text-zinc-950/
|
||||
<div className="bg-zinc-950 dark:bg-white" />
|
||||
```
|
||||
|
||||
To use utility classes for theming set `tailwind.cssVariables` to `false` in your `components.json` file.
|
||||
|
||||
```json {6} title="components.json" showLineNumbers
|
||||
{
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.js",
|
||||
"css": "app/globals.css",
|
||||
"baseColor": "slate",
|
||||
"cssVariables": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### CSS Variables
|
||||
|
||||
```tsx /bg-background/ /text-foreground/
|
||||
<div className="bg-background text-foreground" />
|
||||
```
|
||||
|
||||
To use CSS variables classes for theming set `tailwind.cssVariables` to `true` in your `components.json` file.
|
||||
|
||||
```json {6} title="components.json" showLineNumbers
|
||||
{
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.js",
|
||||
"css": "app/globals.css",
|
||||
"baseColor": "slate",
|
||||
"cssVariables": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Base color
|
||||
|
||||
You can now configure the base color for your project. This will be used to generate the default color palette for your components.
|
||||
|
||||
```json {5} title="components.json" showLineNumbers
|
||||
{
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.js",
|
||||
"css": "app/globals.css",
|
||||
"baseColor": "zinc",
|
||||
"cssVariables": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Choose between `gray`, `neutral`, `slate`, `stone` or `zinc`.
|
||||
|
||||
If you have `cssVariables` set to `true`, we will set the base colors as CSS variables in your `globals.css` file. If you have `cssVariables` set to `false`, we will inline the Tailwind CSS utility classes in your components.
|
||||
|
||||
---
|
||||
|
||||
### React Server Components
|
||||
|
||||
If you're using a framework that does not support React Server Components, you can now opt out by setting `rsc` to `false`. We will automatically append or remove the `use client` directive when adding components.
|
||||
|
||||
```json title="components.json" showLineNumbers
|
||||
{
|
||||
"rsc": false
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Styles
|
||||
|
||||
We are introducing a new concept called _Style_.
|
||||
|
||||
_You can think of style as the visual foundation: shapes, icons, animations & typography._ A style comes with its own set of components, animations, icons and more.
|
||||
|
||||
We are shipping two styles: `default` and `new-york` (with more coming soon).
|
||||
|
||||
<Image
|
||||
src="/images/style.jpg"
|
||||
width="716"
|
||||
height="402"
|
||||
alt="Default vs New York style"
|
||||
className="mt-6 overflow-hidden rounded-lg border"
|
||||
/>
|
||||
|
||||
The `default` style is the one you are used to. It's the one we've been using since the beginning of this project. It uses `lucide-react` for icons and `tailwindcss-animate` for animations.
|
||||
|
||||
The `new-york` style is a new style. It ships with smaller buttons, cards with shadows and a new set of icons from [Radix Icons](https://icons.radix-ui.com).
|
||||
|
||||
When you run the `init` command, you will be asked which style you would like to use. This is saved in your `components.json` file.
|
||||
|
||||
```json title="components.json" showLineNumbers
|
||||
{
|
||||
"style": "new-york"
|
||||
}
|
||||
```
|
||||
|
||||
### Theming
|
||||
|
||||
Start with a style as the base then theme using CSS variables or Tailwind CSS utility classes to completely change the look of your components.
|
||||
|
||||
<Image
|
||||
src="/images/style-with-theming.jpg"
|
||||
width="716"
|
||||
height="402"
|
||||
alt="Style with theming"
|
||||
className="mt-6 overflow-hidden rounded-lg border"
|
||||
/>
|
||||
|
||||
---
|
||||
|
||||
### Exit animations
|
||||
|
||||
I added exit animations to all components. Click on the combobox below to see the subtle exit animation.
|
||||
|
||||
<ComponentPreview name="combobox-demo" className="[&_.preview]:items-start" />
|
||||
|
||||
The animations can be customized using utility classes.
|
||||
|
||||
---
|
||||
|
||||
### Other updates
|
||||
|
||||
### Button
|
||||
|
||||
- Added a new button size `icon`:
|
||||
|
||||
<ComponentPreview name="button-icon" />
|
||||
|
||||
### Sheet
|
||||
|
||||
- Renamed `position` to `side` to match the other elements.
|
||||
|
||||
<ComponentPreview name="sheet-side" />
|
||||
|
||||
- Removed the `size` props. Use `className="w-[200px] md:w-[450px]"` for responsive sizing.
|
||||
|
||||
---
|
||||
|
||||
### Updating your project
|
||||
|
||||
Since we follow a copy and paste approach, you will need to manually update your project to get the latest changes.
|
||||
|
||||
<Callout className="mt-4">
|
||||
Note: we are working on a [`diff`](#diff-experimental) command to help you
|
||||
keep track of upstream updates.
|
||||
</Callout>
|
||||
|
||||
<Steps>
|
||||
|
||||
### Add `components.json`
|
||||
|
||||
Creating a `components.json` file at the root:
|
||||
|
||||
```json title="components.json" showLineNumbers
|
||||
{
|
||||
"style": "default",
|
||||
"rsc": true,
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.js",
|
||||
"css": "app/globals.css",
|
||||
"baseColor": "slate",
|
||||
"cssVariables": true
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Update the values for `tailwind.css` and `aliases` to match your project structure.
|
||||
|
||||
### Button
|
||||
|
||||
Add the `icon` size to the `buttonVariants`:
|
||||
|
||||
```tsx {7} title="components/ui/button.tsx" showLineNumbers
|
||||
const buttonVariants = cva({
|
||||
variants: {
|
||||
size: {
|
||||
default: "h-10 px-4 py-2",
|
||||
sm: "h-9 rounded-md px-3",
|
||||
lg: "h-11 rounded-md px-8",
|
||||
icon: "h-10 w-10",
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
### Sheet
|
||||
|
||||
1. Replace the content of `sheet.tsx` with the following:
|
||||
|
||||
```tsx title="components/ui/sheet.tsx" showLineNumbers
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as SheetPrimitive from "@radix-ui/react-dialog"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { X } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Sheet = SheetPrimitive.Root
|
||||
|
||||
const SheetTrigger = SheetPrimitive.Trigger
|
||||
|
||||
const SheetClose = SheetPrimitive.Close
|
||||
|
||||
const SheetPortal = ({
|
||||
className,
|
||||
...props
|
||||
}: SheetPrimitive.DialogPortalProps) => (
|
||||
<SheetPrimitive.Portal className={cn(className)} {...props} />
|
||||
)
|
||||
SheetPortal.displayName = SheetPrimitive.Portal.displayName
|
||||
|
||||
const SheetOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Overlay
|
||||
className={cn(
|
||||
"bg-background/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 backdrop-blur-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
))
|
||||
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
|
||||
|
||||
const sheetVariants = cva(
|
||||
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
|
||||
{
|
||||
variants: {
|
||||
side: {
|
||||
top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
|
||||
bottom:
|
||||
"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
|
||||
left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
|
||||
right:
|
||||
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
side: "right",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
interface SheetContentProps
|
||||
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
|
||||
VariantProps<typeof sheetVariants> {}
|
||||
|
||||
const SheetContent = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Content>,
|
||||
SheetContentProps
|
||||
>(({ side = "right", className, children, ...props }, ref) => (
|
||||
<SheetPortal>
|
||||
<SheetOverlay />
|
||||
<SheetPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(sheetVariants({ side }), className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-none disabled:pointer-events-none">
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</SheetPrimitive.Close>
|
||||
</SheetPrimitive.Content>
|
||||
</SheetPortal>
|
||||
))
|
||||
SheetContent.displayName = SheetPrimitive.Content.displayName
|
||||
|
||||
const SheetHeader = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col space-y-2 text-center sm:text-left",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
SheetHeader.displayName = "SheetHeader"
|
||||
|
||||
const SheetFooter = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
SheetFooter.displayName = "SheetFooter"
|
||||
|
||||
const SheetTitle = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn("text-foreground text-lg font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SheetTitle.displayName = SheetPrimitive.Title.displayName
|
||||
|
||||
const SheetDescription = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SheetDescription.displayName = SheetPrimitive.Description.displayName
|
||||
|
||||
export {
|
||||
Sheet,
|
||||
SheetTrigger,
|
||||
SheetClose,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetFooter,
|
||||
SheetTitle,
|
||||
SheetDescription,
|
||||
}
|
||||
```
|
||||
|
||||
2. Rename `position` to `side`
|
||||
|
||||
```diff /position/ /side/
|
||||
- <Sheet position="right" />
|
||||
+ <Sheet side="right" />
|
||||
```
|
||||
|
||||
</Steps>
|
||||
|
||||
### Thank you
|
||||
|
||||
I'd like to thank everyone who has been using this project, providing feedback and contributing to it. I really appreciate it. Thank you.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user