mirror of
https://github.com/shadcn-ui/ui.git
synced 2026-06-22 20:25:44 +00:00
Compare commits
60 Commits
shadcn@2.6
...
shadcn@2.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0940c6aec7 | ||
|
|
e244952500 | ||
|
|
0e3d6b24d3 | ||
|
|
cef5af9ed3 | ||
|
|
6deb0fdbb6 | ||
|
|
e9ae79f874 | ||
|
|
d891132f2a | ||
|
|
873f7f2773 | ||
|
|
e6778dee87 | ||
|
|
97a8de1c1b | ||
|
|
19d7fbb731 | ||
|
|
a9ab05ad83 | ||
|
|
6ac114ae68 | ||
|
|
d5770e4350 | ||
|
|
4730276256 | ||
|
|
4e04567b07 | ||
|
|
6f63b04d28 | ||
|
|
e38228b574 | ||
|
|
8807103586 | ||
|
|
3424ab709e | ||
|
|
4a86a55cac | ||
|
|
2926574d0e | ||
|
|
20e913d8e1 | ||
|
|
3433aaffaa | ||
|
|
d9cdc3f7ae | ||
|
|
e75e7b3866 | ||
|
|
ed5237c231 | ||
|
|
f85ca066dc | ||
|
|
54e66d4450 | ||
|
|
6c341c16ae | ||
|
|
06d03d64f4 | ||
|
|
6407a3b330 | ||
|
|
96b15f6090 | ||
|
|
2fe9cf6d26 | ||
|
|
728cb4cfa5 | ||
|
|
db93787712 | ||
|
|
1cdd6c1645 | ||
|
|
4983c6e1f4 | ||
|
|
7443edcfb0 | ||
|
|
9d9a33be52 | ||
|
|
d544a7f7a5 | ||
|
|
48fe0d709f | ||
|
|
ed244ea0b5 | ||
|
|
b8fede1742 | ||
|
|
84d6c83bad | ||
|
|
5b8ee41511 | ||
|
|
7c3d34cdc9 | ||
|
|
56c4c83511 | ||
|
|
2821cb0e39 | ||
|
|
3c87402de2 | ||
|
|
20a88e1f15 | ||
|
|
cb19ab8464 | ||
|
|
cf1851ca09 | ||
|
|
c86c27a2ff | ||
|
|
8847126c65 | ||
|
|
65350857a4 | ||
|
|
40c7473c7e | ||
|
|
4698ee960f | ||
|
|
2ae0e5a07b | ||
|
|
431af4f7ff |
@@ -7,5 +7,5 @@
|
|||||||
"access": "public",
|
"access": "public",
|
||||||
"baseBranch": "main",
|
"baseBranch": "main",
|
||||||
"updateInternalDependencies": "patch",
|
"updateInternalDependencies": "patch",
|
||||||
"ignore": ["www", "v4"]
|
"ignore": ["www", "v4", "tests"]
|
||||||
}
|
}
|
||||||
|
|||||||
9
.claude/settings.local.json
Normal file
9
.claude/settings.local.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(npm test:*)",
|
||||||
|
"Bash(npm run typecheck:*)"
|
||||||
|
],
|
||||||
|
"deny": []
|
||||||
|
}
|
||||||
|
}
|
||||||
20
.github/workflows/issue-stale.yml
vendored
20
.github/workflows/issue-stale.yml
vendored
@@ -18,15 +18,15 @@ jobs:
|
|||||||
repo-token: ${{ secrets.STALE_TOKEN }}
|
repo-token: ${{ secrets.STALE_TOKEN }}
|
||||||
ascending: true
|
ascending: true
|
||||||
days-before-issue-close: 7
|
days-before-issue-close: 7
|
||||||
days-before-issue-stale: 365 # ~2 years
|
days-before-issue-stale: 365
|
||||||
days-before-pr-stale: -1
|
days-before-pr-stale: -1
|
||||||
days-before-pr-close: -1
|
days-before-pr-close: -1
|
||||||
remove-issue-stale-when-updated: true
|
remove-issue-stale-when-updated: true
|
||||||
stale-issue-label: "stale?"
|
stale-issue-label: "stale?"
|
||||||
exempt-issue-labels: "roadmap,next,bug"
|
exempt-issue-labels: "roadmap,next"
|
||||||
stale-issue-message: "This issue has been automatically marked as stale due to one year of inactivity. It will be closed in 7 days unless there’s further input. If you believe this issue is still relevant, please leave a comment or provide updated details. Thank you."
|
stale-issue-message: "This issue has been automatically marked as stale due to one year of inactivity. It will be closed in 7 days unless there’s further input. If you believe this issue is still relevant, please leave a comment or provide updated details. Thank you. (This is an automated message)"
|
||||||
close-issue-message: "This issue has been automatically closed due to one year of inactivity. If you’re still experiencing a similar problem or have additional details to share, please open a new issue following our current issue template. Your updated report helps us investigate and address concerns more efficiently. Thank you for your understanding!"
|
close-issue-message: "This issue has been automatically closed due to one year of inactivity. If you’re still experiencing a similar problem or have additional details to share, please open a new issue following our current issue template. Your updated report helps us investigate and address concerns more efficiently. Thank you for your understanding! (This is an automated message)"
|
||||||
operations-per-run: 300 # 1 operation per 100 issues, the rest is to label/comment/close
|
operations-per-run: 300
|
||||||
- uses: actions/stale@v9
|
- uses: actions/stale@v9
|
||||||
id: pr-state
|
id: pr-state
|
||||||
name: "Mark stale PRs, close stale PRs"
|
name: "Mark stale PRs, close stale PRs"
|
||||||
@@ -36,10 +36,10 @@ jobs:
|
|||||||
days-before-issue-close: -1
|
days-before-issue-close: -1
|
||||||
days-before-issue-stale: -1
|
days-before-issue-stale: -1
|
||||||
days-before-pr-close: 7
|
days-before-pr-close: 7
|
||||||
days-before-pr-stale: 365 # PRs with no activity in over 90 days will be marked as stale
|
days-before-pr-stale: 365
|
||||||
remove-pr-stale-when-updated: true
|
remove-pr-stale-when-updated: true
|
||||||
exempt-pr-labels: "roadmap,nex,awaiting-approval,work-in-progress"
|
exempt-pr-labels: "roadmap,next,bug"
|
||||||
stale-pr-label: "stale?"
|
stale-pr-label: "stale?"
|
||||||
stale-pr-message: "This PR has been automatically marked as stale due to one year of inactivity. It will be closed in 7 days unless there’s further input. If you believe this PR is still relevant, please leave a comment or provide updated details. Thank you."
|
stale-pr-message: "This PR has been automatically marked as stale due to one year of inactivity. It will be closed in 7 days unless there’s further input. If you believe this PR is still relevant, please leave a comment or provide updated details. Thank you. (This is an automated message)"
|
||||||
close-pr-message: "This PR has been automatically closed due to one year of inactivity. Thank you for your understanding!"
|
close-pr-message: "This PR has been automatically closed due to one year of inactivity. Thank you for your understanding! (This is an automated message)"
|
||||||
operations-per-run: 300 # 1 operation per 100 issues, the rest is to label/comment/close
|
operations-per-run: 300
|
||||||
|
|||||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -41,7 +41,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Create Version PR or Publish to NPM
|
- name: Create Version PR or Publish to NPM
|
||||||
id: changesets
|
id: changesets
|
||||||
uses: changesets/action@v1.4.1
|
uses: changesets/action@v1
|
||||||
with:
|
with:
|
||||||
commit: "chore(release): version packages"
|
commit: "chore(release): version packages"
|
||||||
title: "chore(release): version packages"
|
title: "chore(release): version packages"
|
||||||
|
|||||||
6
.github/workflows/test.yml
vendored
6
.github/workflows/test.yml
vendored
@@ -8,6 +8,9 @@ jobs:
|
|||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
name: pnpm test
|
name: pnpm test
|
||||||
|
env:
|
||||||
|
NEXT_PUBLIC_APP_URL: http://localhost:4000
|
||||||
|
NEXT_PUBLIC_V0_URL: https://v0.dev
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
@@ -39,4 +42,7 @@ jobs:
|
|||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install
|
run: pnpm install
|
||||||
|
|
||||||
|
- name: Build packages
|
||||||
|
run: pnpm build --filter=shadcn
|
||||||
|
|
||||||
- run: pnpm test
|
- run: pnpm test
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ To run the CLI locally, you can follow the workflow:
|
|||||||
1. Start by running the registry (main site) to make sure the components are up to date:
|
1. Start by running the registry (main site) to make sure the components are up to date:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pnpm www:dev
|
pnpm v4:dev
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Run the development script for the CLI:
|
2. Run the development script for the CLI:
|
||||||
|
|||||||
2
apps/v4/.env.example
Normal file
2
apps/v4/.env.example
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
NEXT_PUBLIC_V0_URL=https://v0.dev
|
||||||
|
NEXT_PUBLIC_APP_URL=http://localhost:4000
|
||||||
1
apps/v4/.gitignore
vendored
1
apps/v4/.gitignore
vendored
@@ -32,6 +32,7 @@ yarn-error.log*
|
|||||||
|
|
||||||
# env files (can opt-in for committing if needed)
|
# env files (can opt-in for committing if needed)
|
||||||
.env*
|
.env*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
# vercel
|
# vercel
|
||||||
.vercel
|
.vercel
|
||||||
|
|||||||
@@ -15,9 +15,9 @@ import { PageNav } from "@/components/page-nav"
|
|||||||
import { ThemeSelector } from "@/components/theme-selector"
|
import { ThemeSelector } from "@/components/theme-selector"
|
||||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||||
|
|
||||||
const title = "Build your Component Library"
|
const title = "The Foundation for your Design System"
|
||||||
const description =
|
const description =
|
||||||
"A set of beautifully-designed, accessible components and a code distribution platform. Works with your favorite frameworks. Open Source. Open Code."
|
"A set of beautifully designed components that you can customize, extend, and build on. Start here then make it your own. Open Source. Open Code."
|
||||||
|
|
||||||
export const dynamic = "force-static"
|
export const dynamic = "force-static"
|
||||||
export const revalidate = false
|
export const revalidate = false
|
||||||
@@ -51,14 +51,14 @@ export default function IndexPage() {
|
|||||||
<div className="flex flex-1 flex-col">
|
<div className="flex flex-1 flex-col">
|
||||||
<PageHeader>
|
<PageHeader>
|
||||||
<Announcement />
|
<Announcement />
|
||||||
<PageHeaderHeading>{title}</PageHeaderHeading>
|
<PageHeaderHeading className="max-w-4xl">{title}</PageHeaderHeading>
|
||||||
<PageHeaderDescription>{description}</PageHeaderDescription>
|
<PageHeaderDescription>{description}</PageHeaderDescription>
|
||||||
<PageActions>
|
<PageActions>
|
||||||
<Button asChild size="sm">
|
<Button asChild size="sm">
|
||||||
<Link href="/docs/installation">Get Started</Link>
|
<Link href="/docs/installation">Get Started</Link>
|
||||||
</Button>
|
</Button>
|
||||||
<Button asChild size="sm" variant="ghost">
|
<Button asChild size="sm" variant="ghost">
|
||||||
<Link href="/blocks">Browse Blocks</Link>
|
<Link href="/docs/components">View Components</Link>
|
||||||
</Button>
|
</Button>
|
||||||
</PageActions>
|
</PageActions>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { findNeighbour } from "fumadocs-core/server"
|
|||||||
|
|
||||||
import { source } from "@/lib/source"
|
import { source } from "@/lib/source"
|
||||||
import { absoluteUrl } from "@/lib/utils"
|
import { absoluteUrl } from "@/lib/utils"
|
||||||
|
import { DocsCopyPage } from "@/components/docs-copy-page"
|
||||||
import { DocsTableOfContents } from "@/components/docs-toc"
|
import { DocsTableOfContents } from "@/components/docs-toc"
|
||||||
import { OpenInV0Cta } from "@/components/open-in-v0-cta"
|
import { OpenInV0Cta } from "@/components/open-in-v0-cta"
|
||||||
import { Badge } from "@/registry/new-york-v4/ui/badge"
|
import { Badge } from "@/registry/new-york-v4/ui/badge"
|
||||||
@@ -102,12 +103,17 @@ export default async function Page(props: {
|
|||||||
<h1 className="scroll-m-20 text-4xl font-semibold tracking-tight sm:text-3xl xl:text-4xl">
|
<h1 className="scroll-m-20 text-4xl font-semibold tracking-tight sm:text-3xl xl:text-4xl">
|
||||||
{doc.title}
|
{doc.title}
|
||||||
</h1>
|
</h1>
|
||||||
<div className="flex items-center gap-2 pt-1.5">
|
<div className="docs-nav bg-background/80 border-border/50 fixed inset-x-0 bottom-0 isolate z-50 flex items-center gap-2 border-t px-6 py-4 backdrop-blur-sm sm:static sm:z-0 sm:border-t-0 sm:bg-transparent sm:px-0 sm:pt-1.5 sm:backdrop-blur-none">
|
||||||
|
<DocsCopyPage
|
||||||
|
// @ts-expect-error - revisit fumadocs types.
|
||||||
|
page={doc.content}
|
||||||
|
url={absoluteUrl(page.url)}
|
||||||
|
/>
|
||||||
{neighbours.previous && (
|
{neighbours.previous && (
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="extend-touch-target size-8 shadow-none md:size-7"
|
className="extend-touch-target ml-auto size-8 shadow-none md:size-7"
|
||||||
asChild
|
asChild
|
||||||
>
|
>
|
||||||
<Link href={neighbours.previous.url}>
|
<Link href={neighbours.previous.url}>
|
||||||
@@ -160,7 +166,7 @@ export default async function Page(props: {
|
|||||||
<MDX components={mdxComponents} />
|
<MDX components={mdxComponents} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mx-auto flex h-16 w-full max-w-2xl items-center gap-2 px-4 md:px-0">
|
<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 && (
|
{neighbours.previous && (
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ export function DataTable<TData, TValue>({
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<DataTableToolbar table={table} />
|
<DataTableToolbar table={table} />
|
||||||
<div className="rounded-md border">
|
<div className="overflow-hidden rounded-md border">
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
{table.getHeaderGroups().map((headerGroup) => (
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
|
|||||||
29
apps/v4/app/(app)/llm/[[...slug]]/route.ts
Normal file
29
apps/v4/app/(app)/llm/[[...slug]]/route.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { notFound } from "next/navigation"
|
||||||
|
import { NextResponse, type NextRequest } from "next/server"
|
||||||
|
|
||||||
|
import { source } from "@/lib/source"
|
||||||
|
|
||||||
|
export const revalidate = false
|
||||||
|
|
||||||
|
export async function GET(
|
||||||
|
_req: NextRequest,
|
||||||
|
{ params }: { params: Promise<{ slug: string[] }> }
|
||||||
|
) {
|
||||||
|
const slug = (await params).slug
|
||||||
|
const page = source.getPage(slug)
|
||||||
|
|
||||||
|
if (!page) {
|
||||||
|
notFound()
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-expect-error - revisit fumadocs types.
|
||||||
|
return new NextResponse(page.data.content, {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "text/markdown; charset=utf-8",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateStaticParams() {
|
||||||
|
return source.generateParams()
|
||||||
|
}
|
||||||
@@ -15,7 +15,7 @@ export function Callout({
|
|||||||
return (
|
return (
|
||||||
<Alert
|
<Alert
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-surface text-surface-foreground mt-6 w-auto border-none md:-mx-4",
|
"bg-surface text-surface-foreground mt-6 w-auto border-none md:-mx-1",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -209,7 +209,7 @@ export function CardsPayments() {
|
|||||||
</CardAction>
|
</CardAction>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-col gap-4">
|
<CardContent className="flex flex-col gap-4">
|
||||||
<div className="rounded-md border">
|
<div className="overflow-hidden rounded-md border">
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
{table.getHeaderGroups().map((headerGroup) => (
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export function CodeCollapsibleWrapper({
|
|||||||
<Collapsible
|
<Collapsible
|
||||||
open={isOpened}
|
open={isOpened}
|
||||||
onOpenChange={setIsOpened}
|
onOpenChange={setIsOpened}
|
||||||
className={cn("group/collapsible relative md:-mx-4", className)}
|
className={cn("group/collapsible relative md:-mx-1", className)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<CollapsibleTrigger asChild>
|
<CollapsibleTrigger asChild>
|
||||||
|
|||||||
@@ -36,11 +36,13 @@ export function CommandMenu({
|
|||||||
tree,
|
tree,
|
||||||
colors,
|
colors,
|
||||||
blocks,
|
blocks,
|
||||||
|
navItems,
|
||||||
...props
|
...props
|
||||||
}: DialogProps & {
|
}: DialogProps & {
|
||||||
tree: typeof source.pageTree
|
tree: typeof source.pageTree
|
||||||
colors: ColorPalette[]
|
colors: ColorPalette[]
|
||||||
blocks?: { name: string; description: string; categories: string[] }[]
|
blocks?: { name: string; description: string; categories: string[] }[]
|
||||||
|
navItems?: { href: string; label: string }[]
|
||||||
}) {
|
}) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const isMac = useIsMac()
|
const isMac = useIsMac()
|
||||||
@@ -162,12 +164,45 @@ export function CommandMenu({
|
|||||||
<DialogTitle>Search documentation...</DialogTitle>
|
<DialogTitle>Search documentation...</DialogTitle>
|
||||||
<DialogDescription>Search for a command to run...</DialogDescription>
|
<DialogDescription>Search for a command to run...</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<Command className="**:data-[slot=command-input-wrapper]:bg-input/50 **:data-[slot=command-input-wrapper]:border-input rounded-none bg-transparent **:data-[slot=command-input]:!h-9 **:data-[slot=command-input]:py-0 **:data-[slot=command-input-wrapper]:mb-0 **:data-[slot=command-input-wrapper]:!h-9 **:data-[slot=command-input-wrapper]:rounded-md **:data-[slot=command-input-wrapper]:border">
|
<Command
|
||||||
|
className="**:data-[slot=command-input-wrapper]:bg-input/50 **:data-[slot=command-input-wrapper]:border-input rounded-none bg-transparent **:data-[slot=command-input]:!h-9 **:data-[slot=command-input]:py-0 **:data-[slot=command-input-wrapper]:mb-0 **:data-[slot=command-input-wrapper]:!h-9 **:data-[slot=command-input-wrapper]:rounded-md **:data-[slot=command-input-wrapper]:border"
|
||||||
|
filter={(value, search, keywords) => {
|
||||||
|
const extendValue = value + " " + (keywords?.join(" ") || "")
|
||||||
|
if (extendValue.toLowerCase().includes(search.toLowerCase())) {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}}
|
||||||
|
>
|
||||||
<CommandInput placeholder="Search documentation..." />
|
<CommandInput placeholder="Search documentation..." />
|
||||||
<CommandList className="no-scrollbar min-h-80 scroll-pt-2 scroll-pb-1.5">
|
<CommandList className="no-scrollbar min-h-80 scroll-pt-2 scroll-pb-1.5">
|
||||||
<CommandEmpty className="text-muted-foreground py-12 text-center text-sm">
|
<CommandEmpty className="text-muted-foreground py-12 text-center text-sm">
|
||||||
No results found.
|
No results found.
|
||||||
</CommandEmpty>
|
</CommandEmpty>
|
||||||
|
{navItems && navItems.length > 0 && (
|
||||||
|
<CommandGroup
|
||||||
|
heading="Pages"
|
||||||
|
className="!p-0 [&_[cmdk-group-heading]]:scroll-mt-16 [&_[cmdk-group-heading]]:!p-3 [&_[cmdk-group-heading]]:!pb-1"
|
||||||
|
>
|
||||||
|
{navItems.map((item) => (
|
||||||
|
<CommandMenuItem
|
||||||
|
key={item.href}
|
||||||
|
value={`Navigation ${item.label}`}
|
||||||
|
keywords={["nav", "navigation", item.label.toLowerCase()]}
|
||||||
|
onHighlight={() => {
|
||||||
|
setSelectedType("page")
|
||||||
|
setCopyPayload("")
|
||||||
|
}}
|
||||||
|
onSelect={() => {
|
||||||
|
runCommand(() => router.push(item.href))
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconArrowRight />
|
||||||
|
{item.label}
|
||||||
|
</CommandMenuItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
)}
|
||||||
{tree.children.map((group) => (
|
{tree.children.map((group) => (
|
||||||
<CommandGroup
|
<CommandGroup
|
||||||
key={group.$id}
|
key={group.$id}
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export function ComponentPreviewTabs({
|
|||||||
</Tabs>
|
</Tabs>
|
||||||
<div
|
<div
|
||||||
data-tab={tab}
|
data-tab={tab}
|
||||||
className="data-[tab=code]:border-code relative rounded-lg border md:-mx-4"
|
className="data-[tab=code]:border-code relative rounded-lg border md:-mx-1"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
data-slot="preview"
|
data-slot="preview"
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export function ComponentPreview({
|
|||||||
|
|
||||||
if (type === "block") {
|
if (type === "block") {
|
||||||
return (
|
return (
|
||||||
<div className="relative aspect-[4/2.5] w-full overflow-hidden rounded-md border md:-mx-4">
|
<div className="relative aspect-[4/2.5] w-full overflow-hidden rounded-md border md:-mx-1">
|
||||||
<Image
|
<Image
|
||||||
src={`/r/styles/new-york-v4/${name}-light.png`}
|
src={`/r/styles/new-york-v4/${name}-light.png`}
|
||||||
alt={name}
|
alt={name}
|
||||||
|
|||||||
@@ -1,33 +1,156 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { IconCheck, IconCopy } from "@tabler/icons-react"
|
import { IconCheck, IconChevronDown, IconCopy } from "@tabler/icons-react"
|
||||||
|
|
||||||
import { useCopyToClipboard } from "@/hooks/use-copy-to-clipboard"
|
import { useCopyToClipboard } from "@/hooks/use-copy-to-clipboard"
|
||||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
DropdownMenu,
|
||||||
TooltipContent,
|
DropdownMenuContent,
|
||||||
TooltipTrigger,
|
DropdownMenuItem,
|
||||||
} from "@/registry/new-york-v4/ui/tooltip"
|
DropdownMenuTrigger,
|
||||||
|
} from "@/registry/new-york-v4/ui/dropdown-menu"
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverAnchor,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from "@/registry/new-york-v4/ui/popover"
|
||||||
|
import { Separator } from "@/registry/new-york-v4/ui/separator"
|
||||||
|
|
||||||
export function DocsCopyPage({ page }: { page: string }) {
|
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.
|
||||||
|
`
|
||||||
|
)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const menuItems = {
|
||||||
|
markdown: (url: string) => (
|
||||||
|
<a href={`${url}.mdx`} target="_blank" rel="noopener noreferrer">
|
||||||
|
<svg strokeLinejoin="round" viewBox="0 0 22 16">
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M19.5 2.25H2.5C1.80964 2.25 1.25 2.80964 1.25 3.5V12.5C1.25 13.1904 1.80964 13.75 2.5 13.75H19.5C20.1904 13.75 20.75 13.1904 20.75 12.5V3.5C20.75 2.80964 20.1904 2.25 19.5 2.25ZM2.5 1C1.11929 1 0 2.11929 0 3.5V12.5C0 13.8807 1.11929 15 2.5 15H19.5C20.8807 15 22 13.8807 22 12.5V3.5C22 2.11929 20.8807 1 19.5 1H2.5ZM3 4.5H4H4.25H4.6899L4.98715 4.82428L7 7.02011L9.01285 4.82428L9.3101 4.5H9.75H10H11V5.5V11.5H9V7.79807L7.73715 9.17572L7 9.97989L6.26285 9.17572L5 7.79807V11.5H3V5.5V4.5ZM15 8V4.5H17V8H19.5L17 10.5L16 11.5L15 10.5L12.5 8H15Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
View as Markdown
|
||||||
|
</a>
|
||||||
|
),
|
||||||
|
v0: (url: string) => (
|
||||||
|
<a
|
||||||
|
href={getPromptUrl("https://v0.dev", url)}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 147 70"
|
||||||
|
className="size-4.5 -translate-x-px"
|
||||||
|
>
|
||||||
|
<path d="M56 50.203V14h14v46.156C70 65.593 65.593 70 60.156 70c-2.596 0-5.158-1-7-2.843L0 14h19.797L56 50.203ZM147 56h-14V23.953L100.953 56H133v14H96.687C85.814 70 77 61.186 77 50.312V14h14v32.156L123.156 14H91V0h36.312C138.186 0 147 8.814 147 19.688V56Z" />
|
||||||
|
</svg>
|
||||||
|
<span className="-translate-x-[2px]">Open in v0</span>
|
||||||
|
</a>
|
||||||
|
),
|
||||||
|
chatgpt: (url: string) => (
|
||||||
|
<a
|
||||||
|
href={getPromptUrl("https://chatgpt.com", url)}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
d="M22.282 9.821a5.985 5.985 0 0 0-.516-4.91 6.046 6.046 0 0 0-6.51-2.9A6.065 6.065 0 0 0 4.981 4.18a5.985 5.985 0 0 0-3.998 2.9 6.046 6.046 0 0 0 .743 7.097 5.98 5.98 0 0 0 .51 4.911 6.051 6.051 0 0 0 6.515 2.9A5.985 5.985 0 0 0 13.26 24a6.056 6.056 0 0 0 5.772-4.206 5.99 5.99 0 0 0 3.997-2.9 6.056 6.056 0 0 0-.747-7.073zM13.26 22.43a4.476 4.476 0 0 1-2.876-1.04l.141-.081 4.779-2.758a.795.795 0 0 0 .392-.681v-6.737l2.02 1.168a.071.071 0 0 1 .038.052v5.583a4.504 4.504 0 0 1-4.494 4.494zM3.6 18.304a4.47 4.47 0 0 1-.535-3.014l.142.085 4.783 2.759a.771.771 0 0 0 .78 0l5.843-3.369v2.332a.08.08 0 0 1-.033.062L9.74 19.95a4.5 4.5 0 0 1-6.14-1.646zM2.34 7.896a4.485 4.485 0 0 1 2.366-1.973V11.6a.766.766 0 0 0 .388.676l5.815 3.355-2.02 1.168a.076.076 0 0 1-.071 0l-4.83-2.786A4.504 4.504 0 0 1 2.34 7.872zm16.597 3.855-5.833-3.387L15.119 7.2a.076.076 0 0 1 .071 0l4.83 2.791a4.494 4.494 0 0 1-.676 8.105v-5.678a.79.79 0 0 0-.407-.667zm2.01-3.023-.141-.085-4.774-2.782a.776.776 0 0 0-.785 0L9.409 9.23V6.897a.066.066 0 0 1 .028-.061l4.83-2.787a4.5 4.5 0 0 1 6.68 4.66zm-12.64 4.135-2.02-1.164a.08.08 0 0 1-.038-.057V6.075a4.5 4.5 0 0 1 7.375-3.453l-.142.08-4.778 2.758a.795.795 0 0 0-.393.681zm1.097-2.365 2.602-1.5 2.607 1.5v2.999l-2.597 1.5-2.607-1.5Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
Open in ChatGPT
|
||||||
|
</a>
|
||||||
|
),
|
||||||
|
claude: (url: string) => (
|
||||||
|
<a
|
||||||
|
href={getPromptUrl("https://claude.ai/new", url)}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
d="m4.714 15.956 4.718-2.648.079-.23-.08-.128h-.23l-.79-.048-2.695-.073-2.337-.097-2.265-.122-.57-.121-.535-.704.055-.353.48-.321.685.06 1.518.104 2.277.157 1.651.098 2.447.255h.389l.054-.158-.133-.097-.103-.098-2.356-1.596-2.55-1.688-1.336-.972-.722-.491L2 6.223l-.158-1.008.655-.722.88.06.225.061.893.686 1.906 1.476 2.49 1.833.364.304.146-.104.018-.072-.164-.274-1.354-2.446-1.445-2.49-.644-1.032-.17-.619a2.972 2.972 0 0 1-.103-.729L6.287.133 6.7 0l.995.134.42.364.619 1.415L9.735 4.14l1.555 3.03.455.898.243.832.09.255h.159V9.01l.127-1.706.237-2.095.23-2.695.08-.76.376-.91.747-.492.583.28.48.685-.067.444-.286 1.851-.558 2.903-.365 1.942h.213l.243-.242.983-1.306 1.652-2.064.728-.82.85-.904.547-.431h1.032l.759 1.129-.34 1.166-1.063 1.347-.88 1.142-1.263 1.7-.79 1.36.074.11.188-.02 2.853-.606 1.542-.28 1.84-.315.832.388.09.395-.327.807-1.967.486-2.307.462-3.436.813-.043.03.049.061 1.548.146.662.036h1.62l3.018.225.79.522.473.638-.08.485-1.213.62-1.64-.389-3.825-.91-1.31-.329h-.183v.11l1.093 1.068 2.003 1.81 2.508 2.33.127.578-.321.455-.34-.049-2.204-1.657-.85-.747-1.925-1.62h-.127v.17l.443.649 2.343 3.521.122 1.08-.17.353-.607.213-.668-.122-1.372-1.924-1.415-2.168-1.141-1.943-.14.08-.674 7.254-.316.37-.728.28-.607-.461-.322-.747.322-1.476.388-1.924.316-1.53.285-1.9.17-.632-.012-.042-.14.018-1.432 1.967-2.18 2.945-1.724 1.845-.413.164-.716-.37.066-.662.401-.589 2.386-3.036 1.439-1.882.929-1.086-.006-.158h-.055L4.138 18.56l-1.13.146-.485-.456.06-.746.231-.243 1.907-1.312Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
Open in Claude
|
||||||
|
</a>
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DocsCopyPage({ page, url }: { page: string; url: string }) {
|
||||||
const { copyToClipboard, isCopied } = useCopyToClipboard()
|
const { copyToClipboard, isCopied } = useCopyToClipboard()
|
||||||
|
|
||||||
|
const trigger = (
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
|
className="peer -ml-0.5 size-8 shadow-none md:size-7 md:text-[0.8rem]"
|
||||||
|
>
|
||||||
|
<IconChevronDown className="rotate-180 sm:rotate-0" />
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip>
|
<Popover>
|
||||||
<TooltipTrigger asChild>
|
<div className="bg-secondary group/buttons relative flex rounded-lg *:[[data-slot=button]]:focus-visible:relative *:[[data-slot=button]]:focus-visible:z-10">
|
||||||
|
<PopoverAnchor />
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="secondary"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="h-8 pl-1.5 md:h-7 [&>svg]:size-3.5"
|
className="h-8 shadow-none md:h-7 md:text-[0.8rem]"
|
||||||
onClick={() => copyToClipboard(page)}
|
onClick={() => copyToClipboard(page)}
|
||||||
>
|
>
|
||||||
{isCopied ? <IconCheck /> : <IconCopy />} Copy Page
|
{isCopied ? <IconCheck /> : <IconCopy />}
|
||||||
|
Copy Page
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
<DropdownMenu>
|
||||||
<TooltipContent>
|
<DropdownMenuTrigger asChild className="hidden sm:flex">
|
||||||
<p>Copy as Markdown</p>
|
{trigger}
|
||||||
</TooltipContent>
|
</DropdownMenuTrigger>
|
||||||
</Tooltip>
|
<DropdownMenuContent align="end" className="shadow-none">
|
||||||
|
{Object.entries(menuItems).map(([key, value]) => (
|
||||||
|
<DropdownMenuItem key={key} asChild>
|
||||||
|
{value(url)}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
))}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
<Separator
|
||||||
|
orientation="vertical"
|
||||||
|
className="!bg-foreground/10 absolute top-0 right-8 z-0 !h-8 peer-focus-visible:opacity-0 sm:right-7 sm:!h-7"
|
||||||
|
/>
|
||||||
|
<PopoverTrigger asChild className="flex sm:hidden">
|
||||||
|
{trigger}
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent
|
||||||
|
className="bg-background/70 dark:bg-background/60 w-52 !origin-center rounded-lg p-1 shadow-sm backdrop-blur-sm"
|
||||||
|
align="start"
|
||||||
|
>
|
||||||
|
{Object.entries(menuItems).map(([key, value]) => (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="lg"
|
||||||
|
asChild
|
||||||
|
key={key}
|
||||||
|
className="*:[svg]:text-muted-foreground w-full justify-start text-base font-normal"
|
||||||
|
>
|
||||||
|
{value(url)}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</PopoverContent>
|
||||||
|
</div>
|
||||||
|
</Popover>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Icons } from "@/components/icons"
|
|||||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||||
|
|
||||||
// v0 uses the default style.
|
// v0 uses the default style.
|
||||||
const V0_STYLE = "default"
|
const V0_STYLE = "new-york-v4"
|
||||||
|
|
||||||
export function OpenInV0Button({
|
export function OpenInV0Button({
|
||||||
name,
|
name,
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export function OpenInV0Cta({ className }: React.ComponentProps<"div">) {
|
|||||||
Deploy your shadcn/ui app on Vercel
|
Deploy your shadcn/ui app on Vercel
|
||||||
</div>
|
</div>
|
||||||
<div className="text-muted-foreground">
|
<div className="text-muted-foreground">
|
||||||
Trusted by OpenAI, Sonos, Chick-fil-A, and more.
|
Trusted by OpenAI, Sonos, Adobe, and more.
|
||||||
</div>
|
</div>
|
||||||
<div className="text-muted-foreground">
|
<div className="text-muted-foreground">
|
||||||
Vercel provides tools and infrastructure to deploy apps and features at
|
Vercel provides tools and infrastructure to deploy apps and features at
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ import { siteConfig } from "@/lib/config"
|
|||||||
|
|
||||||
export function SiteFooter() {
|
export function SiteFooter() {
|
||||||
return (
|
return (
|
||||||
<footer className="group-has-[.section-soft]/body:bg-surface/40 3xl:fixed:bg-transparent 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-[.docs-nav]/body:sm:pb-0 dark:bg-transparent">
|
||||||
<div className="container-wrapper px-4 xl:px-6">
|
<div className="container-wrapper px-4 xl:px-6">
|
||||||
<div className="flex h-(--footer-height) items-center justify-between">
|
<div className="flex h-(--footer-height) items-center justify-between">
|
||||||
<div className="text-muted-foreground w-full text-center text-xs leading-loose sm:text-sm">
|
<div className="text-muted-foreground w-full px-1 text-center text-xs leading-loose sm:text-sm">
|
||||||
Built by{" "}
|
Built by{" "}
|
||||||
<a
|
<a
|
||||||
href={siteConfig.links.twitter}
|
href={siteConfig.links.twitter}
|
||||||
|
|||||||
@@ -41,7 +41,11 @@ export function SiteHeader() {
|
|||||||
<MainNav items={siteConfig.navItems} className="hidden lg:flex" />
|
<MainNav items={siteConfig.navItems} className="hidden lg:flex" />
|
||||||
<div className="ml-auto flex items-center gap-2 md:flex-1 md:justify-end">
|
<div className="ml-auto flex items-center gap-2 md:flex-1 md:justify-end">
|
||||||
<div className="hidden w-full flex-1 md:flex md:w-auto md:flex-none">
|
<div className="hidden w-full flex-1 md:flex md:w-auto md:flex-none">
|
||||||
<CommandMenu tree={pageTree} colors={colors} />
|
<CommandMenu
|
||||||
|
tree={pageTree}
|
||||||
|
colors={colors}
|
||||||
|
navItems={siteConfig.navItems}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Separator
|
<Separator
|
||||||
orientation="vertical"
|
orientation="vertical"
|
||||||
|
|||||||
@@ -4,6 +4,33 @@ description: Latest updates and announcements.
|
|||||||
toc: false
|
toc: false
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## July 2025 - Universal Registry Items
|
||||||
|
|
||||||
|
We've added support for universal registry items. This allows you to create registry items that can be distributed to any project i.e. no framework, no components.json, no tailwind, no react required.
|
||||||
|
|
||||||
|
This new registry item type unlocks a lot of new workflows. You can now distribute code, config, rules, docs, anything to any code project.
|
||||||
|
|
||||||
|
See the [docs](/docs/registry/examples) for more details and examples.
|
||||||
|
|
||||||
|
## July 2025 - Local File Support
|
||||||
|
|
||||||
|
The shadcn CLI now supports local files. Initialize projects and add components, themes, hooks, utils and more from local JSON files.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Initialize a project from a local file
|
||||||
|
npx shadcn init ./template.json
|
||||||
|
|
||||||
|
# Add a component from a local file
|
||||||
|
npx shadcn add ./block.json
|
||||||
|
```
|
||||||
|
|
||||||
|
This feature enables powerful new workflows:
|
||||||
|
|
||||||
|
- **Zero setup** - No remote registries required
|
||||||
|
- **Faster development** - Test registry items locally before publishing
|
||||||
|
- **Enhanced workflow for agents and MCP** - Generate and run registry items locally
|
||||||
|
- **Private components** - Keep proprietary components local and private.
|
||||||
|
|
||||||
## June 2025 - `radix-ui`
|
## June 2025 - `radix-ui`
|
||||||
|
|
||||||
We've added a new command to migrate to the new `radix-ui` package. This command will replace all `@radix-ui/react-*` imports with `radix-ui`.
|
We've added a new command to migrate to the new `radix-ui` package. This command will replace all `@radix-ui/react-*` imports with `radix-ui`.
|
||||||
|
|||||||
@@ -21,15 +21,14 @@ Usage: shadcn init [options] [components...]
|
|||||||
initialize your project and install dependencies
|
initialize your project and install dependencies
|
||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
components the components to add or a url to the component.
|
components name, url or local path to component
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
-t, --template <template> the template to use. (next, next-monorepo)
|
-t, --template <template> the template to use. (next, next-monorepo)
|
||||||
-b, --base-color <base-color> the base color to use. (neutral, gray, zinc, stone, slate)
|
-b, --base-color <base-color> the base color to use. (neutral, gray, zinc, stone, slate)
|
||||||
-y, --yes skip confirmation prompt. (default: true)
|
-y, --yes skip confirmation prompt. (default: true)
|
||||||
-f, --force force overwrite of existing configuration. (default: false)
|
-f, --force force overwrite of existing configuration. (default: false)
|
||||||
-c, --cwd <cwd> the working directory. defaults to the current directory. (default:
|
-c, --cwd <cwd> the working directory. defaults to the current directory.
|
||||||
"/Users/shadcn/Code/shadcn/ui/packages/shadcn")
|
|
||||||
-s, --silent mute output. (default: false)
|
-s, --silent mute output. (default: false)
|
||||||
--src-dir use the src directory when creating a new project. (default: false)
|
--src-dir use the src directory when creating a new project. (default: false)
|
||||||
--no-src-dir do not use the src directory when creating a new project.
|
--no-src-dir do not use the src directory when creating a new project.
|
||||||
@@ -54,12 +53,12 @@ Usage: shadcn add [options] [components...]
|
|||||||
add a component to your project
|
add a component to your project
|
||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
components the components to add or a url to the component.
|
components name, url or local path to component
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
-y, --yes skip confirmation prompt. (default: false)
|
-y, --yes skip confirmation prompt. (default: false)
|
||||||
-o, --overwrite overwrite existing files. (default: false)
|
-o, --overwrite overwrite existing files. (default: false)
|
||||||
-c, --cwd <cwd> the working directory. defaults to the current directory. (default: "/Users/shadcn/Desktop")
|
-c, --cwd <cwd> the working directory. defaults to the current directory.
|
||||||
-a, --all add all available components (default: false)
|
-a, --all add all available components (default: false)
|
||||||
-p, --path <path> the path to add the component to.
|
-p, --path <path> the path to add the component to.
|
||||||
-s, --silent mute output. (default: false)
|
-s, --silent mute output. (default: false)
|
||||||
@@ -92,8 +91,7 @@ Arguments:
|
|||||||
|
|
||||||
Options:
|
Options:
|
||||||
-o, --output <path> destination directory for json files (default: "./public/r")
|
-o, --output <path> destination directory for json files (default: "./public/r")
|
||||||
-c, --cwd <cwd> the working directory. defaults to the current directory. (default:
|
-c, --cwd <cwd> the working directory. defaults to the current directory.
|
||||||
"/Users/shadcn/Code/shadcn/ui/packages/shadcn")
|
|
||||||
-h, --help display help for command
|
-h, --help display help for command
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -15,3 +15,4 @@ description: Every component recreated in Figma. With customizable props, typogr
|
|||||||
## Free
|
## Free
|
||||||
|
|
||||||
- [shadcn/ui design system](https://www.figma.com/community/file/1203061493325953101) by [Pietro Schirano](https://twitter.com/skirano) - A design companion for shadcn/ui. Each component was painstakingly crafted to perfectly match the code implementation.
|
- [shadcn/ui design system](https://www.figma.com/community/file/1203061493325953101) by [Pietro Schirano](https://twitter.com/skirano) - A design companion for shadcn/ui. Each component was painstakingly crafted to perfectly match the code implementation.
|
||||||
|
- [Obra shadcn/ui](https://www.figma.com/community/file/1514746685758799870/obra-shadcn-ui) by [Obra Studio](https://obra.studio/) - Carefully crafted kit designed in the philosophy of shadcn, tracks v4, MIT licensed
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ npx shadcn@latest add calendar
|
|||||||
<Step>Install the following dependencies:</Step>
|
<Step>Install the following dependencies:</Step>
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install react-day-picker@8.10.1 date-fns
|
npm install react-day-picker date-fns
|
||||||
```
|
```
|
||||||
|
|
||||||
<Step>Add the `Button` component to your project.</Step>
|
<Step>Add the `Button` component to your project.</Step>
|
||||||
|
|||||||
@@ -4,9 +4,15 @@ description: Beautiful charts. Built using Recharts. Copy and paste into your ap
|
|||||||
component: true
|
component: true
|
||||||
---
|
---
|
||||||
|
|
||||||
|
<Callout>
|
||||||
|
|
||||||
|
**Note:** We're working on upgrading to Recharts v3. In the meantime, if you'd like to start testing v3, see the code in the comment [here](https://github.com/shadcn-ui/ui/issues/7669#issuecomment-2998299159). We'll have an official release soon.
|
||||||
|
|
||||||
|
</Callout>
|
||||||
|
|
||||||
<ComponentPreview
|
<ComponentPreview
|
||||||
name="chart-bar-interactive"
|
name="chart-bar-interactive"
|
||||||
className="theme-blue -mt-4 [&_.preview]:p-0 [&_.preview]:lg:min-h-[404px] [&_.preview>div]:w-full [&_.preview>div]:border-none [&_.preview>div]:shadow-none"
|
className="theme-blue [&_.preview]:h-auto [&_.preview]:p-0 [&_.preview]:lg:min-h-[404px] [&_.preview>div]:w-full [&_.preview>div]:border-none [&_.preview>div]:shadow-none"
|
||||||
hideCode
|
hideCode
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -185,7 +185,7 @@ export function DataTable<TData, TValue>({
|
|||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="rounded-md border">
|
<div className="overflow-hidden rounded-md border">
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
{table.getHeaderGroups().map((headerGroup) => (
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
@@ -425,7 +425,7 @@ export function DataTable<TData, TValue>({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="rounded-md border">
|
<div className="overflow-hidden rounded-md border">
|
||||||
<Table>
|
<Table>
|
||||||
{ // .... }
|
{ // .... }
|
||||||
</Table>
|
</Table>
|
||||||
@@ -499,7 +499,7 @@ export function DataTable<TData, TValue>({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="rounded-md border">
|
<div className="overflow-hidden rounded-md border">
|
||||||
<Table>{ ... }</Table>
|
<Table>{ ... }</Table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -602,7 +602,7 @@ export function DataTable<TData, TValue>({
|
|||||||
className="max-w-sm"
|
className="max-w-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-md border">
|
<div className="overflow-hidden rounded-md border">
|
||||||
<Table>{ ... }</Table>
|
<Table>{ ... }</Table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -715,7 +715,7 @@ export function DataTable<TData, TValue>({
|
|||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-md border">
|
<div className="overflow-hidden rounded-md border">
|
||||||
<Table>{ ... }</Table>
|
<Table>{ ... }</Table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -805,7 +805,7 @@ export function DataTable<TData, TValue>({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="rounded-md border">
|
<div className="overflow-hidden rounded-md border">
|
||||||
<Table />
|
<Table />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -328,6 +328,20 @@ Add custom theme variables to the `theme` object.
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Add custom plugins
|
||||||
|
|
||||||
|
```json title="example-plugin.json" showLineNumbers
|
||||||
|
{
|
||||||
|
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
||||||
|
"name": "custom-plugin",
|
||||||
|
"type": "registry:component",
|
||||||
|
"css": {
|
||||||
|
"@plugin @tailwindcss/typography": {},
|
||||||
|
"@plugin foo": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Add custom animations
|
## Add custom animations
|
||||||
|
|
||||||
Note: you need to define both `@keyframes` in css and `theme` in cssVars to use animations.
|
Note: you need to define both `@keyframes` in css and `theme` in cssVars to use animations.
|
||||||
@@ -354,3 +368,70 @@ Note: you need to define both `@keyframes` in css and `theme` in cssVars to use
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Universal Items
|
||||||
|
|
||||||
|
As of `2.9.0`, you can create universal items that can be installed without framework detection or components.json.
|
||||||
|
|
||||||
|
To make an item universal i.e framework agnostic, all the files in the item must have an explicit target.
|
||||||
|
|
||||||
|
Here's an example of a registry item that installs custom Cursor rules for _python_:
|
||||||
|
|
||||||
|
```json title=".cursor/rules/custom-python.mdc" showLineNumbers {9}
|
||||||
|
{
|
||||||
|
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
||||||
|
"name": "python-rules",
|
||||||
|
"type": "registry:item",
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"path": "/path/to/your/registry/default/custom-python.mdc",
|
||||||
|
"type": "registry:file",
|
||||||
|
"target": "~/.cursor/rules/custom-python.mdc",
|
||||||
|
"content": "..."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Here's another example for installation custom ESLint config:
|
||||||
|
|
||||||
|
```json title=".eslintrc.json" showLineNumbers {9}
|
||||||
|
{
|
||||||
|
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
||||||
|
"name": "my-eslint-config",
|
||||||
|
"type": "registry:item",
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"path": "/path/to/your/registry/default/custom-eslint.json",
|
||||||
|
"type": "registry:file",
|
||||||
|
"target": "~/.eslintrc.json",
|
||||||
|
"content": "..."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also have a universal item that installs multiple files:
|
||||||
|
|
||||||
|
```json title="my-custom-starter-template.json" showLineNumbers {9}
|
||||||
|
{
|
||||||
|
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
||||||
|
"name": "my-custom-start-template",
|
||||||
|
"type": "registry:item",
|
||||||
|
dependencies: ["better-auth"]
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"path": "/path/to/file-01.json",
|
||||||
|
"type": "registry:file",
|
||||||
|
"target": "~/file-01.json",
|
||||||
|
"content": "..."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "/path/to/file-02.vue",
|
||||||
|
"type": "registry:file",
|
||||||
|
"target": "~/pages/file-02.vue",
|
||||||
|
"content": "..."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
---
|
---
|
||||||
title: Registry
|
title: Registry
|
||||||
description: Run your own component registry.
|
description: Run your own code registry.
|
||||||
---
|
---
|
||||||
|
|
||||||
<Callout>
|
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.
|
||||||
**Note:** This feature is currently experimental. Help us improve it by
|
|
||||||
testing it out and sending feedback. If you have any questions, please [reach
|
|
||||||
out to us](https://github.com/shadcn-ui/ui/discussions).
|
|
||||||
</Callout>
|
|
||||||
|
|
||||||
You can use the `shadcn` CLI to run your own component registry. Running your own registry allows you to distribute your custom components, hooks, pages, and other files to any React project.
|
<Callout>
|
||||||
|
**Note:** The registry works with any project type and any framework, and is
|
||||||
|
not limited to React.
|
||||||
|
</Callout>
|
||||||
|
|
||||||
<figure className="flex flex-col gap-4">
|
<figure className="flex flex-col gap-4">
|
||||||
<Image
|
<Image
|
||||||
@@ -27,12 +26,10 @@ You can use the `shadcn` CLI to run your own component registry. Running your ow
|
|||||||
className="mt-6 hidden w-full overflow-hidden rounded-lg border shadow-sm dark:block"
|
className="mt-6 hidden w-full overflow-hidden rounded-lg border shadow-sm dark:block"
|
||||||
/>
|
/>
|
||||||
<figcaption className="text-center text-sm text-gray-500">
|
<figcaption className="text-center text-sm text-gray-500">
|
||||||
Distribute code to any React project.
|
A distribution system for code
|
||||||
</figcaption>
|
</figcaption>
|
||||||
</figure>
|
</figure>
|
||||||
|
|
||||||
Registry items are automatically compatible with the `shadcn` CLI and `Open in v0`.
|
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
You are free to design and host your custom registry as you see fit. The only requirement is that your registry items must be valid JSON files that conform to the [registry-item schema specification](/docs/registry/registry-item-json).
|
You are free to design and host your custom registry as you see fit. The only requirement is that your registry items must be valid JSON files that conform to the [registry-item schema specification](/docs/registry/registry-item-json).
|
||||||
|
|||||||
@@ -107,6 +107,7 @@ The following types are supported:
|
|||||||
| `registry:file` | Use for miscellaneous files. |
|
| `registry:file` | Use for miscellaneous files. |
|
||||||
| `registry:style` | Use for registry styles. eg. `new-york` |
|
| `registry:style` | Use for registry styles. eg. `new-york` |
|
||||||
| `registry:theme` | Use for themes. |
|
| `registry:theme` | Use for themes. |
|
||||||
|
| `registry:item` | Use for universal registry items. |
|
||||||
|
|
||||||
### author
|
### author
|
||||||
|
|
||||||
@@ -260,11 +261,13 @@ Use to define CSS variables for your registry item.
|
|||||||
|
|
||||||
### css
|
### css
|
||||||
|
|
||||||
Use `css` to add new rules to the project's CSS file eg. `@layer base`, `@layer components`, `@utility`, `@keyframes`, etc.
|
Use `css` to add new rules to the project's CSS file eg. `@layer base`, `@layer components`, `@utility`, `@keyframes`, `@plugin`, etc.
|
||||||
|
|
||||||
```json title="registry-item.json" showLineNumbers
|
```json title="registry-item.json" showLineNumbers
|
||||||
{
|
{
|
||||||
"css": {
|
"css": {
|
||||||
|
"@plugin @tailwindcss/typography": {},
|
||||||
|
"@plugin foo": {},
|
||||||
"@layer base": {
|
"@layer base": {
|
||||||
"body": {
|
"body": {
|
||||||
"font-size": "var(--text-base)",
|
"font-size": "var(--text-base)",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ export const siteConfig = {
|
|||||||
url: "https://ui.shadcn.com",
|
url: "https://ui.shadcn.com",
|
||||||
ogImage: "https://ui.shadcn.com/og.jpg",
|
ogImage: "https://ui.shadcn.com/og.jpg",
|
||||||
description:
|
description:
|
||||||
"A set of beautifully-designed, accessible components and a code distribution platform. Works with your favorite frameworks. Open Source. Open Code.",
|
"A set of beautifully designed components that you can customize, extend, and build on. Start here then make it your own. Open Source. Open Code.",
|
||||||
links: {
|
links: {
|
||||||
twitter: "https://twitter.com/shadcn",
|
twitter: "https://twitter.com/shadcn",
|
||||||
github: "https://github.com/shadcn-ui/ui",
|
github: "https://github.com/shadcn-ui/ui",
|
||||||
|
|||||||
@@ -70,6 +70,14 @@ const nextConfig = {
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
rewrites() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
source: "/docs/:path*.mdx",
|
||||||
|
destination: "/llm/:path*",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const withMDX = createMDX({})
|
const withMDX = createMDX({})
|
||||||
|
|||||||
@@ -54,7 +54,7 @@
|
|||||||
"@radix-ui/react-toggle-group": "^1.1.1",
|
"@radix-ui/react-toggle-group": "^1.1.1",
|
||||||
"@radix-ui/react-tooltip": "^1.1.7",
|
"@radix-ui/react-tooltip": "^1.1.7",
|
||||||
"@tabler/icons-react": "^3.31.0",
|
"@tabler/icons-react": "^3.31.0",
|
||||||
"@tailwindcss/postcss": "^4.0.1",
|
"@tailwindcss/postcss": "^4.1.11",
|
||||||
"@tanstack/react-table": "^8.9.1",
|
"@tanstack/react-table": "^8.9.1",
|
||||||
"@vercel/analytics": "^1.4.1",
|
"@vercel/analytics": "^1.4.1",
|
||||||
"change-case": "^5.4.4",
|
"change-case": "^5.4.4",
|
||||||
@@ -86,7 +86,7 @@
|
|||||||
"recharts": "2.15.1",
|
"recharts": "2.15.1",
|
||||||
"rehype-pretty-code": "^0.14.1",
|
"rehype-pretty-code": "^0.14.1",
|
||||||
"rimraf": "^6.0.1",
|
"rimraf": "^6.0.1",
|
||||||
"shadcn": "2.6.2",
|
"shadcn": "2.10.0",
|
||||||
"shiki": "^1.10.1",
|
"shiki": "^1.10.1",
|
||||||
"sonner": "^2.0.0",
|
"sonner": "^2.0.0",
|
||||||
"tailwind-merge": "^3.0.1",
|
"tailwind-merge": "^3.0.1",
|
||||||
@@ -108,7 +108,7 @@
|
|||||||
"eslint-config-next": "15.3.1",
|
"eslint-config-next": "15.3.1",
|
||||||
"prettier": "^3.4.2",
|
"prettier": "^3.4.2",
|
||||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||||
"tailwindcss": "^4.1.7",
|
"tailwindcss": "^4.1.11",
|
||||||
"tw-animate-css": "^1.2.4",
|
"tw-animate-css": "^1.2.4",
|
||||||
"typescript": "^5",
|
"typescript": "^5",
|
||||||
"unist-builder": "3.0.0",
|
"unist-builder": "3.0.0",
|
||||||
|
|||||||
@@ -180,7 +180,7 @@
|
|||||||
"name": "chart",
|
"name": "chart",
|
||||||
"type": "registry:ui",
|
"type": "registry:ui",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"recharts",
|
"recharts@2.15.4",
|
||||||
"lucide-react"
|
"lucide-react"
|
||||||
],
|
],
|
||||||
"registryDependencies": [
|
"registryDependencies": [
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -4,7 +4,7 @@
|
|||||||
"type": "registry:ui",
|
"type": "registry:ui",
|
||||||
"author": "shadcn (https://ui.shadcn.com)",
|
"author": "shadcn (https://ui.shadcn.com)",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"recharts",
|
"recharts@2.15.4",
|
||||||
"lucide-react"
|
"lucide-react"
|
||||||
],
|
],
|
||||||
"registryDependencies": [
|
"registryDependencies": [
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
"files": [
|
"files": [
|
||||||
{
|
{
|
||||||
"path": "registry/new-york-v4/blocks/calendar-24.tsx",
|
"path": "registry/new-york-v4/blocks/calendar-24.tsx",
|
||||||
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { ChevronDownIcon } from \"lucide-react\"\n\nimport { Button } from \"@/registry/new-york-v4/ui/button\"\nimport { Calendar } from \"@/registry/new-york-v4/ui/calendar\"\nimport { Input } from \"@/registry/new-york-v4/ui/input\"\nimport { Label } from \"@/registry/new-york-v4/ui/label\"\nimport {\n Popover,\n PopoverContent,\n PopoverTrigger,\n} from \"@/registry/new-york-v4/ui/popover\"\n\nexport default function Calendar24() {\n const [open, setOpen] = React.useState(false)\n const [date, setDate] = React.useState<Date | undefined>(undefined)\n\n return (\n <div className=\"flex gap-4\">\n <div className=\"flex flex-col gap-3\">\n <Label htmlFor=\"date\" className=\"px-1\">\n Date\n </Label>\n <Popover open={open} onOpenChange={setOpen}>\n <PopoverTrigger asChild>\n <Button\n variant=\"outline\"\n id=\"date\"\n className=\"w-32 justify-between font-normal\"\n >\n {date ? date.toLocaleDateString() : \"Select date\"}\n <ChevronDownIcon />\n </Button>\n </PopoverTrigger>\n <PopoverContent className=\"w-auto overflow-hidden p-0\" align=\"start\">\n <Calendar\n mode=\"single\"\n selected={date}\n captionLayout=\"dropdown\"\n onSelect={(date) => {\n setDate(date)\n setOpen(false)\n }}\n />\n </PopoverContent>\n </Popover>\n </div>\n <div className=\"flex flex-col gap-3\">\n <Label htmlFor=\"time\" className=\"px-1\">\n Time\n </Label>\n <Input\n type=\"time\"\n id=\"time\"\n step=\"1\"\n defaultValue=\"10:30:00\"\n className=\"bg-background appearance-none [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-calendar-picker-indicator]:appearance-none\"\n />\n </div>\n </div>\n )\n}\n",
|
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { ChevronDownIcon } from \"lucide-react\"\n\nimport { Button } from \"@/registry/new-york-v4/ui/button\"\nimport { Calendar } from \"@/registry/new-york-v4/ui/calendar\"\nimport { Input } from \"@/registry/new-york-v4/ui/input\"\nimport { Label } from \"@/registry/new-york-v4/ui/label\"\nimport {\n Popover,\n PopoverContent,\n PopoverTrigger,\n} from \"@/registry/new-york-v4/ui/popover\"\n\nexport default function Calendar24() {\n const [open, setOpen] = React.useState(false)\n const [date, setDate] = React.useState<Date | undefined>(undefined)\n\n return (\n <div className=\"flex gap-4\">\n <div className=\"flex flex-col gap-3\">\n <Label htmlFor=\"date-picker\" className=\"px-1\">\n Date\n </Label>\n <Popover open={open} onOpenChange={setOpen}>\n <PopoverTrigger asChild>\n <Button\n variant=\"outline\"\n id=\"date-picker\"\n className=\"w-32 justify-between font-normal\"\n >\n {date ? date.toLocaleDateString() : \"Select date\"}\n <ChevronDownIcon />\n </Button>\n </PopoverTrigger>\n <PopoverContent className=\"w-auto overflow-hidden p-0\" align=\"start\">\n <Calendar\n mode=\"single\"\n selected={date}\n captionLayout=\"dropdown\"\n onSelect={(date) => {\n setDate(date)\n setOpen(false)\n }}\n />\n </PopoverContent>\n </Popover>\n </div>\n <div className=\"flex flex-col gap-3\">\n <Label htmlFor=\"time-picker\" className=\"px-1\">\n Time\n </Label>\n <Input\n type=\"time\"\n id=\"time-picker\"\n step=\"1\"\n defaultValue=\"10:30:00\"\n className=\"bg-background appearance-none [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-calendar-picker-indicator]:appearance-none\"\n />\n </div>\n </div>\n )\n}\n",
|
||||||
"type": "registry:component"
|
"type": "registry:component"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -3,7 +3,7 @@
|
|||||||
"name": "chart",
|
"name": "chart",
|
||||||
"type": "registry:ui",
|
"type": "registry:ui",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"recharts",
|
"recharts@2.15.4",
|
||||||
"lucide-react"
|
"lucide-react"
|
||||||
],
|
],
|
||||||
"registryDependencies": [
|
"registryDependencies": [
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -4,7 +4,7 @@
|
|||||||
"type": "registry:ui",
|
"type": "registry:ui",
|
||||||
"author": "shadcn (https://ui.shadcn.com)",
|
"author": "shadcn (https://ui.shadcn.com)",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"recharts",
|
"recharts@2.15.4",
|
||||||
"lucide-react"
|
"lucide-react"
|
||||||
],
|
],
|
||||||
"registryDependencies": [
|
"registryDependencies": [
|
||||||
|
|||||||
@@ -17,7 +17,8 @@
|
|||||||
"registry:theme",
|
"registry:theme",
|
||||||
"registry:page",
|
"registry:page",
|
||||||
"registry:file",
|
"registry:file",
|
||||||
"registry:style"
|
"registry:style",
|
||||||
|
"registry:item"
|
||||||
],
|
],
|
||||||
"description": "The type of the item. This is used to determine the type and target path of the item when resolved for a project."
|
"description": "The type of the item. This is used to determine the type and target path of the item when resolved for a project."
|
||||||
},
|
},
|
||||||
@@ -79,7 +80,8 @@
|
|||||||
"registry:theme",
|
"registry:theme",
|
||||||
"registry:page",
|
"registry:page",
|
||||||
"registry:file",
|
"registry:file",
|
||||||
"registry:style"
|
"registry:style",
|
||||||
|
"registry:item"
|
||||||
],
|
],
|
||||||
"description": "The type of the file. This is used to determine the type of the file when resolved for a project."
|
"description": "The type of the file. This is used to determine the type of the file when resolved for a project."
|
||||||
},
|
},
|
||||||
@@ -189,6 +191,13 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"envVars": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Environment variables required by the registry item. Key-value pairs that will be added to the project's .env file.",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
"meta": {
|
"meta": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"description": "Additional metadata for the registry item. This is an object with any key value pairs.",
|
"description": "Additional metadata for the registry item. This is an object with any key value pairs.",
|
||||||
|
|||||||
@@ -169,7 +169,7 @@
|
|||||||
"name": "chart",
|
"name": "chart",
|
||||||
"type": "registry:ui",
|
"type": "registry:ui",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"recharts",
|
"recharts@2.15.4",
|
||||||
"lucide-react"
|
"lucide-react"
|
||||||
],
|
],
|
||||||
"registryDependencies": [
|
"registryDependencies": [
|
||||||
|
|||||||
@@ -20,14 +20,14 @@ export default function Calendar24() {
|
|||||||
return (
|
return (
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<Label htmlFor="date" className="px-1">
|
<Label htmlFor="date-picker" className="px-1">
|
||||||
Date
|
Date
|
||||||
</Label>
|
</Label>
|
||||||
<Popover open={open} onOpenChange={setOpen}>
|
<Popover open={open} onOpenChange={setOpen}>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
id="date"
|
id="date-picker"
|
||||||
className="w-32 justify-between font-normal"
|
className="w-32 justify-between font-normal"
|
||||||
>
|
>
|
||||||
{date ? date.toLocaleDateString() : "Select date"}
|
{date ? date.toLocaleDateString() : "Select date"}
|
||||||
@@ -48,12 +48,12 @@ export default function Calendar24() {
|
|||||||
</Popover>
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<Label htmlFor="time" className="px-1">
|
<Label htmlFor="time-picker" className="px-1">
|
||||||
Time
|
Time
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
type="time"
|
type="time"
|
||||||
id="time"
|
id="time-picker"
|
||||||
step="1"
|
step="1"
|
||||||
defaultValue="10:30:00"
|
defaultValue="10:30:00"
|
||||||
className="bg-background appearance-none [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-calendar-picker-indicator]:appearance-none"
|
className="bg-background appearance-none [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-calendar-picker-indicator]:appearance-none"
|
||||||
|
|||||||
@@ -257,7 +257,6 @@ export function ChartAreaInteractive() {
|
|||||||
/>
|
/>
|
||||||
<ChartTooltip
|
<ChartTooltip
|
||||||
cursor={false}
|
cursor={false}
|
||||||
defaultIndex={isMobile ? -1 : 10}
|
|
||||||
content={
|
content={
|
||||||
<ChartTooltipContent
|
<ChartTooltipContent
|
||||||
labelFormatter={(value) => {
|
labelFormatter={(value) => {
|
||||||
|
|||||||
@@ -233,7 +233,7 @@ export default function DataTableDemo() {
|
|||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-md border">
|
<div className="overflow-hidden rounded-md border">
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
{table.getHeaderGroups().map((headerGroup) => (
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
|
|||||||
@@ -73,7 +73,10 @@ function Calendar({
|
|||||||
"relative has-focus:border-ring border border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] rounded-md",
|
"relative has-focus:border-ring border border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] rounded-md",
|
||||||
defaultClassNames.dropdown_root
|
defaultClassNames.dropdown_root
|
||||||
),
|
),
|
||||||
dropdown: cn("absolute inset-0 opacity-0", defaultClassNames.dropdown),
|
dropdown: cn(
|
||||||
|
"absolute bg-popover inset-0 opacity-0",
|
||||||
|
defaultClassNames.dropdown
|
||||||
|
),
|
||||||
caption_label: cn(
|
caption_label: cn(
|
||||||
"select-none font-medium",
|
"select-none font-medium",
|
||||||
captionLayout === "label"
|
captionLayout === "label"
|
||||||
|
|||||||
@@ -155,7 +155,7 @@ export const ui: Registry["items"] = [
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
registryDependencies: ["card"],
|
registryDependencies: ["card"],
|
||||||
dependencies: ["recharts", "lucide-react"],
|
dependencies: ["recharts@2.15.4", "lucide-react"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "checkbox",
|
name: "checkbox",
|
||||||
|
|||||||
@@ -248,7 +248,7 @@
|
|||||||
font-size: var(--text-sm);
|
font-size: var(--text-sm);
|
||||||
outline: none;
|
outline: none;
|
||||||
position: relative;
|
position: relative;
|
||||||
@apply md:-mx-4;
|
@apply md:-mx-1;
|
||||||
|
|
||||||
&:has([data-rehype-pretty-code-title]) [data-slot="copy-button"] {
|
&:has([data-rehype-pretty-code-title]) [data-slot="copy-button"] {
|
||||||
top: calc(var(--spacing) * 1.5) !important;
|
top: calc(var(--spacing) * 1.5) !important;
|
||||||
|
|||||||
6
apps/www/README.md
Normal file
6
apps/www/README.md
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# ui.shadcn.com
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> The site at ui.shadcn.com now points to `apps/v4`. All changes should be made in the `apps/v4` directory.
|
||||||
|
|
||||||
|
Licensed under the [MIT license](https://github.com/shadcn/ui/blob/main/LICENSE.md).
|
||||||
@@ -70,7 +70,7 @@ export function DataTable<TData, TValue>({
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<DataTableToolbar table={table} />
|
<DataTableToolbar table={table} />
|
||||||
<div className="rounded-md border">
|
<div className="overflow-hidden rounded-md border">
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
{table.getHeaderGroups().map((headerGroup) => (
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
|
|||||||
@@ -239,7 +239,7 @@ export function CardsDataTable() {
|
|||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-md border">
|
<div className="overflow-hidden rounded-md border">
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
{table.getHeaderGroups().map((headerGroup) => (
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ npx shadcn@latest add calendar
|
|||||||
<Step>Install the following dependencies:</Step>
|
<Step>Install the following dependencies:</Step>
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install react-day-picker@8.10.1 date-fns
|
npm install react-day-picker date-fns
|
||||||
```
|
```
|
||||||
|
|
||||||
<Step>Add the `Button` component to your project.</Step>
|
<Step>Add the `Button` component to your project.</Step>
|
||||||
|
|||||||
@@ -185,7 +185,7 @@ export function DataTable<TData, TValue>({
|
|||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="rounded-md border">
|
<div className="overflow-hidden rounded-md border">
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
{table.getHeaderGroups().map((headerGroup) => (
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
@@ -425,7 +425,7 @@ export function DataTable<TData, TValue>({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="rounded-md border">
|
<div className="overflow-hidden rounded-md border">
|
||||||
<Table>
|
<Table>
|
||||||
{ // .... }
|
{ // .... }
|
||||||
</Table>
|
</Table>
|
||||||
@@ -499,7 +499,7 @@ export function DataTable<TData, TValue>({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="rounded-md border">
|
<div className="overflow-hidden rounded-md border">
|
||||||
<Table>{ ... }</Table>
|
<Table>{ ... }</Table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -602,7 +602,7 @@ export function DataTable<TData, TValue>({
|
|||||||
className="max-w-sm"
|
className="max-w-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-md border">
|
<div className="overflow-hidden rounded-md border">
|
||||||
<Table>{ ... }</Table>
|
<Table>{ ... }</Table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -715,7 +715,7 @@ export function DataTable<TData, TValue>({
|
|||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-md border">
|
<div className="overflow-hidden rounded-md border">
|
||||||
<Table>{ ... }</Table>
|
<Table>{ ... }</Table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -805,7 +805,7 @@ export function DataTable<TData, TValue>({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="rounded-md border">
|
<div className="overflow-hidden rounded-md border">
|
||||||
<Table />
|
<Table />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,3 +10,4 @@ description: Every component recreated in Figma. With customizable props, typogr
|
|||||||
## Free
|
## Free
|
||||||
|
|
||||||
- [shadcn/ui design system](https://www.figma.com/community/file/1203061493325953101) by [Pietro Schirano](https://twitter.com/skirano) - A design companion for shadcn/ui. Each component was painstakingly crafted to perfectly match the code implementation.
|
- [shadcn/ui design system](https://www.figma.com/community/file/1203061493325953101) by [Pietro Schirano](https://twitter.com/skirano) - A design companion for shadcn/ui. Each component was painstakingly crafted to perfectly match the code implementation.
|
||||||
|
- [Obra shadcn/ui](https://www.figma.com/community/file/1514746685758799870/obra-shadcn-ui) by [Obra Studio](https://https://obra.studio/) - Carefully crafted kit designed in the philosophy of shadcn, tracks v4, MIT licensed
|
||||||
|
|||||||
@@ -88,7 +88,7 @@
|
|||||||
"react-resizable-panels": "^2.0.22",
|
"react-resizable-panels": "^2.0.22",
|
||||||
"react-wrap-balancer": "^0.4.1",
|
"react-wrap-balancer": "^0.4.1",
|
||||||
"recharts": "2.12.7",
|
"recharts": "2.12.7",
|
||||||
"shadcn": "2.6.2",
|
"shadcn": "2.10.0",
|
||||||
"sharp": "^0.32.6",
|
"sharp": "^0.32.6",
|
||||||
"sonner": "^1.2.3",
|
"sonner": "^1.2.3",
|
||||||
"swr": "2.2.6-beta.3",
|
"swr": "2.2.6-beta.3",
|
||||||
|
|||||||
@@ -180,7 +180,7 @@
|
|||||||
"name": "chart",
|
"name": "chart",
|
||||||
"type": "registry:ui",
|
"type": "registry:ui",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"recharts",
|
"recharts@2.15.4",
|
||||||
"lucide-react"
|
"lucide-react"
|
||||||
],
|
],
|
||||||
"registryDependencies": [
|
"registryDependencies": [
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -4,7 +4,7 @@
|
|||||||
"type": "registry:ui",
|
"type": "registry:ui",
|
||||||
"author": "shadcn (https://ui.shadcn.com)",
|
"author": "shadcn (https://ui.shadcn.com)",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"recharts",
|
"recharts@2.15.4",
|
||||||
"lucide-react"
|
"lucide-react"
|
||||||
],
|
],
|
||||||
"registryDependencies": [
|
"registryDependencies": [
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
"files": [
|
"files": [
|
||||||
{
|
{
|
||||||
"path": "registry/new-york-v4/blocks/calendar-24.tsx",
|
"path": "registry/new-york-v4/blocks/calendar-24.tsx",
|
||||||
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { ChevronDownIcon } from \"lucide-react\"\n\nimport { Button } from \"@/registry/new-york-v4/ui/button\"\nimport { Calendar } from \"@/registry/new-york-v4/ui/calendar\"\nimport { Input } from \"@/registry/new-york-v4/ui/input\"\nimport { Label } from \"@/registry/new-york-v4/ui/label\"\nimport {\n Popover,\n PopoverContent,\n PopoverTrigger,\n} from \"@/registry/new-york-v4/ui/popover\"\n\nexport default function Calendar24() {\n const [open, setOpen] = React.useState(false)\n const [date, setDate] = React.useState<Date | undefined>(undefined)\n\n return (\n <div className=\"flex gap-4\">\n <div className=\"flex flex-col gap-3\">\n <Label htmlFor=\"date\" className=\"px-1\">\n Date\n </Label>\n <Popover open={open} onOpenChange={setOpen}>\n <PopoverTrigger asChild>\n <Button\n variant=\"outline\"\n id=\"date\"\n className=\"w-32 justify-between font-normal\"\n >\n {date ? date.toLocaleDateString() : \"Select date\"}\n <ChevronDownIcon />\n </Button>\n </PopoverTrigger>\n <PopoverContent className=\"w-auto overflow-hidden p-0\" align=\"start\">\n <Calendar\n mode=\"single\"\n selected={date}\n captionLayout=\"dropdown\"\n onSelect={(date) => {\n setDate(date)\n setOpen(false)\n }}\n />\n </PopoverContent>\n </Popover>\n </div>\n <div className=\"flex flex-col gap-3\">\n <Label htmlFor=\"time\" className=\"px-1\">\n Time\n </Label>\n <Input\n type=\"time\"\n id=\"time\"\n step=\"1\"\n defaultValue=\"10:30:00\"\n className=\"bg-background appearance-none [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-calendar-picker-indicator]:appearance-none\"\n />\n </div>\n </div>\n )\n}\n",
|
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { ChevronDownIcon } from \"lucide-react\"\n\nimport { Button } from \"@/registry/new-york-v4/ui/button\"\nimport { Calendar } from \"@/registry/new-york-v4/ui/calendar\"\nimport { Input } from \"@/registry/new-york-v4/ui/input\"\nimport { Label } from \"@/registry/new-york-v4/ui/label\"\nimport {\n Popover,\n PopoverContent,\n PopoverTrigger,\n} from \"@/registry/new-york-v4/ui/popover\"\n\nexport default function Calendar24() {\n const [open, setOpen] = React.useState(false)\n const [date, setDate] = React.useState<Date | undefined>(undefined)\n\n return (\n <div className=\"flex gap-4\">\n <div className=\"flex flex-col gap-3\">\n <Label htmlFor=\"date-picker\" className=\"px-1\">\n Date\n </Label>\n <Popover open={open} onOpenChange={setOpen}>\n <PopoverTrigger asChild>\n <Button\n variant=\"outline\"\n id=\"date-picker\"\n className=\"w-32 justify-between font-normal\"\n >\n {date ? date.toLocaleDateString() : \"Select date\"}\n <ChevronDownIcon />\n </Button>\n </PopoverTrigger>\n <PopoverContent className=\"w-auto overflow-hidden p-0\" align=\"start\">\n <Calendar\n mode=\"single\"\n selected={date}\n captionLayout=\"dropdown\"\n onSelect={(date) => {\n setDate(date)\n setOpen(false)\n }}\n />\n </PopoverContent>\n </Popover>\n </div>\n <div className=\"flex flex-col gap-3\">\n <Label htmlFor=\"time-picker\" className=\"px-1\">\n Time\n </Label>\n <Input\n type=\"time\"\n id=\"time-picker\"\n step=\"1\"\n defaultValue=\"10:30:00\"\n className=\"bg-background appearance-none [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-calendar-picker-indicator]:appearance-none\"\n />\n </div>\n </div>\n )\n}\n",
|
||||||
"type": "registry:component"
|
"type": "registry:component"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -3,7 +3,7 @@
|
|||||||
"name": "chart",
|
"name": "chart",
|
||||||
"type": "registry:ui",
|
"type": "registry:ui",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"recharts",
|
"recharts@2.15.4",
|
||||||
"lucide-react"
|
"lucide-react"
|
||||||
],
|
],
|
||||||
"registryDependencies": [
|
"registryDependencies": [
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -4,7 +4,7 @@
|
|||||||
"type": "registry:ui",
|
"type": "registry:ui",
|
||||||
"author": "shadcn (https://ui.shadcn.com)",
|
"author": "shadcn (https://ui.shadcn.com)",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"recharts",
|
"recharts@2.15.4",
|
||||||
"lucide-react"
|
"lucide-react"
|
||||||
],
|
],
|
||||||
"registryDependencies": [
|
"registryDependencies": [
|
||||||
|
|||||||
@@ -79,7 +79,7 @@
|
|||||||
{
|
{
|
||||||
"name": "calendar",
|
"name": "calendar",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"react-day-picker@8.10.1",
|
"react-day-picker",
|
||||||
"date-fns"
|
"date-fns"
|
||||||
],
|
],
|
||||||
"registryDependencies": [
|
"registryDependencies": [
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "calendar",
|
"name": "calendar",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"react-day-picker@8.10.1",
|
"react-day-picker",
|
||||||
"date-fns"
|
"date-fns"
|
||||||
],
|
],
|
||||||
"registryDependencies": [
|
"registryDependencies": [
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "calendar",
|
"name": "calendar",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"react-day-picker@8.10.1",
|
"react-day-picker",
|
||||||
"date-fns"
|
"date-fns"
|
||||||
],
|
],
|
||||||
"registryDependencies": [
|
"registryDependencies": [
|
||||||
|
|||||||
@@ -233,7 +233,7 @@ export default function DataTableDemo() {
|
|||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-md border">
|
<div className="overflow-hidden rounded-md border">
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
{table.getHeaderGroups().map((headerGroup) => (
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
|
|||||||
@@ -73,7 +73,10 @@ function Calendar({
|
|||||||
"has-focus:border-ring border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] relative rounded-md border",
|
"has-focus:border-ring border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] relative rounded-md border",
|
||||||
defaultClassNames.dropdown_root
|
defaultClassNames.dropdown_root
|
||||||
),
|
),
|
||||||
dropdown: cn("absolute inset-0 opacity-0", defaultClassNames.dropdown),
|
dropdown: cn(
|
||||||
|
"bg-popover absolute inset-0 opacity-0",
|
||||||
|
defaultClassNames.dropdown
|
||||||
|
),
|
||||||
caption_label: cn(
|
caption_label: cn(
|
||||||
"select-none font-medium",
|
"select-none font-medium",
|
||||||
captionLayout === "label"
|
captionLayout === "label"
|
||||||
|
|||||||
@@ -233,7 +233,7 @@ export default function DataTableDemo() {
|
|||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-md border">
|
<div className="overflow-hidden rounded-md border">
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
{table.getHeaderGroups().map((headerGroup) => (
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
|
|||||||
@@ -73,7 +73,10 @@ function Calendar({
|
|||||||
"has-focus:border-ring border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] relative rounded-md border",
|
"has-focus:border-ring border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] relative rounded-md border",
|
||||||
defaultClassNames.dropdown_root
|
defaultClassNames.dropdown_root
|
||||||
),
|
),
|
||||||
dropdown: cn("absolute inset-0 opacity-0", defaultClassNames.dropdown),
|
dropdown: cn(
|
||||||
|
"bg-popover absolute inset-0 opacity-0",
|
||||||
|
defaultClassNames.dropdown
|
||||||
|
),
|
||||||
caption_label: cn(
|
caption_label: cn(
|
||||||
"select-none font-medium",
|
"select-none font-medium",
|
||||||
captionLayout === "label"
|
captionLayout === "label"
|
||||||
|
|||||||
@@ -155,7 +155,7 @@ export const ui: Registry["items"] = [
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
registryDependencies: ["card"],
|
registryDependencies: ["card"],
|
||||||
dependencies: ["recharts", "lucide-react"],
|
dependencies: ["recharts@2.15.4", "lucide-react"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "checkbox",
|
name: "checkbox",
|
||||||
|
|||||||
@@ -46,7 +46,8 @@
|
|||||||
"release": "changeset version",
|
"release": "changeset version",
|
||||||
"pub:beta": "cd packages/shadcn && pnpm pub:beta",
|
"pub:beta": "cd packages/shadcn && pnpm pub:beta",
|
||||||
"pub:release": "cd packages/shadcn && pnpm pub:release",
|
"pub:release": "cd packages/shadcn && pnpm pub:release",
|
||||||
"test": "turbo run test --filter=!shadcn-ui --force"
|
"test:dev": "turbo run test --filter=!shadcn-ui --force",
|
||||||
|
"test": "start-server-and-test v4:dev http://localhost:4000 test:dev"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@9.0.6",
|
"packageManager": "pnpm@9.0.6",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -85,6 +86,7 @@
|
|||||||
"@types/node": "^20.11.27",
|
"@types/node": "^20.11.27",
|
||||||
"@types/react": "^18.2.65",
|
"@types/react": "^18.2.65",
|
||||||
"@types/react-dom": "^18.2.22",
|
"@types/react-dom": "^18.2.22",
|
||||||
|
"start-server-and-test": "^2.0.12",
|
||||||
"typescript": "^5.5.3"
|
"typescript": "^5.5.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,81 @@
|
|||||||
# @shadcn/ui
|
# @shadcn/ui
|
||||||
|
|
||||||
|
## 2.10.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- [#7902](https://github.com/shadcn-ui/ui/pull/7902) [`e6778dee87de1a183843f233b3f27fbfb1a700ec`](https://github.com/shadcn-ui/ui/commit/e6778dee87de1a183843f233b3f27fbfb1a700ec) Thanks [@shadcn](https://github.com/shadcn)! - add support for envVars in schema
|
||||||
|
|
||||||
|
- [#7896](https://github.com/shadcn-ui/ui/pull/7896) [`97a8de1c1b2ae590cc9dbe17970a882990c35a59`](https://github.com/shadcn-ui/ui/commit/97a8de1c1b2ae590cc9dbe17970a882990c35a59) Thanks [@shadcn](https://github.com/shadcn)! - add support for env vars in registry
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- [#7908](https://github.com/shadcn-ui/ui/pull/7908) [`d891132f2a0121e12c92839e19f5d90252f9a640`](https://github.com/shadcn-ui/ui/commit/d891132f2a0121e12c92839e19f5d90252f9a640) Thanks [@shadcn](https://github.com/shadcn)! - remove init tests
|
||||||
|
|
||||||
|
## 2.9.3
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- [#7837](https://github.com/shadcn-ui/ui/pull/7837) [`20e913d8e1df1acddc7bd4b8328088a25869ba7c`](https://github.com/shadcn-ui/ui/commit/20e913d8e1df1acddc7bd4b8328088a25869ba7c) Thanks [@shadcn](https://github.com/shadcn)! - fix handling of themes
|
||||||
|
|
||||||
|
## 2.9.2
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- [#7833](https://github.com/shadcn-ui/ui/pull/7833) [`d9cdc3f7ae69e571de7dc116effc381ad76685c3`](https://github.com/shadcn-ui/ui/commit/d9cdc3f7ae69e571de7dc116effc381ad76685c3) Thanks [@shadcn](https://github.com/shadcn)! - Revert "fix: handling of shouldOverwriteCssVars"
|
||||||
|
|
||||||
|
## 2.9.1
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- [#7829](https://github.com/shadcn-ui/ui/pull/7829) [`ed5237c231f3b70107131bd7ba517e73b8c9014d`](https://github.com/shadcn-ui/ui/commit/ed5237c231f3b70107131bd7ba517e73b8c9014d) Thanks [@shadcn](https://github.com/shadcn)! - fix handling of shouldOverwriteCssVars
|
||||||
|
|
||||||
|
## 2.9.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- [#7782](https://github.com/shadcn-ui/ui/pull/7782) [`06d03d64f437b543bf5fa07ccbc559f285538ffd`](https://github.com/shadcn-ui/ui/commit/06d03d64f437b543bf5fa07ccbc559f285538ffd) Thanks [@shadcn](https://github.com/shadcn)! - add universal registry items support
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- [#7795](https://github.com/shadcn-ui/ui/pull/7795) [`6c341c16aeaf5ade177a4a1ba4fb9afcd33d5fee`](https://github.com/shadcn-ui/ui/commit/6c341c16aeaf5ade177a4a1ba4fb9afcd33d5fee) Thanks [@shadcn](https://github.com/shadcn)! - fix safe target handling
|
||||||
|
|
||||||
|
- [#7757](https://github.com/shadcn-ui/ui/pull/7757) [`db93787712fe51346bf87dbae8b4cf4e38ed8c27`](https://github.com/shadcn-ui/ui/commit/db93787712fe51346bf87dbae8b4cf4e38ed8c27) Thanks [@shadcn](https://github.com/shadcn)! - implement registry path validation
|
||||||
|
|
||||||
|
## 2.8.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- [#7720](https://github.com/shadcn-ui/ui/pull/7720) [`d544a7f7a519cd5b171d9ee7cb2fd1a226659ece`](https://github.com/shadcn-ui/ui/commit/d544a7f7a519cd5b171d9ee7cb2fd1a226659ece) Thanks [@shadcn](https://github.com/shadcn)! - refactor registry dependencies resolution
|
||||||
|
|
||||||
|
- [#7717](https://github.com/shadcn-ui/ui/pull/7717) [`48fe0d709fd2b244314f95f56e7afb38b117ed8a`](https://github.com/shadcn-ui/ui/commit/48fe0d709fd2b244314f95f56e7afb38b117ed8a) Thanks [@shadcn](https://github.com/shadcn)! - add support for local registry item
|
||||||
|
|
||||||
|
- [#6330](https://github.com/shadcn-ui/ui/pull/6330) [`ed244ea0b5abf7db50ac5fcf26e2993133fe94f7`](https://github.com/shadcn-ui/ui/commit/ed244ea0b5abf7db50ac5fcf26e2993133fe94f7) Thanks [@KitsuneDev](https://github.com/KitsuneDev)! - add support for vinxi based framework
|
||||||
|
|
||||||
|
## 2.7.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- [#7540](https://github.com/shadcn-ui/ui/pull/7540) [`cb19ab84646fc017c15fadc81fc47b695560a04c`](https://github.com/shadcn-ui/ui/commit/cb19ab84646fc017c15fadc81fc47b695560a04c) Thanks [@mrzachnugent](https://github.com/mrzachnugent)! - add support for expo
|
||||||
|
|
||||||
|
- [#7640](https://github.com/shadcn-ui/ui/pull/7640) [`7c3d34cdc91681815f8897709917ec9fbcd69245`](https://github.com/shadcn-ui/ui/commit/7c3d34cdc91681815f8897709917ec9fbcd69245) Thanks [@shadcn](https://github.com/shadcn)! - add support for @plugin in css
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- [#7609](https://github.com/shadcn-ui/ui/pull/7609) [`5b8ee41511fb5ff468d9218f97b8545e145d773c`](https://github.com/shadcn-ui/ui/commit/5b8ee41511fb5ff468d9218f97b8545e145d773c) Thanks [@xabierlameiro](https://github.com/xabierlameiro)! - fix typo in function name unnsetSpreadElements
|
||||||
|
|
||||||
|
## 2.6.4
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- [#7601](https://github.com/shadcn-ui/ui/pull/7601) [`c86c27a2ffb8d186770afa42bfb62ab46e3db975`](https://github.com/shadcn-ui/ui/commit/c86c27a2ffb8d186770afa42bfb62ab46e3db975) Thanks [@schiller-manuel](https://github.com/schiller-manuel)! - fix tanstack start detection
|
||||||
|
|
||||||
|
## 2.6.3
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- [#7594](https://github.com/shadcn-ui/ui/pull/7594) [`431af4f7ff294af032c0687b8b655ed6db2e690f`](https://github.com/shadcn-ui/ui/commit/431af4f7ff294af032c0687b8b655ed6db2e690f) Thanks [@shadcn](https://github.com/shadcn)! - fix: semicolon in code style
|
||||||
|
|
||||||
## 2.6.2
|
## 2.6.2
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "shadcn",
|
"name": "shadcn",
|
||||||
"version": "2.6.2",
|
"version": "2.10.0",
|
||||||
"description": "Add components to your apps.",
|
"description": "Add components to your apps.",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public"
|
"access": "public"
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
|
import fs from "fs"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { runInit } from "@/src/commands/init"
|
import { runInit } from "@/src/commands/init"
|
||||||
import { preFlightAdd } from "@/src/preflights/preflight-add"
|
import { preFlightAdd } from "@/src/preflights/preflight-add"
|
||||||
import { getRegistryIndex, getRegistryItem, isUrl } from "@/src/registry/api"
|
import { getRegistryIndex, getRegistryItem } from "@/src/registry/api"
|
||||||
import { registryItemTypeSchema } from "@/src/registry/schema"
|
import { registryItemTypeSchema } from "@/src/registry/schema"
|
||||||
|
import {
|
||||||
|
isLocalFile,
|
||||||
|
isUniversalRegistryItem,
|
||||||
|
isUrl,
|
||||||
|
} from "@/src/registry/utils"
|
||||||
import { addComponents } from "@/src/utils/add-components"
|
import { addComponents } from "@/src/utils/add-components"
|
||||||
import { createProject } from "@/src/utils/create-project"
|
import { createProject } from "@/src/utils/create-project"
|
||||||
import * as ERRORS from "@/src/utils/errors"
|
import * as ERRORS from "@/src/utils/errors"
|
||||||
import { getConfig } from "@/src/utils/get-config"
|
import { createConfig, getConfig } from "@/src/utils/get-config"
|
||||||
import { getProjectInfo } from "@/src/utils/get-project-info"
|
import { getProjectInfo } from "@/src/utils/get-project-info"
|
||||||
import { handleError } from "@/src/utils/handle-error"
|
import { handleError } from "@/src/utils/handle-error"
|
||||||
import { highlighter } from "@/src/utils/highlighter"
|
import { highlighter } from "@/src/utils/highlighter"
|
||||||
@@ -46,10 +52,7 @@ export const addOptionsSchema = z.object({
|
|||||||
export const add = new Command()
|
export const add = new Command()
|
||||||
.name("add")
|
.name("add")
|
||||||
.description("add a component to your project")
|
.description("add a component to your project")
|
||||||
.argument(
|
.argument("[components...]", "names, url or local path to component")
|
||||||
"[components...]",
|
|
||||||
"the components to add or a url to the component."
|
|
||||||
)
|
|
||||||
.option("-y, --yes", "skip confirmation prompt.", false)
|
.option("-y, --yes", "skip confirmation prompt.", false)
|
||||||
.option("-o, --overwrite", "overwrite existing files.", false)
|
.option("-o, --overwrite", "overwrite existing files.", false)
|
||||||
.option(
|
.option(
|
||||||
@@ -80,10 +83,14 @@ export const add = new Command()
|
|||||||
})
|
})
|
||||||
|
|
||||||
let itemType: z.infer<typeof registryItemTypeSchema> | undefined
|
let itemType: z.infer<typeof registryItemTypeSchema> | undefined
|
||||||
|
let registryItem: any = null
|
||||||
|
|
||||||
if (components.length > 0 && isUrl(components[0])) {
|
if (
|
||||||
const item = await getRegistryItem(components[0], "")
|
components.length > 0 &&
|
||||||
itemType = item?.type
|
(isUrl(components[0]) || isLocalFile(components[0]))
|
||||||
|
) {
|
||||||
|
registryItem = await getRegistryItem(components[0], "")
|
||||||
|
itemType = registryItem?.type
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -129,6 +136,22 @@ export const add = new Command()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isUniversalRegistryItem(registryItem)) {
|
||||||
|
// Universal items only cares about the cwd.
|
||||||
|
if (!fs.existsSync(options.cwd)) {
|
||||||
|
throw new Error(`Directory ${options.cwd} does not exist`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const minimalConfig = createConfig({
|
||||||
|
resolvedPaths: {
|
||||||
|
cwd: options.cwd,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
await addComponents(options.components, minimalConfig, options)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
let { errors, config } = await preFlightAdd(options)
|
let { errors, config } = await preFlightAdd(options)
|
||||||
|
|
||||||
// No components.json file. Prompt the user to run init.
|
// No components.json file. Prompt the user to run init.
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import {
|
|||||||
getRegistryBaseColors,
|
getRegistryBaseColors,
|
||||||
getRegistryItem,
|
getRegistryItem,
|
||||||
getRegistryStyles,
|
getRegistryStyles,
|
||||||
isUrl,
|
|
||||||
} from "@/src/registry/api"
|
} from "@/src/registry/api"
|
||||||
|
import { isLocalFile, isUrl } from "@/src/registry/utils"
|
||||||
import { addComponents } from "@/src/utils/add-components"
|
import { addComponents } from "@/src/utils/add-components"
|
||||||
import { TEMPLATES, createProject } from "@/src/utils/create-project"
|
import { TEMPLATES, createProject } from "@/src/utils/create-project"
|
||||||
import * as ERRORS from "@/src/utils/errors"
|
import * as ERRORS from "@/src/utils/errors"
|
||||||
@@ -82,10 +82,7 @@ export const initOptionsSchema = z.object({
|
|||||||
export const init = new Command()
|
export const init = new Command()
|
||||||
.name("init")
|
.name("init")
|
||||||
.description("initialize your project and install dependencies")
|
.description("initialize your project and install dependencies")
|
||||||
.argument(
|
.argument("[components...]", "names, url or local path to component")
|
||||||
"[components...]",
|
|
||||||
"the components to add or a url to the component."
|
|
||||||
)
|
|
||||||
.option(
|
.option(
|
||||||
"-t, --template <template>",
|
"-t, --template <template>",
|
||||||
"the template to use. (next, next-monorepo)"
|
"the template to use. (next, next-monorepo)"
|
||||||
@@ -128,7 +125,10 @@ export const init = new Command()
|
|||||||
// We need to check if we're initializing with a new style.
|
// We need to check if we're initializing with a new style.
|
||||||
// We fetch the payload of the first item.
|
// We fetch the payload of the first item.
|
||||||
// This is okay since the request is cached and deduped.
|
// This is okay since the request is cached and deduped.
|
||||||
if (components.length > 0 && isUrl(components[0])) {
|
if (
|
||||||
|
components.length > 0 &&
|
||||||
|
(isUrl(components[0]) || isLocalFile(components[0]))
|
||||||
|
) {
|
||||||
const item = await getRegistryItem(components[0], "")
|
const item = await getRegistryItem(components[0], "")
|
||||||
|
|
||||||
// Skip base color if style.
|
// Skip base color if style.
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ import * as SelectPrimitive from "@radix-ui/react-select"
|
|||||||
export const Dialog = DialogPrimitive.Root
|
export const Dialog = DialogPrimitive.Root
|
||||||
export const Select = SelectPrimitive.Root`
|
export const Select = SelectPrimitive.Root`
|
||||||
|
|
||||||
const expected = `import { Dialog as DialogPrimitive, Select as SelectPrimitive } from "radix-ui";
|
const expected = `import { Dialog as DialogPrimitive, Select as SelectPrimitive } from "radix-ui"
|
||||||
|
|
||||||
export const Dialog = DialogPrimitive.Root
|
export const Dialog = DialogPrimitive.Root
|
||||||
export const Select = SelectPrimitive.Root`
|
export const Select = SelectPrimitive.Root`
|
||||||
@@ -109,7 +109,7 @@ export const DialogRoot = Root
|
|||||||
export const DialogTrigger = Trigger
|
export const DialogTrigger = Trigger
|
||||||
export const SelectContent = Content`
|
export const SelectContent = Content`
|
||||||
|
|
||||||
const expected = `import { Root, Trigger, Content } from "radix-ui";
|
const expected = `import { Root, Trigger, Content } from "radix-ui"
|
||||||
|
|
||||||
export const DialogRoot = Root
|
export const DialogRoot = Root
|
||||||
export const DialogTrigger = Trigger
|
export const DialogTrigger = Trigger
|
||||||
@@ -131,7 +131,7 @@ import { useState } from "react"
|
|||||||
export const Dialog = DialogPrimitive.Root
|
export const Dialog = DialogPrimitive.Root
|
||||||
export const Select = SelectRoot`
|
export const Select = SelectRoot`
|
||||||
|
|
||||||
const expected = `import { Dialog as DialogPrimitive, Root as SelectRoot } from "radix-ui";
|
const expected = `import { Dialog as DialogPrimitive, Root as SelectRoot } from "radix-ui"
|
||||||
|
|
||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
|
|
||||||
@@ -184,7 +184,7 @@ export const Dialog = DialogPrimitive.Root`
|
|||||||
|
|
||||||
const expected = `"use client"
|
const expected = `"use client"
|
||||||
|
|
||||||
import { Dialog as DialogPrimitive } from "radix-ui";
|
import { Dialog as DialogPrimitive } from "radix-ui"
|
||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
|
|
||||||
export const Dialog = DialogPrimitive.Root`
|
export const Dialog = DialogPrimitive.Root`
|
||||||
@@ -206,7 +206,7 @@ export const Dialog = DialogPrimitive.Root`
|
|||||||
|
|
||||||
const expected = `"use client"
|
const expected = `"use client"
|
||||||
|
|
||||||
import { DropdownMenu as DropdownMenuPrimitive, Dialog as DialogPrimitive } from "radix-ui";
|
import { DropdownMenu as DropdownMenuPrimitive, Dialog as DialogPrimitive } from "radix-ui"
|
||||||
|
|
||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
|
|
||||||
@@ -228,7 +228,7 @@ import * as SelectPrimitive from '@radix-ui/react-select'
|
|||||||
export const Dialog = DialogPrimitive.Root
|
export const Dialog = DialogPrimitive.Root
|
||||||
export const Select = SelectPrimitive.Root`
|
export const Select = SelectPrimitive.Root`
|
||||||
|
|
||||||
const expected = `import { Dialog as DialogPrimitive, Select as SelectPrimitive } from 'radix-ui';
|
const expected = `import { Dialog as DialogPrimitive, Select as SelectPrimitive } from 'radix-ui'
|
||||||
|
|
||||||
export const Dialog = DialogPrimitive.Root
|
export const Dialog = DialogPrimitive.Root
|
||||||
export const Select = SelectPrimitive.Root`
|
export const Select = SelectPrimitive.Root`
|
||||||
@@ -248,7 +248,7 @@ import * as SelectPrimitive from "@radix-ui/react-select"
|
|||||||
export const Dialog = DialogPrimitive.Root
|
export const Dialog = DialogPrimitive.Root
|
||||||
export const Select = SelectPrimitive.Root`
|
export const Select = SelectPrimitive.Root`
|
||||||
|
|
||||||
const expected = `import { Dialog as DialogPrimitive, Select as SelectPrimitive } from 'radix-ui';
|
const expected = `import { Dialog as DialogPrimitive, Select as SelectPrimitive } from 'radix-ui'
|
||||||
|
|
||||||
export const Dialog = DialogPrimitive.Root
|
export const Dialog = DialogPrimitive.Root
|
||||||
export const Select = SelectPrimitive.Root`
|
export const Select = SelectPrimitive.Root`
|
||||||
@@ -270,7 +270,7 @@ export type MyDialogProps = ComponentProps
|
|||||||
export type MySelectProps = SelectProps
|
export type MySelectProps = SelectProps
|
||||||
export const Dialog = DialogPrimitive.Root`
|
export const Dialog = DialogPrimitive.Root`
|
||||||
|
|
||||||
const expected = `import { type ComponentProps, type SelectProps, Root, Dialog as DialogPrimitive } from "radix-ui";
|
const expected = `import { type ComponentProps, type SelectProps, Root, Dialog as DialogPrimitive } from "radix-ui"
|
||||||
|
|
||||||
export type MyDialogProps = ComponentProps
|
export type MyDialogProps = ComponentProps
|
||||||
export type MySelectProps = SelectProps
|
export type MySelectProps = SelectProps
|
||||||
@@ -291,7 +291,7 @@ import { Root, Trigger } from "@radix-ui/react-dialog"
|
|||||||
export type Props = DialogProps
|
export type Props = DialogProps
|
||||||
export const DialogRoot = Root`
|
export const DialogRoot = Root`
|
||||||
|
|
||||||
const expected = `import { type DialogProps, Root, Trigger } from "radix-ui";
|
const expected = `import { type DialogProps, Root, Trigger } from "radix-ui"
|
||||||
|
|
||||||
export type Props = DialogProps
|
export type Props = DialogProps
|
||||||
export const DialogRoot = Root`
|
export const DialogRoot = Root`
|
||||||
@@ -308,7 +308,7 @@ import * as DialogPrimitive from "@radix-ui/react-dialog"
|
|||||||
export type Props = DialogTypes.ComponentProps
|
export type Props = DialogTypes.ComponentProps
|
||||||
export const Dialog = DialogPrimitive.Root`
|
export const Dialog = DialogPrimitive.Root`
|
||||||
|
|
||||||
const expected = `import { type Dialog as DialogTypes, Dialog as DialogPrimitive } from "radix-ui";
|
const expected = `import { type Dialog as DialogTypes, Dialog as DialogPrimitive } from "radix-ui"
|
||||||
|
|
||||||
export type Props = DialogTypes.ComponentProps
|
export type Props = DialogTypes.ComponentProps
|
||||||
export const Dialog = DialogPrimitive.Root`
|
export const Dialog = DialogPrimitive.Root`
|
||||||
@@ -327,7 +327,7 @@ export const Dialog = DialogPrimitive.Root
|
|||||||
export const ChevronDown = ChevronDownIcon`
|
export const ChevronDown = ChevronDownIcon`
|
||||||
|
|
||||||
const expected = `import { ChevronDownIcon, Cross2Icon } from "@radix-ui/react-icons"
|
const expected = `import { ChevronDownIcon, Cross2Icon } from "@radix-ui/react-icons"
|
||||||
import { Dialog as DialogPrimitive, Root } from "radix-ui";
|
import { Dialog as DialogPrimitive, Root } from "radix-ui"
|
||||||
|
|
||||||
export const Dialog = DialogPrimitive.Root
|
export const Dialog = DialogPrimitive.Root
|
||||||
export const ChevronDown = ChevronDownIcon`
|
export const ChevronDown = ChevronDownIcon`
|
||||||
@@ -349,7 +349,7 @@ export type MyIconProps = IconProps
|
|||||||
export type MyDialogProps = ComponentProps`
|
export type MyDialogProps = ComponentProps`
|
||||||
|
|
||||||
const expected = `import type { IconProps } from "@radix-ui/react-icons/dist/types"
|
const expected = `import type { IconProps } from "@radix-ui/react-icons/dist/types"
|
||||||
import { type ComponentProps, Root } from "radix-ui";
|
import { type ComponentProps, Root } from "radix-ui"
|
||||||
|
|
||||||
export type MyIconProps = IconProps
|
export type MyIconProps = IconProps
|
||||||
export type MyDialogProps = ComponentProps`
|
export type MyDialogProps = ComponentProps`
|
||||||
@@ -373,7 +373,7 @@ export type Props = IconProps`
|
|||||||
const expected = `import * as Icons from "@radix-ui/react-icons"
|
const expected = `import * as Icons from "@radix-ui/react-icons"
|
||||||
import { ChevronDownIcon } from "@radix-ui/react-icons"
|
import { ChevronDownIcon } from "@radix-ui/react-icons"
|
||||||
import type { IconProps } from "@radix-ui/react-icons/dist/types"
|
import type { IconProps } from "@radix-ui/react-icons/dist/types"
|
||||||
import { Dialog as DialogPrimitive, Root } from "radix-ui";
|
import { Dialog as DialogPrimitive, Root } from "radix-ui"
|
||||||
|
|
||||||
export const Dialog = DialogPrimitive.Root
|
export const Dialog = DialogPrimitive.Root
|
||||||
export const Icon = ChevronDownIcon
|
export const Icon = ChevronDownIcon
|
||||||
@@ -401,7 +401,7 @@ import {
|
|||||||
export const DialogRoot = Root
|
export const DialogRoot = Root
|
||||||
export const SelectValue = Value`
|
export const SelectValue = Value`
|
||||||
|
|
||||||
const expected = `import { Root, Trigger, Content, Value, Item } from "radix-ui";
|
const expected = `import { Root, Trigger, Content, Value, Item } from "radix-ui"
|
||||||
|
|
||||||
export const DialogRoot = Root
|
export const DialogRoot = Root
|
||||||
export const SelectValue = Value`
|
export const SelectValue = Value`
|
||||||
@@ -425,7 +425,7 @@ import * as SelectPrimitive from "@radix-ui/react-select"
|
|||||||
export const Dialog = DialogRoot
|
export const Dialog = DialogRoot
|
||||||
export const Select = SelectPrimitive.Root`
|
export const Select = SelectPrimitive.Root`
|
||||||
|
|
||||||
const expected = `import { Root as DialogRoot, Trigger, Content, Select as SelectPrimitive } from "radix-ui";
|
const expected = `import { Root as DialogRoot, Trigger, Content, Select as SelectPrimitive } from "radix-ui"
|
||||||
|
|
||||||
export const Dialog = DialogRoot
|
export const Dialog = DialogRoot
|
||||||
export const Select = SelectPrimitive.Root`
|
export const Select = SelectPrimitive.Root`
|
||||||
@@ -450,7 +450,7 @@ import {
|
|||||||
|
|
||||||
export type Props = DialogProps`
|
export type Props = DialogProps`
|
||||||
|
|
||||||
const expected = `import { type ComponentProps, type DialogProps, type SelectProps, Root } from "radix-ui";
|
const expected = `import { type ComponentProps, type DialogProps, type SelectProps, Root } from "radix-ui"
|
||||||
|
|
||||||
export type Props = DialogProps`
|
export type Props = DialogProps`
|
||||||
|
|
||||||
@@ -472,7 +472,7 @@ export type Props = DialogProps`
|
|||||||
|
|
||||||
export const DialogRoot = Root`
|
export const DialogRoot = Root`
|
||||||
|
|
||||||
const expected = `import { Root, Trigger, Content } from "radix-ui";
|
const expected = `import { Root, Trigger, Content } from "radix-ui"
|
||||||
|
|
||||||
export const DialogRoot = Root`
|
export const DialogRoot = Root`
|
||||||
|
|
||||||
@@ -490,7 +490,7 @@ export const DialogRoot = Root`
|
|||||||
|
|
||||||
export const DialogRoot = Root`
|
export const DialogRoot = Root`
|
||||||
|
|
||||||
const expected = `import { Root, Trigger, Content } from "radix-ui";
|
const expected = `import { Root, Trigger, Content } from "radix-ui"
|
||||||
|
|
||||||
export const DialogRoot = Root`
|
export const DialogRoot = Root`
|
||||||
|
|
||||||
@@ -532,7 +532,7 @@ export const Accordion = AccordionPrimitive.Root
|
|||||||
export const AlertDialog = AlertDialogPrimitive.Root
|
export const AlertDialog = AlertDialogPrimitive.Root
|
||||||
export const Button = Slot`
|
export const Button = Slot`
|
||||||
|
|
||||||
const expected = `import { Accordion as AccordionPrimitive, AlertDialog as AlertDialogPrimitive, AspectRatio as AspectRatioPrimitive, Avatar as AvatarPrimitive, Slot as SlotPrimitive, Checkbox as CheckboxPrimitive, Collapsible as CollapsiblePrimitive, ContextMenu as ContextMenuPrimitive, Dialog as DialogPrimitive, DropdownMenu as DropdownMenuPrimitive, HoverCard as HoverCardPrimitive, Label as LabelPrimitive, Menubar as MenubarPrimitive, NavigationMenu as NavigationMenuPrimitive, Popover as PopoverPrimitive, Progress as ProgressPrimitive, RadioGroup as RadioGroupPrimitive, ScrollArea as ScrollAreaPrimitive, Select as SelectPrimitive, Separator as SeparatorPrimitive, Slider as SliderPrimitive, Switch as SwitchPrimitive, Tabs as TabsPrimitive, Toggle as TogglePrimitive, ToggleGroup as ToggleGroupPrimitive, Tooltip as TooltipPrimitive } from "radix-ui";
|
const expected = `import { Accordion as AccordionPrimitive, AlertDialog as AlertDialogPrimitive, AspectRatio as AspectRatioPrimitive, Avatar as AvatarPrimitive, Slot as SlotPrimitive, Checkbox as CheckboxPrimitive, Collapsible as CollapsiblePrimitive, ContextMenu as ContextMenuPrimitive, Dialog as DialogPrimitive, DropdownMenu as DropdownMenuPrimitive, HoverCard as HoverCardPrimitive, Label as LabelPrimitive, Menubar as MenubarPrimitive, NavigationMenu as NavigationMenuPrimitive, Popover as PopoverPrimitive, Progress as ProgressPrimitive, RadioGroup as RadioGroupPrimitive, ScrollArea as ScrollAreaPrimitive, Select as SelectPrimitive, Separator as SeparatorPrimitive, Slider as SliderPrimitive, Switch as SwitchPrimitive, Tabs as TabsPrimitive, Toggle as TogglePrimitive, ToggleGroup as ToggleGroupPrimitive, Tooltip as TooltipPrimitive } from "radix-ui"
|
||||||
|
|
||||||
export const Accordion = AccordionPrimitive.Root
|
export const Accordion = AccordionPrimitive.Root
|
||||||
export const AlertDialog = AlertDialogPrimitive.Root
|
export const AlertDialog = AlertDialogPrimitive.Root
|
||||||
@@ -578,7 +578,7 @@ import { cn } from "@/lib/utils"
|
|||||||
export const Sheet = SheetPrimitive.Root
|
export const Sheet = SheetPrimitive.Root
|
||||||
export const SheetTrigger = SheetPrimitive.Trigger`
|
export const SheetTrigger = SheetPrimitive.Trigger`
|
||||||
|
|
||||||
const expected = `import { Dialog as SheetPrimitive } from "radix-ui";
|
const expected = `import { Dialog as SheetPrimitive } from "radix-ui"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
export const Sheet = SheetPrimitive.Root
|
export const Sheet = SheetPrimitive.Root
|
||||||
@@ -597,7 +597,7 @@ import { Slot } from "@radix-ui/react-slot"
|
|||||||
export const FormLabel = LabelPrimitive.Root
|
export const FormLabel = LabelPrimitive.Root
|
||||||
export const FormControl = Slot`
|
export const FormControl = Slot`
|
||||||
|
|
||||||
const expected = `import { Label as LabelPrimitive, Slot as SlotPrimitive } from "radix-ui";
|
const expected = `import { Label as LabelPrimitive, Slot as SlotPrimitive } from "radix-ui"
|
||||||
|
|
||||||
export const FormLabel = LabelPrimitive.Root
|
export const FormLabel = LabelPrimitive.Root
|
||||||
export const FormControl = SlotPrimitive.Slot`
|
export const FormControl = SlotPrimitive.Slot`
|
||||||
@@ -664,7 +664,7 @@ export const FormControl = SlotPrimitive.Slot`
|
|||||||
expect(result.replacedPackages.sort()).toEqual(allPackages.sort())
|
expect(result.replacedPackages.sort()).toEqual(allPackages.sort())
|
||||||
|
|
||||||
// Should be a single unified import from radix-ui
|
// Should be a single unified import from radix-ui
|
||||||
expect(result.content).toContain('from "radix-ui";')
|
expect(result.content).toContain('from "radix-ui"')
|
||||||
expect(result.content.startsWith("import {")).toBe(true)
|
expect(result.content.startsWith("import {")).toBe(true)
|
||||||
expect(result.content).toContain("Slot as SlotPrimitive") // Slot should be aliased as SlotPrimitive
|
expect(result.content).toContain("Slot as SlotPrimitive") // Slot should be aliased as SlotPrimitive
|
||||||
expect(result.content).toContain("Accordion as AccordionPrimitive") // Namespace should be aliased
|
expect(result.content).toContain("Accordion as AccordionPrimitive") // Namespace should be aliased
|
||||||
@@ -685,7 +685,7 @@ const Button = ({ asChild, children }) => {
|
|||||||
return <Comp>{children}</Comp>
|
return <Comp>{children}</Comp>
|
||||||
}`
|
}`
|
||||||
|
|
||||||
const expected = `import { Slot as SlotPrimitive } from "radix-ui";
|
const expected = `import { Slot as SlotPrimitive } from "radix-ui"
|
||||||
|
|
||||||
const Button = ({ asChild, children }) => {
|
const Button = ({ asChild, children }) => {
|
||||||
const Comp = asChild ? SlotPrimitive.Slot : "button"
|
const Comp = asChild ? SlotPrimitive.Slot : "button"
|
||||||
@@ -708,7 +708,7 @@ const Button = ({ asChild }) => {
|
|||||||
return null
|
return null
|
||||||
}`
|
}`
|
||||||
|
|
||||||
const expected = `import { Slot as SlotPrimitive } from "radix-ui";
|
const expected = `import { Slot as SlotPrimitive } from "radix-ui"
|
||||||
|
|
||||||
const Button = ({ asChild }) => {
|
const Button = ({ asChild }) => {
|
||||||
const Comp1 = asChild ? SlotPrimitive.Slot : "button"
|
const Comp1 = asChild ? SlotPrimitive.Slot : "button"
|
||||||
@@ -731,7 +731,7 @@ const Button = ({ asChild }) => {
|
|||||||
return null
|
return null
|
||||||
}`
|
}`
|
||||||
|
|
||||||
const expected = `import { Slot as SlotComponent } from "radix-ui";
|
const expected = `import { Slot as SlotComponent } from "radix-ui"
|
||||||
|
|
||||||
const Button = ({ asChild }) => {
|
const Button = ({ asChild }) => {
|
||||||
const Comp = asChild ? Slot : "button"
|
const Comp = asChild ? Slot : "button"
|
||||||
@@ -754,7 +754,7 @@ const Button = ({ asChild }) => {
|
|||||||
return <Slot />
|
return <Slot />
|
||||||
}`
|
}`
|
||||||
|
|
||||||
const expected = `import { Slot as SlotPrimitive } from "radix-ui";
|
const expected = `import { Slot as SlotPrimitive } from "radix-ui"
|
||||||
|
|
||||||
const Button = ({ asChild }) => {
|
const Button = ({ asChild }) => {
|
||||||
const SlotName = "Slot"
|
const SlotName = "Slot"
|
||||||
@@ -781,7 +781,7 @@ const Button = ({ asChild, ...props }: ButtonProps) => {
|
|||||||
return <Comp {...props} />
|
return <Comp {...props} />
|
||||||
}`
|
}`
|
||||||
|
|
||||||
const expected = `import { Slot as SlotPrimitive } from "radix-ui";
|
const expected = `import { Slot as SlotPrimitive } from "radix-ui"
|
||||||
import React from "react"
|
import React from "react"
|
||||||
|
|
||||||
type ButtonProps = React.ComponentProps<typeof SlotPrimitive.Slot> & {
|
type ButtonProps = React.ComponentProps<typeof SlotPrimitive.Slot> & {
|
||||||
@@ -811,7 +811,7 @@ const Button = ({ asChild, ...props }: ButtonProps) => {
|
|||||||
return <Comp {...props} />
|
return <Comp {...props} />
|
||||||
}`
|
}`
|
||||||
|
|
||||||
const expected = `import { Slot as SlotPrimitive } from "radix-ui";
|
const expected = `import { Slot as SlotPrimitive } from "radix-ui"
|
||||||
import { ComponentProps } from "react"
|
import { ComponentProps } from "react"
|
||||||
|
|
||||||
type ButtonProps = ComponentProps<typeof SlotPrimitive.Slot> & {
|
type ButtonProps = ComponentProps<typeof SlotPrimitive.Slot> & {
|
||||||
@@ -827,6 +827,46 @@ const Button = ({ asChild, ...props }: ButtonProps) => {
|
|||||||
expect(result.content.trim()).toBe(expected.trim())
|
expect(result.content.trim()).toBe(expected.trim())
|
||||||
expect(result.replacedPackages).toEqual(["@radix-ui/react-slot"])
|
expect(result.replacedPackages).toEqual(["@radix-ui/react-slot"])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("should not add double semicolons when import already ends with semicolon", async () => {
|
||||||
|
const input = `import * as DialogPrimitive from "@radix-ui/react-dialog";
|
||||||
|
import * as SelectPrimitive from "@radix-ui/react-select";
|
||||||
|
|
||||||
|
export const Dialog = DialogPrimitive.Root
|
||||||
|
export const Select = SelectPrimitive.Root`
|
||||||
|
|
||||||
|
const expected = `import { Dialog as DialogPrimitive, Select as SelectPrimitive } from "radix-ui";
|
||||||
|
|
||||||
|
export const Dialog = DialogPrimitive.Root
|
||||||
|
export const Select = SelectPrimitive.Root`
|
||||||
|
|
||||||
|
const result = await migrateRadixFile(input)
|
||||||
|
expect(result.content.trim()).toBe(expected.trim())
|
||||||
|
expect(result.replacedPackages).toEqual([
|
||||||
|
"@radix-ui/react-dialog",
|
||||||
|
"@radix-ui/react-select",
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should not add semicolon when original imports don't have semicolons", async () => {
|
||||||
|
const input = `import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||||
|
import * as SelectPrimitive from "@radix-ui/react-select"
|
||||||
|
|
||||||
|
export const Dialog = DialogPrimitive.Root
|
||||||
|
export const Select = SelectPrimitive.Root`
|
||||||
|
|
||||||
|
const expected = `import { Dialog as DialogPrimitive, Select as SelectPrimitive } from "radix-ui"
|
||||||
|
|
||||||
|
export const Dialog = DialogPrimitive.Root
|
||||||
|
export const Select = SelectPrimitive.Root`
|
||||||
|
|
||||||
|
const result = await migrateRadixFile(input)
|
||||||
|
expect(result.content.trim()).toBe(expected.trim())
|
||||||
|
expect(result.replacedPackages).toEqual([
|
||||||
|
"@radix-ui/react-dialog",
|
||||||
|
"@radix-ui/react-select",
|
||||||
|
])
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("migrateRadix - package.json updates", () => {
|
describe("migrateRadix - package.json updates", () => {
|
||||||
|
|||||||
@@ -215,13 +215,15 @@ export async function migrateRadixFile(
|
|||||||
content: string
|
content: string
|
||||||
): Promise<{ content: string; replacedPackages: string[] }> {
|
): Promise<{ content: string; replacedPackages: string[] }> {
|
||||||
// Enhanced regex to handle type-only imports, but exclude react-icons
|
// Enhanced regex to handle type-only imports, but exclude react-icons
|
||||||
|
// Also capture optional semicolon at the end
|
||||||
const radixImportPattern =
|
const radixImportPattern =
|
||||||
/import\s+(?:(type)\s+)?(?:\*\s+as\s+(\w+)|{([^}]+)})\s+from\s+(["'])@radix-ui\/react-([^"']+)\4/g
|
/import\s+(?:(type)\s+)?(?:\*\s+as\s+(\w+)|{([^}]+)})\s+from\s+(["'])@radix-ui\/react-([^"']+)\4(;?)/g
|
||||||
|
|
||||||
const imports: Array<{ name: string; alias?: string; isType?: boolean }> = []
|
const imports: Array<{ name: string; alias?: string; isType?: boolean }> = []
|
||||||
const linesToRemove: string[] = []
|
const linesToRemove: string[] = []
|
||||||
const replacedPackages: string[] = []
|
const replacedPackages: string[] = []
|
||||||
let quoteStyle = '"' // Default to double quotes
|
let quoteStyle = '"' // Default to double quotes
|
||||||
|
let hasSemicolon = false // Track if any import had a semicolon
|
||||||
|
|
||||||
let result = content
|
let result = content
|
||||||
let match
|
let match
|
||||||
@@ -235,6 +237,7 @@ export async function migrateRadixFile(
|
|||||||
namedImports,
|
namedImports,
|
||||||
quote,
|
quote,
|
||||||
packageName,
|
packageName,
|
||||||
|
semicolon,
|
||||||
] = match
|
] = match
|
||||||
|
|
||||||
// Skip react-icons package and any sub-paths (like react-icons/dist/types)
|
// Skip react-icons package and any sub-paths (like react-icons/dist/types)
|
||||||
@@ -244,9 +247,10 @@ export async function migrateRadixFile(
|
|||||||
|
|
||||||
linesToRemove.push(fullMatch)
|
linesToRemove.push(fullMatch)
|
||||||
|
|
||||||
// Use the quote style from the first import
|
// Use the quote style and semicolon style from the first import
|
||||||
if (linesToRemove.length === 1) {
|
if (linesToRemove.length === 1) {
|
||||||
quoteStyle = quote
|
quoteStyle = quote
|
||||||
|
hasSemicolon = semicolon === ";"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Track which package we're replacing
|
// Track which package we're replacing
|
||||||
@@ -301,7 +305,9 @@ export async function migrateRadixFile(
|
|||||||
})
|
})
|
||||||
.join(", ")
|
.join(", ")
|
||||||
|
|
||||||
const unifiedImport = `import { ${importList} } from ${quoteStyle}radix-ui${quoteStyle};`
|
const unifiedImport = `import { ${importList} } from ${quoteStyle}radix-ui${quoteStyle}${
|
||||||
|
hasSemicolon ? ";" : ""
|
||||||
|
}`
|
||||||
|
|
||||||
// Replace first import with unified import, remove the rest
|
// Replace first import with unified import, remove the rest
|
||||||
result = linesToRemove.reduce((acc, line, index) => {
|
result = linesToRemove.reduce((acc, line, index) => {
|
||||||
|
|||||||
@@ -1,12 +1,54 @@
|
|||||||
|
import { promises as fs } from "fs"
|
||||||
|
import { tmpdir } from "os"
|
||||||
|
import path from "path"
|
||||||
import { HttpResponse, http } from "msw"
|
import { HttpResponse, http } from "msw"
|
||||||
import { setupServer } from "msw/node"
|
import { setupServer } from "msw/node"
|
||||||
import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest"
|
import {
|
||||||
|
afterAll,
|
||||||
|
afterEach,
|
||||||
|
beforeAll,
|
||||||
|
describe,
|
||||||
|
expect,
|
||||||
|
it,
|
||||||
|
vi,
|
||||||
|
} from "vitest"
|
||||||
|
|
||||||
import { clearRegistryCache, fetchRegistry } from "./api"
|
import {
|
||||||
|
clearRegistryCache,
|
||||||
|
fetchRegistry,
|
||||||
|
getRegistryItem,
|
||||||
|
registryResolveItemsTree,
|
||||||
|
} from "./api"
|
||||||
|
|
||||||
|
// Mock the handleError function to prevent process.exit in tests
|
||||||
|
vi.mock("@/src/utils/handle-error", () => ({
|
||||||
|
handleError: vi.fn(),
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Mock the logger to prevent console output in tests
|
||||||
|
vi.mock("@/src/utils/logger", () => ({
|
||||||
|
logger: {
|
||||||
|
error: vi.fn(),
|
||||||
|
break: vi.fn(),
|
||||||
|
log: vi.fn(),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
const REGISTRY_URL = "https://ui.shadcn.com/r"
|
const REGISTRY_URL = "https://ui.shadcn.com/r"
|
||||||
|
|
||||||
const server = setupServer(
|
const server = setupServer(
|
||||||
|
http.get(`${REGISTRY_URL}/index.json`, () => {
|
||||||
|
return HttpResponse.json([
|
||||||
|
{
|
||||||
|
name: "button",
|
||||||
|
type: "registry:ui",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "card",
|
||||||
|
type: "registry:ui",
|
||||||
|
},
|
||||||
|
])
|
||||||
|
}),
|
||||||
http.get(`${REGISTRY_URL}/styles/new-york/button.json`, () => {
|
http.get(`${REGISTRY_URL}/styles/new-york/button.json`, () => {
|
||||||
return HttpResponse.json({
|
return HttpResponse.json({
|
||||||
name: "button",
|
name: "button",
|
||||||
@@ -112,3 +154,277 @@ describe("fetchRegistry", () => {
|
|||||||
expect(result[1]).toMatchObject({ name: "card" })
|
expect(result[1]).toMatchObject({ name: "card" })
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("getRegistryItem with local files", () => {
|
||||||
|
it("should read and parse a valid local JSON file", async () => {
|
||||||
|
// Create a temporary file
|
||||||
|
const tempDir = await fs.mkdtemp(path.join(tmpdir(), "shadcn-test-"))
|
||||||
|
const tempFile = path.join(tempDir, "test-component.json")
|
||||||
|
|
||||||
|
const componentData = {
|
||||||
|
name: "test-component",
|
||||||
|
type: "registry:ui",
|
||||||
|
dependencies: ["@radix-ui/react-dialog"],
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
path: "ui/test-component.tsx",
|
||||||
|
content: "// test component content",
|
||||||
|
type: "registry:ui",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
await fs.writeFile(tempFile, JSON.stringify(componentData, null, 2))
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await getRegistryItem(tempFile, "unused-style")
|
||||||
|
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
name: "test-component",
|
||||||
|
type: "registry:ui",
|
||||||
|
dependencies: ["@radix-ui/react-dialog"],
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
path: "ui/test-component.tsx",
|
||||||
|
content: "// test component content",
|
||||||
|
type: "registry:ui",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
// Clean up
|
||||||
|
await fs.unlink(tempFile)
|
||||||
|
await fs.rmdir(tempDir)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should handle relative paths", async () => {
|
||||||
|
const tempDir = await fs.mkdtemp(path.join(tmpdir(), "shadcn-test-"))
|
||||||
|
const tempFile = path.join(tempDir, "relative-component.json")
|
||||||
|
|
||||||
|
const componentData = {
|
||||||
|
name: "relative-component",
|
||||||
|
type: "registry:ui",
|
||||||
|
files: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
await fs.writeFile(tempFile, JSON.stringify(componentData))
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Change to temp directory to test relative path
|
||||||
|
const originalCwd = process.cwd()
|
||||||
|
process.chdir(tempDir)
|
||||||
|
|
||||||
|
const result = await getRegistryItem(
|
||||||
|
"./relative-component.json",
|
||||||
|
"unused-style"
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
name: "relative-component",
|
||||||
|
type: "registry:ui",
|
||||||
|
})
|
||||||
|
|
||||||
|
process.chdir(originalCwd)
|
||||||
|
} finally {
|
||||||
|
// Clean up
|
||||||
|
await fs.unlink(tempFile)
|
||||||
|
await fs.rmdir(tempDir)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should handle tilde (~) home directory paths", async () => {
|
||||||
|
const os = await import("os")
|
||||||
|
const homeDir = os.homedir()
|
||||||
|
const tempFile = path.join(homeDir, "shadcn-test-tilde.json")
|
||||||
|
|
||||||
|
const componentData = {
|
||||||
|
name: "tilde-component",
|
||||||
|
type: "registry:ui",
|
||||||
|
files: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
await fs.writeFile(tempFile, JSON.stringify(componentData))
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Test with tilde path
|
||||||
|
const tildeePath = "~/shadcn-test-tilde.json"
|
||||||
|
const result = await getRegistryItem(tildeePath, "unused-style")
|
||||||
|
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
name: "tilde-component",
|
||||||
|
type: "registry:ui",
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
// Clean up
|
||||||
|
await fs.unlink(tempFile)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return null for non-existent files", async () => {
|
||||||
|
const result = await getRegistryItem(
|
||||||
|
"/non/existent/file.json",
|
||||||
|
"unused-style"
|
||||||
|
)
|
||||||
|
expect(result).toBe(null)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return null for invalid JSON", async () => {
|
||||||
|
const tempDir = await fs.mkdtemp(path.join(tmpdir(), "shadcn-test-"))
|
||||||
|
const tempFile = path.join(tempDir, "invalid.json")
|
||||||
|
|
||||||
|
await fs.writeFile(tempFile, "{ invalid json }")
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await getRegistryItem(tempFile, "unused-style")
|
||||||
|
expect(result).toBe(null)
|
||||||
|
} finally {
|
||||||
|
// Clean up
|
||||||
|
await fs.unlink(tempFile)
|
||||||
|
await fs.rmdir(tempDir)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return null for JSON that doesn't match registry schema", async () => {
|
||||||
|
const tempDir = await fs.mkdtemp(path.join(tmpdir(), "shadcn-test-"))
|
||||||
|
const tempFile = path.join(tempDir, "invalid-schema.json")
|
||||||
|
|
||||||
|
const invalidData = {
|
||||||
|
notAValidRegistryItem: true,
|
||||||
|
missing: "required fields",
|
||||||
|
}
|
||||||
|
|
||||||
|
await fs.writeFile(tempFile, JSON.stringify(invalidData))
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await getRegistryItem(tempFile, "unused-style")
|
||||||
|
expect(result).toBe(null)
|
||||||
|
} finally {
|
||||||
|
// Clean up
|
||||||
|
await fs.unlink(tempFile)
|
||||||
|
await fs.rmdir(tempDir)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should still handle URLs and component names", async () => {
|
||||||
|
// Test that existing functionality still works
|
||||||
|
const result = await getRegistryItem("button", "new-york")
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
name: "button",
|
||||||
|
type: "registry:ui",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should handle local files with URL dependencies", async () => {
|
||||||
|
// Mock a URL endpoint for dependency
|
||||||
|
const dependencyUrl = "https://example.com/dependency.json"
|
||||||
|
server.use(
|
||||||
|
http.get(dependencyUrl, () => {
|
||||||
|
return HttpResponse.json({
|
||||||
|
name: "url-dependency",
|
||||||
|
type: "registry:ui",
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
path: "ui/url-dependency.tsx",
|
||||||
|
content: "// url dependency content",
|
||||||
|
type: "registry:ui",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const tempDir = await fs.mkdtemp(path.join(tmpdir(), "shadcn-test-"))
|
||||||
|
const tempFile = path.join(tempDir, "component-with-url-deps.json")
|
||||||
|
|
||||||
|
const componentData = {
|
||||||
|
name: "component-with-url-deps",
|
||||||
|
type: "registry:ui",
|
||||||
|
registryDependencies: [dependencyUrl, "button"], // Mix of URL and registry name
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
path: "ui/component-with-url-deps.tsx",
|
||||||
|
content: "// component with url deps content",
|
||||||
|
type: "registry:ui",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
await fs.writeFile(tempFile, JSON.stringify(componentData, null, 2))
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await getRegistryItem(tempFile, "unused-style")
|
||||||
|
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
name: "component-with-url-deps",
|
||||||
|
type: "registry:ui",
|
||||||
|
registryDependencies: [dependencyUrl, "button"],
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
// Clean up
|
||||||
|
await fs.unlink(tempFile)
|
||||||
|
await fs.rmdir(tempDir)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("registryResolveItemsTree with URL dependencies", () => {
|
||||||
|
it("should resolve URL dependencies from local files", async () => {
|
||||||
|
// Mock a URL endpoint for dependency
|
||||||
|
const dependencyUrl = "https://example.com/dependency.json"
|
||||||
|
server.use(
|
||||||
|
http.get(dependencyUrl, () => {
|
||||||
|
return HttpResponse.json({
|
||||||
|
name: "url-dependency",
|
||||||
|
type: "registry:ui",
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
path: "ui/url-dependency.tsx",
|
||||||
|
content: "// url dependency content",
|
||||||
|
type: "registry:ui",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const tempDir = await fs.mkdtemp(path.join(tmpdir(), "shadcn-test-"))
|
||||||
|
const tempFile = path.join(tempDir, "component-with-url-deps.json")
|
||||||
|
|
||||||
|
const componentData = {
|
||||||
|
name: "component-with-url-deps",
|
||||||
|
type: "registry:ui",
|
||||||
|
registryDependencies: [dependencyUrl], // URL dependency
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
path: "ui/component-with-url-deps.tsx",
|
||||||
|
content: "// component with url deps content",
|
||||||
|
type: "registry:ui",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
await fs.writeFile(tempFile, JSON.stringify(componentData, null, 2))
|
||||||
|
|
||||||
|
try {
|
||||||
|
const mockConfig = {
|
||||||
|
style: "new-york",
|
||||||
|
tailwind: { baseColor: "neutral", cssVariables: true },
|
||||||
|
resolvedPaths: { cwd: process.cwd() },
|
||||||
|
} as any
|
||||||
|
|
||||||
|
const result = await registryResolveItemsTree([tempFile], mockConfig)
|
||||||
|
|
||||||
|
expect(result).toBeDefined()
|
||||||
|
expect(result?.files).toBeDefined()
|
||||||
|
// Should contain files from both the main component and its URL dependency
|
||||||
|
const filePaths = result?.files?.map((f: any) => f.path) ?? []
|
||||||
|
expect(filePaths).toContain("ui/component-with-url-deps.tsx")
|
||||||
|
expect(filePaths).toContain("ui/url-dependency.tsx")
|
||||||
|
} finally {
|
||||||
|
// Clean up
|
||||||
|
await fs.unlink(tempFile)
|
||||||
|
await fs.rmdir(tempDir)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
|
import { promises as fs } from "fs"
|
||||||
|
import { homedir } from "os"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
|
import { isLocalFile } from "@/src/registry/utils"
|
||||||
import { Config, getTargetStyleFromConfig } from "@/src/utils/get-config"
|
import { Config, getTargetStyleFromConfig } from "@/src/utils/get-config"
|
||||||
import { getProjectTailwindVersionFromConfig } from "@/src/utils/get-project-info"
|
import { getProjectTailwindVersionFromConfig } from "@/src/utils/get-project-info"
|
||||||
import { handleError } from "@/src/utils/handle-error"
|
import { handleError } from "@/src/utils/handle-error"
|
||||||
@@ -85,6 +88,12 @@ export async function getRegistryIcons() {
|
|||||||
|
|
||||||
export async function getRegistryItem(name: string, style: string) {
|
export async function getRegistryItem(name: string, style: string) {
|
||||||
try {
|
try {
|
||||||
|
// Handle local file paths
|
||||||
|
if (isLocalFile(name)) {
|
||||||
|
return await getLocalRegistryItem(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle URLs and component names
|
||||||
const [result] = await fetchRegistry([
|
const [result] = await fetchRegistry([
|
||||||
isUrl(name) ? name : `styles/${style}/${name}.json`,
|
isUrl(name) ? name : `styles/${style}/${name}.json`,
|
||||||
])
|
])
|
||||||
@@ -97,6 +106,26 @@ export async function getRegistryItem(name: string, style: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getLocalRegistryItem(filePath: string) {
|
||||||
|
try {
|
||||||
|
// Handle tilde expansion for home directory
|
||||||
|
let expandedPath = filePath
|
||||||
|
if (filePath.startsWith("~/")) {
|
||||||
|
expandedPath = path.join(homedir(), filePath.slice(2))
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolvedPath = path.resolve(expandedPath)
|
||||||
|
const content = await fs.readFile(resolvedPath, "utf8")
|
||||||
|
const parsed = JSON.parse(content)
|
||||||
|
|
||||||
|
return registryItemSchema.parse(parsed)
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Failed to read local registry file: ${filePath}`)
|
||||||
|
handleError(error)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function getRegistryBaseColors() {
|
export async function getRegistryBaseColors() {
|
||||||
return BASE_COLORS
|
return BASE_COLORS
|
||||||
}
|
}
|
||||||
@@ -263,26 +292,144 @@ export function clearRegistryCache() {
|
|||||||
registryCache.clear()
|
registryCache.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function resolveDependenciesRecursively(
|
||||||
|
dependencies: string[],
|
||||||
|
config?: Config,
|
||||||
|
visited: Set<string> = new Set()
|
||||||
|
): Promise<{
|
||||||
|
items: z.infer<typeof registryItemSchema>[]
|
||||||
|
registryNames: string[]
|
||||||
|
}> {
|
||||||
|
const items: z.infer<typeof registryItemSchema>[] = []
|
||||||
|
const registryNames: string[] = []
|
||||||
|
|
||||||
|
for (const dep of dependencies) {
|
||||||
|
// Avoid infinite recursion.
|
||||||
|
if (visited.has(dep)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
visited.add(dep)
|
||||||
|
|
||||||
|
if (isUrl(dep) || isLocalFile(dep)) {
|
||||||
|
const item = await getRegistryItem(dep, "")
|
||||||
|
if (item) {
|
||||||
|
items.push(item)
|
||||||
|
if (item.registryDependencies) {
|
||||||
|
const nested = await resolveDependenciesRecursively(
|
||||||
|
item.registryDependencies,
|
||||||
|
config,
|
||||||
|
visited
|
||||||
|
)
|
||||||
|
items.push(...nested.items)
|
||||||
|
registryNames.push(...nested.registryNames)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Registry name - add it to the list
|
||||||
|
registryNames.push(dep)
|
||||||
|
|
||||||
|
// If we have config, we can also fetch the item to get its dependencies
|
||||||
|
if (config) {
|
||||||
|
const style = config.resolvedPaths?.cwd
|
||||||
|
? await getTargetStyleFromConfig(
|
||||||
|
config.resolvedPaths.cwd,
|
||||||
|
config.style
|
||||||
|
)
|
||||||
|
: config.style
|
||||||
|
|
||||||
|
try {
|
||||||
|
const item = await getRegistryItem(dep, style)
|
||||||
|
if (item && item.registryDependencies) {
|
||||||
|
const nested = await resolveDependenciesRecursively(
|
||||||
|
item.registryDependencies,
|
||||||
|
config,
|
||||||
|
visited
|
||||||
|
)
|
||||||
|
items.push(...nested.items)
|
||||||
|
registryNames.push(...nested.registryNames)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// If we can't fetch the registry item, that's okay - we'll still include the name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { items, registryNames }
|
||||||
|
}
|
||||||
|
|
||||||
export async function registryResolveItemsTree(
|
export async function registryResolveItemsTree(
|
||||||
names: z.infer<typeof registryItemSchema>["name"][],
|
names: z.infer<typeof registryItemSchema>["name"][],
|
||||||
config: Config
|
config: Config
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const index = await getRegistryIndex()
|
// Separate local files, URLs, and registry names.
|
||||||
if (!index) {
|
const localFiles = names.filter((name) => isLocalFile(name))
|
||||||
return null
|
const urls = names.filter((name) => isUrl(name))
|
||||||
|
const registryNames = names.filter(
|
||||||
|
(name) => !isLocalFile(name) && !isUrl(name)
|
||||||
|
)
|
||||||
|
|
||||||
|
const payload: z.infer<typeof registryItemSchema>[] = []
|
||||||
|
|
||||||
|
// Handle local files and URLs directly, collecting their dependencies.
|
||||||
|
const allDependencies: string[] = []
|
||||||
|
|
||||||
|
for (const localFile of localFiles) {
|
||||||
|
const item = await getRegistryItem(localFile, "")
|
||||||
|
if (item) {
|
||||||
|
payload.push(item)
|
||||||
|
if (item.registryDependencies) {
|
||||||
|
allDependencies.push(...item.registryDependencies)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we're resolving the index, we want it to go first.
|
for (const url of urls) {
|
||||||
if (names.includes("index")) {
|
const item = await getRegistryItem(url, "")
|
||||||
names.unshift("index")
|
if (item) {
|
||||||
|
payload.push(item)
|
||||||
|
if (item.registryDependencies) {
|
||||||
|
allDependencies.push(...item.registryDependencies)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let registryItems = await resolveRegistryItems(names, config)
|
// Recursively resolve all dependencies.
|
||||||
let result = await fetchRegistry(registryItems)
|
const { items: dependencyItems, registryNames: dependencyRegistryNames } =
|
||||||
const payload = z.array(registryItemSchema).parse(result)
|
await resolveDependenciesRecursively(allDependencies, config)
|
||||||
|
|
||||||
if (!payload) {
|
payload.push(...dependencyItems)
|
||||||
|
|
||||||
|
// Handle registry names using existing resolveRegistryItems logic.
|
||||||
|
const allRegistryNames = [...registryNames, ...dependencyRegistryNames]
|
||||||
|
if (allRegistryNames.length > 0) {
|
||||||
|
const index = await getRegistryIndex()
|
||||||
|
if (!index) {
|
||||||
|
// If we only have local files or URLs, that's fine.
|
||||||
|
if (payload.length === 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Remove duplicates.
|
||||||
|
const uniqueRegistryNames = Array.from(new Set(allRegistryNames))
|
||||||
|
|
||||||
|
// If we're resolving the index, we want it to go first.
|
||||||
|
if (uniqueRegistryNames.includes("index")) {
|
||||||
|
uniqueRegistryNames.unshift("index")
|
||||||
|
}
|
||||||
|
|
||||||
|
let registryItems = await resolveRegistryItems(
|
||||||
|
uniqueRegistryNames,
|
||||||
|
config
|
||||||
|
)
|
||||||
|
let result = await fetchRegistry(registryItems)
|
||||||
|
const registryPayload = z.array(registryItemSchema).parse(result)
|
||||||
|
payload.push(...registryPayload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!payload.length) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -290,7 +437,7 @@ export async function registryResolveItemsTree(
|
|||||||
// the theme item if a base color is provided.
|
// the theme item if a base color is provided.
|
||||||
// We do this for index only.
|
// We do this for index only.
|
||||||
// Other components will ship with their theme tokens.
|
// Other components will ship with their theme tokens.
|
||||||
if (names.includes("index")) {
|
if (allRegistryNames.includes("index")) {
|
||||||
if (config.tailwind.baseColor) {
|
if (config.tailwind.baseColor) {
|
||||||
const theme = await registryGetTheme(config.tailwind.baseColor, config)
|
const theme = await registryGetTheme(config.tailwind.baseColor, config)
|
||||||
if (theme) {
|
if (theme) {
|
||||||
@@ -329,7 +476,12 @@ export async function registryResolveItemsTree(
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return registryResolvedItemsTreeSchema.parse({
|
let envVars = {}
|
||||||
|
payload.forEach((item) => {
|
||||||
|
envVars = deepmerge(envVars, item.envVars ?? {})
|
||||||
|
})
|
||||||
|
|
||||||
|
const parsed = registryResolvedItemsTreeSchema.parse({
|
||||||
dependencies: deepmerge.all(
|
dependencies: deepmerge.all(
|
||||||
payload.map((item) => item.dependencies ?? [])
|
payload.map((item) => item.dependencies ?? [])
|
||||||
),
|
),
|
||||||
@@ -342,6 +494,12 @@ export async function registryResolveItemsTree(
|
|||||||
css,
|
css,
|
||||||
docs,
|
docs,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (Object.keys(envVars).length > 0) {
|
||||||
|
parsed.envVars = envVars
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsed
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error)
|
handleError(error)
|
||||||
return null
|
return null
|
||||||
@@ -352,44 +510,17 @@ async function resolveRegistryDependencies(
|
|||||||
url: string,
|
url: string,
|
||||||
config: Config
|
config: Config
|
||||||
): Promise<string[]> {
|
): Promise<string[]> {
|
||||||
const visited = new Set<string>()
|
const { registryNames } = await resolveDependenciesRecursively([url], config)
|
||||||
const payload: string[] = []
|
|
||||||
|
|
||||||
const style = config.resolvedPaths?.cwd
|
const style = config.resolvedPaths?.cwd
|
||||||
? await getTargetStyleFromConfig(config.resolvedPaths.cwd, config.style)
|
? await getTargetStyleFromConfig(config.resolvedPaths.cwd, config.style)
|
||||||
: config.style
|
: config.style
|
||||||
|
|
||||||
async function resolveDependencies(itemUrl: string) {
|
const urls = registryNames.map((name) =>
|
||||||
const url = getRegistryUrl(
|
getRegistryUrl(isUrl(name) ? name : `styles/${style}/${name}.json`)
|
||||||
isUrl(itemUrl) ? itemUrl : `styles/${style}/${itemUrl}.json`
|
)
|
||||||
)
|
|
||||||
|
|
||||||
if (visited.has(url)) {
|
return Array.from(new Set(urls))
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
visited.add(url)
|
|
||||||
|
|
||||||
try {
|
|
||||||
const [result] = await fetchRegistry([url])
|
|
||||||
const item = registryItemSchema.parse(result)
|
|
||||||
payload.push(url)
|
|
||||||
|
|
||||||
if (item.registryDependencies) {
|
|
||||||
for (const dependency of item.registryDependencies) {
|
|
||||||
await resolveDependencies(dependency)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(
|
|
||||||
`Error fetching or parsing registry item at ${itemUrl}:`,
|
|
||||||
error
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await resolveDependencies(url)
|
|
||||||
return Array.from(new Set(payload))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function registryGetTheme(name: string, config: Config) {
|
export async function registryGetTheme(name: string, config: Config) {
|
||||||
@@ -495,7 +626,13 @@ export function isUrl(path: string) {
|
|||||||
// TODO: We're double-fetching here. Use a cache.
|
// TODO: We're double-fetching here. Use a cache.
|
||||||
export async function resolveRegistryItems(names: string[], config: Config) {
|
export async function resolveRegistryItems(names: string[], config: Config) {
|
||||||
let registryDependencies: string[] = []
|
let registryDependencies: string[] = []
|
||||||
for (const name of names) {
|
|
||||||
|
// Filter out local files and URLs - these should be handled directly by getRegistryItem
|
||||||
|
const registryNames = names.filter(
|
||||||
|
(name) => !isLocalFile(name) && !isUrl(name)
|
||||||
|
)
|
||||||
|
|
||||||
|
for (const name of registryNames) {
|
||||||
const itemRegistryDependencies = await resolveRegistryDependencies(
|
const itemRegistryDependencies = await resolveRegistryDependencies(
|
||||||
name,
|
name,
|
||||||
config
|
config
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export const registryItemTypeSchema = z.enum([
|
|||||||
"registry:file",
|
"registry:file",
|
||||||
"registry:theme",
|
"registry:theme",
|
||||||
"registry:style",
|
"registry:style",
|
||||||
|
"registry:item",
|
||||||
|
|
||||||
// Internal use only
|
// Internal use only
|
||||||
"registry:example",
|
"registry:example",
|
||||||
@@ -64,6 +65,8 @@ export const registryItemCssSchema = z.record(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export const registryItemEnvVarsSchema = z.record(z.string(), z.string())
|
||||||
|
|
||||||
export const registryItemSchema = z.object({
|
export const registryItemSchema = z.object({
|
||||||
$schema: z.string().optional(),
|
$schema: z.string().optional(),
|
||||||
extends: z.string().optional(),
|
extends: z.string().optional(),
|
||||||
@@ -79,6 +82,7 @@ export const registryItemSchema = z.object({
|
|||||||
tailwind: registryItemTailwindSchema.optional(),
|
tailwind: registryItemTailwindSchema.optional(),
|
||||||
cssVars: registryItemCssVarsSchema.optional(),
|
cssVars: registryItemCssVarsSchema.optional(),
|
||||||
css: registryItemCssSchema.optional(),
|
css: registryItemCssSchema.optional(),
|
||||||
|
envVars: registryItemEnvVarsSchema.optional(),
|
||||||
meta: z.record(z.string(), z.any()).optional(),
|
meta: z.record(z.string(), z.any()).optional(),
|
||||||
docs: z.string().optional(),
|
docs: z.string().optional(),
|
||||||
categories: z.array(z.string()).optional(),
|
categories: z.array(z.string()).optional(),
|
||||||
@@ -126,5 +130,6 @@ export const registryResolvedItemsTreeSchema = registryItemSchema.pick({
|
|||||||
tailwind: true,
|
tailwind: true,
|
||||||
cssVars: true,
|
cssVars: true,
|
||||||
css: true,
|
css: true,
|
||||||
|
envVars: true,
|
||||||
docs: true,
|
docs: true,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
import { describe, expect, it } from "vitest"
|
import { describe, expect, it } from "vitest"
|
||||||
|
|
||||||
import { getDependencyFromModuleSpecifier } from "./utils"
|
import {
|
||||||
|
getDependencyFromModuleSpecifier,
|
||||||
|
isLocalFile,
|
||||||
|
isUniversalRegistryItem,
|
||||||
|
isUrl,
|
||||||
|
} from "./utils"
|
||||||
|
|
||||||
describe("getDependencyFromModuleSpecifier", () => {
|
describe("getDependencyFromModuleSpecifier", () => {
|
||||||
it("should return the first part of a non-scoped package with path", () => {
|
it("should return the first part of a non-scoped package with path", () => {
|
||||||
@@ -74,3 +79,263 @@ describe("getDependencyFromModuleSpecifier", () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("isUrl", () => {
|
||||||
|
it("should return true for valid URLs", () => {
|
||||||
|
expect(isUrl("https://example.com")).toBe(true)
|
||||||
|
expect(isUrl("http://example.com")).toBe(true)
|
||||||
|
expect(isUrl("https://example.com/path")).toBe(true)
|
||||||
|
expect(isUrl("https://subdomain.example.com")).toBe(true)
|
||||||
|
expect(isUrl("https://ui.shadcn.com/r/styles/new-york/button.json")).toBe(
|
||||||
|
true
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return false for non-URLs", () => {
|
||||||
|
expect(isUrl("./local-file.json")).toBe(false)
|
||||||
|
expect(isUrl("../relative/path.json")).toBe(false)
|
||||||
|
expect(isUrl("/absolute/path.json")).toBe(false)
|
||||||
|
expect(isUrl("component-name")).toBe(false)
|
||||||
|
expect(isUrl("")).toBe(false)
|
||||||
|
expect(isUrl("just-text")).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("isLocalFile", () => {
|
||||||
|
it("should return true for local JSON files", () => {
|
||||||
|
expect(isLocalFile("./component.json")).toBe(true)
|
||||||
|
expect(isLocalFile("../shared/button.json")).toBe(true)
|
||||||
|
expect(isLocalFile("/absolute/path/card.json")).toBe(true)
|
||||||
|
expect(isLocalFile("local-component.json")).toBe(true)
|
||||||
|
expect(isLocalFile("nested/directory/dialog.json")).toBe(true)
|
||||||
|
expect(isLocalFile("~/Desktop/component.json")).toBe(true)
|
||||||
|
expect(isLocalFile("~/Documents/shared/button.json")).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return false for URLs ending with .json", () => {
|
||||||
|
expect(isLocalFile("https://example.com/component.json")).toBe(false)
|
||||||
|
expect(isLocalFile("http://registry.com/button.json")).toBe(false)
|
||||||
|
expect(
|
||||||
|
isLocalFile("https://ui.shadcn.com/r/styles/new-york/button.json")
|
||||||
|
).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return false for non-JSON files", () => {
|
||||||
|
expect(isLocalFile("./component.tsx")).toBe(false)
|
||||||
|
expect(isLocalFile("../shared/button.ts")).toBe(false)
|
||||||
|
expect(isLocalFile("/absolute/path/card.js")).toBe(false)
|
||||||
|
expect(isLocalFile("local-component.css")).toBe(false)
|
||||||
|
expect(isLocalFile("component-name")).toBe(false)
|
||||||
|
expect(isLocalFile("")).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return false for directory paths", () => {
|
||||||
|
expect(isLocalFile("./components/")).toBe(false)
|
||||||
|
expect(isLocalFile("../shared")).toBe(false)
|
||||||
|
expect(isLocalFile("/absolute/path")).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("isUniversalRegistryItem", () => {
|
||||||
|
it("should return true when all files have targets with registry:file type", () => {
|
||||||
|
const registryItem = {
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
path: "file1.ts",
|
||||||
|
target: "src/file1.ts",
|
||||||
|
type: "registry:file" as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "file2.ts",
|
||||||
|
target: "src/utils/file2.ts",
|
||||||
|
type: "registry:file" as const,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
expect(isUniversalRegistryItem(registryItem)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return true for any registry item type if all files are registry:file with targets", () => {
|
||||||
|
const registryItem = {
|
||||||
|
type: "registry:ui" as const,
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
path: "cursor-rules.txt",
|
||||||
|
target: "~/.cursor/rules/react.txt",
|
||||||
|
type: "registry:file" as const,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
expect(isUniversalRegistryItem(registryItem)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return false when some files lack targets", () => {
|
||||||
|
const registryItem = {
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
path: "file1.ts",
|
||||||
|
target: "src/file1.ts",
|
||||||
|
type: "registry:file" as const,
|
||||||
|
},
|
||||||
|
{ path: "file2.ts", target: "", type: "registry:file" as const },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
expect(isUniversalRegistryItem(registryItem)).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return false when files have non-registry:file type", () => {
|
||||||
|
const registryItem = {
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
path: "file1.ts",
|
||||||
|
target: "src/file1.ts",
|
||||||
|
type: "registry:file" as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "file2.ts",
|
||||||
|
target: "src/lib/file2.ts",
|
||||||
|
type: "registry:lib" as const, // Not registry:file
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
expect(isUniversalRegistryItem(registryItem)).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return false when no files have targets", () => {
|
||||||
|
const registryItem = {
|
||||||
|
files: [
|
||||||
|
{ path: "file1.ts", target: "", type: "registry:file" as const },
|
||||||
|
{ path: "file2.ts", target: "", type: "registry:file" as const },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
expect(isUniversalRegistryItem(registryItem)).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return false when files array is empty", () => {
|
||||||
|
const registryItem = {
|
||||||
|
files: [],
|
||||||
|
}
|
||||||
|
expect(isUniversalRegistryItem(registryItem)).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return false when files is undefined", () => {
|
||||||
|
const registryItem = {}
|
||||||
|
expect(isUniversalRegistryItem(registryItem)).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return false when registryItem is null", () => {
|
||||||
|
expect(isUniversalRegistryItem(null)).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return false when registryItem is undefined", () => {
|
||||||
|
expect(isUniversalRegistryItem(undefined)).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return false when target is null", () => {
|
||||||
|
const registryItem = {
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
path: "file1.ts",
|
||||||
|
target: null as any,
|
||||||
|
type: "registry:file" as const,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
expect(isUniversalRegistryItem(registryItem)).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return false when target is undefined", () => {
|
||||||
|
const registryItem = {
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
path: "file1.ts",
|
||||||
|
type: "registry:file" as const,
|
||||||
|
target: undefined as any,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
expect(isUniversalRegistryItem(registryItem)).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return false when files have registry:component type even with targets", () => {
|
||||||
|
const registryItem = {
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
path: "component.tsx",
|
||||||
|
target: "components/ui/component.tsx",
|
||||||
|
type: "registry:component" as const,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
expect(isUniversalRegistryItem(registryItem)).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return false when files have registry:hook type even with targets", () => {
|
||||||
|
const registryItem = {
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
path: "use-hook.ts",
|
||||||
|
target: "hooks/use-hook.ts",
|
||||||
|
type: "registry:hook" as const,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
expect(isUniversalRegistryItem(registryItem)).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return false when files have registry:lib type even with targets", () => {
|
||||||
|
const registryItem = {
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
path: "utils.ts",
|
||||||
|
target: "lib/utils.ts",
|
||||||
|
type: "registry:lib" as const,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
expect(isUniversalRegistryItem(registryItem)).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return true when all targets are non-empty strings for registry:file", () => {
|
||||||
|
const registryItem = {
|
||||||
|
files: [
|
||||||
|
{ path: "file1.ts", target: " ", type: "registry:file" as const }, // whitespace is truthy
|
||||||
|
{ path: "file2.ts", target: "0", type: "registry:file" as const }, // "0" is truthy
|
||||||
|
],
|
||||||
|
}
|
||||||
|
expect(isUniversalRegistryItem(registryItem)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should handle real-world example with path traversal attempts for registry:file", () => {
|
||||||
|
const registryItem = {
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
path: "malicious.ts",
|
||||||
|
target: "../../../etc/passwd",
|
||||||
|
type: "registry:file" as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "normal.ts",
|
||||||
|
target: "src/normal.ts",
|
||||||
|
type: "registry:file" as const,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
// The function should still return true - path validation is handled elsewhere
|
||||||
|
expect(isUniversalRegistryItem(registryItem)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return false when files have non-registry:file type in a UI registry item", () => {
|
||||||
|
const registryItem = {
|
||||||
|
type: "registry:ui" as const,
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
path: "button.tsx",
|
||||||
|
target: "src/components/ui/button.tsx",
|
||||||
|
type: "registry:ui" as const, // Not registry:file
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
expect(isUniversalRegistryItem(registryItem)).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
@@ -242,3 +242,38 @@ function determineFileType(
|
|||||||
|
|
||||||
return "registry:component"
|
return "registry:component"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Additional utility functions for local file support
|
||||||
|
export function isUrl(path: string) {
|
||||||
|
try {
|
||||||
|
new URL(path)
|
||||||
|
return true
|
||||||
|
} catch (error) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isLocalFile(path: string) {
|
||||||
|
return path.endsWith(".json") && !isUrl(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a registry item is universal (framework-agnostic).
|
||||||
|
* A universal registry item must have all files with:
|
||||||
|
* 1. Explicit targets
|
||||||
|
* 2. Type "registry:file"
|
||||||
|
* It can be installed without framework detection or components.json.
|
||||||
|
*/
|
||||||
|
export function isUniversalRegistryItem(
|
||||||
|
registryItem:
|
||||||
|
| Pick<z.infer<typeof registryItemSchema>, "files">
|
||||||
|
| null
|
||||||
|
| undefined
|
||||||
|
): boolean {
|
||||||
|
return (
|
||||||
|
!!registryItem?.files?.length &&
|
||||||
|
registryItem.files.every(
|
||||||
|
(file) => !!file.target && file.type === "registry:file"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
import path from "path"
|
import path from "path"
|
||||||
import {
|
import {
|
||||||
fetchRegistry,
|
fetchRegistry,
|
||||||
|
getRegistryItem,
|
||||||
getRegistryParentMap,
|
getRegistryParentMap,
|
||||||
getRegistryTypeAliasMap,
|
getRegistryTypeAliasMap,
|
||||||
registryResolveItemsTree,
|
registryResolveItemsTree,
|
||||||
resolveRegistryItems,
|
resolveRegistryItems,
|
||||||
} from "@/src/registry/api"
|
} from "@/src/registry/api"
|
||||||
import { registryItemSchema } from "@/src/registry/schema"
|
import {
|
||||||
|
registryItemFileSchema,
|
||||||
|
registryItemSchema,
|
||||||
|
} from "@/src/registry/schema"
|
||||||
import {
|
import {
|
||||||
configSchema,
|
configSchema,
|
||||||
findCommonRoot,
|
findCommonRoot,
|
||||||
@@ -17,11 +21,13 @@ import {
|
|||||||
} from "@/src/utils/get-config"
|
} from "@/src/utils/get-config"
|
||||||
import { getProjectTailwindVersionFromConfig } from "@/src/utils/get-project-info"
|
import { getProjectTailwindVersionFromConfig } from "@/src/utils/get-project-info"
|
||||||
import { handleError } from "@/src/utils/handle-error"
|
import { handleError } from "@/src/utils/handle-error"
|
||||||
|
import { isSafeTarget } from "@/src/utils/is-safe-target"
|
||||||
import { logger } from "@/src/utils/logger"
|
import { logger } from "@/src/utils/logger"
|
||||||
import { spinner } from "@/src/utils/spinner"
|
import { spinner } from "@/src/utils/spinner"
|
||||||
import { updateCss } from "@/src/utils/updaters/update-css"
|
import { updateCss } from "@/src/utils/updaters/update-css"
|
||||||
import { updateCssVars } from "@/src/utils/updaters/update-css-vars"
|
import { updateCssVars } from "@/src/utils/updaters/update-css-vars"
|
||||||
import { updateDependencies } from "@/src/utils/updaters/update-dependencies"
|
import { updateDependencies } from "@/src/utils/updaters/update-dependencies"
|
||||||
|
import { updateEnvVars } from "@/src/utils/updaters/update-env-vars"
|
||||||
import { updateFiles } from "@/src/utils/updaters/update-files"
|
import { updateFiles } from "@/src/utils/updaters/update-files"
|
||||||
import { updateTailwindConfig } from "@/src/utils/updaters/update-tailwind-config"
|
import { updateTailwindConfig } from "@/src/utils/updaters/update-tailwind-config"
|
||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
@@ -79,6 +85,14 @@ async function addProjectComponents(
|
|||||||
registrySpinner?.fail()
|
registrySpinner?.fail()
|
||||||
return handleError(new Error("Failed to fetch components from registry."))
|
return handleError(new Error("Failed to fetch components from registry."))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
validateFilesTarget(tree.files ?? [], config.resolvedPaths.cwd)
|
||||||
|
} catch (error) {
|
||||||
|
registrySpinner?.fail()
|
||||||
|
return handleError(error)
|
||||||
|
}
|
||||||
|
|
||||||
registrySpinner?.succeed()
|
registrySpinner?.succeed()
|
||||||
|
|
||||||
const tailwindVersion = await getProjectTailwindVersionFromConfig(config)
|
const tailwindVersion = await getProjectTailwindVersionFromConfig(config)
|
||||||
@@ -103,6 +117,10 @@ async function addProjectComponents(
|
|||||||
silent: options.silent,
|
silent: options.silent,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
await updateEnvVars(tree.envVars, config, {
|
||||||
|
silent: options.silent,
|
||||||
|
})
|
||||||
|
|
||||||
await updateDependencies(tree.dependencies, tree.devDependencies, config, {
|
await updateDependencies(tree.dependencies, tree.devDependencies, config, {
|
||||||
silent: options.silent,
|
silent: options.silent,
|
||||||
})
|
})
|
||||||
@@ -147,6 +165,13 @@ async function addWorkspaceComponents(
|
|||||||
const filesUpdated: string[] = []
|
const filesUpdated: string[] = []
|
||||||
const filesSkipped: string[] = []
|
const filesSkipped: string[] = []
|
||||||
|
|
||||||
|
const files = payload.flatMap((item) => item.files ?? [])
|
||||||
|
try {
|
||||||
|
validateFilesTarget(files, config.resolvedPaths.cwd)
|
||||||
|
} catch (error) {
|
||||||
|
return handleError(error)
|
||||||
|
}
|
||||||
|
|
||||||
const rootSpinner = spinner(`Installing components.`)?.start()
|
const rootSpinner = spinner(`Installing components.`)?.start()
|
||||||
|
|
||||||
for (const component of payload) {
|
for (const component of payload) {
|
||||||
@@ -212,7 +237,14 @@ async function addWorkspaceComponents(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Update dependencies.
|
// 4. Update environment variables
|
||||||
|
if (component.envVars) {
|
||||||
|
await updateEnvVars(component.envVars, targetConfig, {
|
||||||
|
silent: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Update dependencies.
|
||||||
await updateDependencies(
|
await updateDependencies(
|
||||||
component.dependencies,
|
component.dependencies,
|
||||||
component.devDependencies,
|
component.devDependencies,
|
||||||
@@ -222,7 +254,7 @@ async function addWorkspaceComponents(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// 5. Update files.
|
// 6. Update files.
|
||||||
const files = await updateFiles(component.files, targetConfig, {
|
const files = await updateFiles(component.files, targetConfig, {
|
||||||
overwrite: options.overwrite,
|
overwrite: options.overwrite,
|
||||||
silent: true,
|
silent: true,
|
||||||
@@ -308,8 +340,9 @@ async function shouldOverwriteCssVars(
|
|||||||
components: z.infer<typeof registryItemSchema>["name"][],
|
components: z.infer<typeof registryItemSchema>["name"][],
|
||||||
config: z.infer<typeof configSchema>
|
config: z.infer<typeof configSchema>
|
||||||
) {
|
) {
|
||||||
let registryItems = await resolveRegistryItems(components, config)
|
let result = await Promise.all(
|
||||||
let result = await fetchRegistry(registryItems)
|
components.map((component) => getRegistryItem(component, config.style))
|
||||||
|
)
|
||||||
const payload = z.array(registryItemSchema).parse(result)
|
const payload = z.array(registryItemSchema).parse(result)
|
||||||
|
|
||||||
return payload.some(
|
return payload.some(
|
||||||
@@ -317,3 +350,20 @@ async function shouldOverwriteCssVars(
|
|||||||
component.type === "registry:theme" || component.type === "registry:style"
|
component.type === "registry:theme" || component.type === "registry:style"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function validateFilesTarget(
|
||||||
|
files: z.infer<typeof registryItemFileSchema>[],
|
||||||
|
cwd: string
|
||||||
|
) {
|
||||||
|
for (const file of files) {
|
||||||
|
if (!file?.target) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isSafeTarget(file.target, cwd)) {
|
||||||
|
throw new Error(
|
||||||
|
`We found an unsafe file path "${file.target} in the registry item. Installation aborted.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
369
packages/shadcn/src/utils/env-helpers.test.ts
Normal file
369
packages/shadcn/src/utils/env-helpers.test.ts
Normal file
@@ -0,0 +1,369 @@
|
|||||||
|
import { existsSync } from "fs"
|
||||||
|
import { beforeEach, describe, expect, test, vi } from "vitest"
|
||||||
|
|
||||||
|
import {
|
||||||
|
findExistingEnvFile,
|
||||||
|
getNewEnvKeys,
|
||||||
|
isEnvFile,
|
||||||
|
mergeEnvContent,
|
||||||
|
parseEnvContent,
|
||||||
|
} from "./env-helpers"
|
||||||
|
|
||||||
|
// Mock fs module
|
||||||
|
vi.mock("fs", () => ({
|
||||||
|
existsSync: vi.fn(),
|
||||||
|
}))
|
||||||
|
|
||||||
|
describe("isEnvFile", () => {
|
||||||
|
test("should identify .env files", () => {
|
||||||
|
expect(isEnvFile("/path/to/.env")).toBe(true)
|
||||||
|
expect(isEnvFile(".env")).toBe(true)
|
||||||
|
expect(isEnvFile("/path/to/.env.local")).toBe(true)
|
||||||
|
expect(isEnvFile(".env.local")).toBe(true)
|
||||||
|
expect(isEnvFile(".env.example")).toBe(true)
|
||||||
|
expect(isEnvFile(".env.development.local")).toBe(true)
|
||||||
|
expect(isEnvFile(".env.production.local")).toBe(true)
|
||||||
|
expect(isEnvFile(".env.test.local")).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should not identify non-.env files", () => {
|
||||||
|
expect(isEnvFile("/path/to/file.txt")).toBe(false)
|
||||||
|
expect(isEnvFile("environment.ts")).toBe(false)
|
||||||
|
expect(isEnvFile("/path/to/.environment")).toBe(false)
|
||||||
|
expect(isEnvFile("env.config")).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("parseEnvContent", () => {
|
||||||
|
test("should parse basic key-value pairs", () => {
|
||||||
|
const content = `KEY1=value1
|
||||||
|
KEY2=value2`
|
||||||
|
expect(parseEnvContent(content)).toEqual({
|
||||||
|
KEY1: "value1",
|
||||||
|
KEY2: "value2",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should handle comments and empty lines", () => {
|
||||||
|
const content = `# This is a comment
|
||||||
|
KEY1=value1
|
||||||
|
|
||||||
|
# Another comment
|
||||||
|
KEY2=value2
|
||||||
|
`
|
||||||
|
expect(parseEnvContent(content)).toEqual({
|
||||||
|
KEY1: "value1",
|
||||||
|
KEY2: "value2",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should handle quoted values", () => {
|
||||||
|
const content = `KEY1="value with spaces"
|
||||||
|
KEY2='single quotes'`
|
||||||
|
expect(parseEnvContent(content)).toEqual({
|
||||||
|
KEY1: "value with spaces",
|
||||||
|
KEY2: "single quotes",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should handle values with equals signs", () => {
|
||||||
|
const content = `DATABASE_URL=postgresql://user:pass@host:5432/db?ssl=true`
|
||||||
|
expect(parseEnvContent(content)).toEqual({
|
||||||
|
DATABASE_URL: "postgresql://user:pass@host:5432/db?ssl=true",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should handle empty values", () => {
|
||||||
|
const content = `EMPTY_KEY=
|
||||||
|
KEY_WITH_VALUE=value`
|
||||||
|
expect(parseEnvContent(content)).toEqual({
|
||||||
|
EMPTY_KEY: "",
|
||||||
|
KEY_WITH_VALUE: "value",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should skip malformed lines", () => {
|
||||||
|
const content = `VALID_KEY=value
|
||||||
|
this is not a valid line
|
||||||
|
ANOTHER_KEY=another_value`
|
||||||
|
expect(parseEnvContent(content)).toEqual({
|
||||||
|
VALID_KEY: "value",
|
||||||
|
ANOTHER_KEY: "another_value",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should handle multi-line values (current limitation: breaks them)", () => {
|
||||||
|
// This test documents that multi-line values are NOT properly supported
|
||||||
|
const content = `SINGLE_LINE="This is fine"
|
||||||
|
MULTI_LINE="This is line 1
|
||||||
|
This is line 2
|
||||||
|
This is line 3"
|
||||||
|
NEXT_KEY=value`
|
||||||
|
|
||||||
|
const result = parseEnvContent(content)
|
||||||
|
|
||||||
|
// Current behavior: only gets first line of multi-line value
|
||||||
|
expect(result.SINGLE_LINE).toBe("This is fine")
|
||||||
|
expect(result.MULTI_LINE).toBe("This is line 1")
|
||||||
|
// The other lines are lost/treated as malformed
|
||||||
|
expect(result["This is line 2"]).toBeUndefined()
|
||||||
|
expect(result.NEXT_KEY).toBe("value")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should handle escaped newlines in values", () => {
|
||||||
|
const content = `KEY_WITH_ESCAPED_NEWLINE="Line 1\\nLine 2\\nLine 3"
|
||||||
|
REGULAR_KEY=regular_value`
|
||||||
|
|
||||||
|
const result = parseEnvContent(content)
|
||||||
|
|
||||||
|
// Escaped newlines are preserved as literal \n
|
||||||
|
expect(result.KEY_WITH_ESCAPED_NEWLINE).toBe("Line 1\\nLine 2\\nLine 3")
|
||||||
|
expect(result.REGULAR_KEY).toBe("regular_value")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should handle values with unmatched quotes", () => {
|
||||||
|
const content = `GOOD_KEY="proper quotes"
|
||||||
|
BAD_KEY="unmatched quote
|
||||||
|
NEXT_KEY=value`
|
||||||
|
|
||||||
|
const result = parseEnvContent(content)
|
||||||
|
|
||||||
|
expect(result.GOOD_KEY).toBe("proper quotes")
|
||||||
|
// Current behavior: strips the opening quote even if unmatched
|
||||||
|
expect(result.BAD_KEY).toBe("unmatched quote")
|
||||||
|
expect(result.NEXT_KEY).toBe("value")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should handle backtick quotes (not supported)", () => {
|
||||||
|
const content = 'KEY1=`backtick value`\nKEY2="double quotes"'
|
||||||
|
|
||||||
|
const result = parseEnvContent(content)
|
||||||
|
|
||||||
|
// Backticks are not treated as quotes
|
||||||
|
expect(result.KEY1).toBe("`backtick value`")
|
||||||
|
expect(result.KEY2).toBe("double quotes")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("mergeEnvContent", () => {
|
||||||
|
test("should append only new keys", () => {
|
||||||
|
const existing = `KEY1=value1`
|
||||||
|
const newContent = `KEY2=value2`
|
||||||
|
const result = mergeEnvContent(existing, newContent)
|
||||||
|
expect(result).toBe(`KEY1=value1
|
||||||
|
|
||||||
|
KEY2=value2
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should preserve existing values and NOT overwrite them", () => {
|
||||||
|
const existing = `KEY1=existing_value
|
||||||
|
KEY2=value2`
|
||||||
|
const newContent = `KEY1=new_value_should_be_ignored
|
||||||
|
KEY3=value3`
|
||||||
|
const result = mergeEnvContent(existing, newContent)
|
||||||
|
expect(result).toBe(`KEY1=existing_value
|
||||||
|
KEY2=value2
|
||||||
|
|
||||||
|
KEY3=value3
|
||||||
|
`)
|
||||||
|
|
||||||
|
expect(result).toContain("KEY1=existing_value")
|
||||||
|
expect(result).not.toContain("KEY1=new_value_should_be_ignored")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should handle empty existing content", () => {
|
||||||
|
const existing = ""
|
||||||
|
const newContent = "KEY1=value1"
|
||||||
|
const result = mergeEnvContent(existing, newContent)
|
||||||
|
expect(result).toBe(`KEY1=value1
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should not add any content if all keys already exist", () => {
|
||||||
|
const existing = `KEY1=value1
|
||||||
|
KEY2=value2`
|
||||||
|
const newContent = `KEY1=ignored
|
||||||
|
KEY2=ignored`
|
||||||
|
const result = mergeEnvContent(existing, newContent)
|
||||||
|
|
||||||
|
expect(result).toBe(`KEY1=value1
|
||||||
|
KEY2=value2
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should return unchanged content when all keys exist and formatting is correct", () => {
|
||||||
|
const existing = `KEY1=value1
|
||||||
|
KEY2=value2
|
||||||
|
`
|
||||||
|
const newContent = `KEY1=ignored
|
||||||
|
KEY2=ignored`
|
||||||
|
const result = mergeEnvContent(existing, newContent)
|
||||||
|
|
||||||
|
expect(result).toBe(existing)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should handle existing content with comments", () => {
|
||||||
|
const existing = `# Production configuration
|
||||||
|
KEY1=value1
|
||||||
|
# API Keys
|
||||||
|
KEY2=value2`
|
||||||
|
const newContent = `KEY3=value3
|
||||||
|
KEY1=should_be_ignored`
|
||||||
|
const result = mergeEnvContent(existing, newContent)
|
||||||
|
|
||||||
|
expect(result).toBe(`# Production configuration
|
||||||
|
KEY1=value1
|
||||||
|
# API Keys
|
||||||
|
KEY2=value2
|
||||||
|
|
||||||
|
KEY3=value3
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should maintain proper formatting", () => {
|
||||||
|
const existing = `KEY1=value1
|
||||||
|
KEY2=value2
|
||||||
|
`
|
||||||
|
const newContent = `KEY3=value3`
|
||||||
|
const result = mergeEnvContent(existing, newContent)
|
||||||
|
|
||||||
|
expect(result).toBe(`KEY1=value1
|
||||||
|
KEY2=value2
|
||||||
|
|
||||||
|
KEY3=value3
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should handle multiple new keys", () => {
|
||||||
|
const existing = `KEY1=value1`
|
||||||
|
const newContent = `KEY2=value2
|
||||||
|
KEY3=value3
|
||||||
|
KEY4=value4`
|
||||||
|
const result = mergeEnvContent(existing, newContent)
|
||||||
|
|
||||||
|
expect(result).toBe(`KEY1=value1
|
||||||
|
|
||||||
|
KEY2=value2
|
||||||
|
KEY3=value3
|
||||||
|
KEY4=value4
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should handle multi-line values in merge (current limitation)", () => {
|
||||||
|
const existing = `EXISTING_KEY=existing_value`
|
||||||
|
const newContent = `MULTI_LINE_KEY="Line 1
|
||||||
|
Line 2
|
||||||
|
Line 3"
|
||||||
|
SIMPLE_KEY=simple`
|
||||||
|
|
||||||
|
const result = mergeEnvContent(existing, newContent)
|
||||||
|
|
||||||
|
// Current behavior: only the first line is added
|
||||||
|
expect(result).toBe(`EXISTING_KEY=existing_value
|
||||||
|
|
||||||
|
MULTI_LINE_KEY=Line 1
|
||||||
|
SIMPLE_KEY=simple
|
||||||
|
`)
|
||||||
|
|
||||||
|
// The multi-line value is broken
|
||||||
|
expect(result).not.toContain("Line 2")
|
||||||
|
expect(result).not.toContain("Line 3")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("getNewEnvKeys", () => {
|
||||||
|
test("should identify new keys", () => {
|
||||||
|
const existing = `KEY1=value1
|
||||||
|
KEY2=value2`
|
||||||
|
const newContent = `KEY1=ignored
|
||||||
|
KEY3=value3
|
||||||
|
KEY4=value4`
|
||||||
|
|
||||||
|
const result = getNewEnvKeys(existing, newContent)
|
||||||
|
expect(result).toEqual(["KEY3", "KEY4"])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should return empty array when all keys exist", () => {
|
||||||
|
const existing = `KEY1=value1
|
||||||
|
KEY2=value2`
|
||||||
|
const newContent = `KEY1=different
|
||||||
|
KEY2=different`
|
||||||
|
|
||||||
|
const result = getNewEnvKeys(existing, newContent)
|
||||||
|
expect(result).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should handle empty existing content", () => {
|
||||||
|
const existing = ""
|
||||||
|
const newContent = `KEY1=value1
|
||||||
|
KEY2=value2`
|
||||||
|
|
||||||
|
const result = getNewEnvKeys(existing, newContent)
|
||||||
|
expect(result).toEqual(["KEY1", "KEY2"])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("findExistingEnvFile", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should return .env.local if it exists", () => {
|
||||||
|
vi.mocked(existsSync).mockImplementation((path) => {
|
||||||
|
const pathStr = typeof path === "string" ? path : path.toString()
|
||||||
|
return pathStr.endsWith(".env.local")
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = findExistingEnvFile("/test/dir")
|
||||||
|
expect(result).toBe("/test/dir/.env.local")
|
||||||
|
expect(existsSync).toHaveBeenCalledWith("/test/dir/.env.local")
|
||||||
|
expect(existsSync).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should return .env if .env.local doesn't exist", () => {
|
||||||
|
vi.mocked(existsSync).mockImplementation((path) => {
|
||||||
|
const pathStr = typeof path === "string" ? path : path.toString()
|
||||||
|
return pathStr.endsWith(".env")
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = findExistingEnvFile("/test/dir")
|
||||||
|
expect(result).toBe("/test/dir/.env")
|
||||||
|
expect(existsSync).toHaveBeenCalledWith("/test/dir/.env.local")
|
||||||
|
expect(existsSync).toHaveBeenCalledWith("/test/dir/.env")
|
||||||
|
expect(existsSync).toHaveBeenCalledTimes(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should return .env.development.local if earlier variants don't exist", () => {
|
||||||
|
vi.mocked(existsSync).mockImplementation((path) => {
|
||||||
|
const pathStr = typeof path === "string" ? path : path.toString()
|
||||||
|
return pathStr.endsWith(".env.development.local")
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = findExistingEnvFile("/test/dir")
|
||||||
|
expect(result).toBe("/test/dir/.env.development.local")
|
||||||
|
expect(existsSync).toHaveBeenCalledWith("/test/dir/.env.local")
|
||||||
|
expect(existsSync).toHaveBeenCalledWith("/test/dir/.env")
|
||||||
|
expect(existsSync).toHaveBeenCalledWith("/test/dir/.env.development.local")
|
||||||
|
expect(existsSync).toHaveBeenCalledTimes(3)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should return null if no env files exist", () => {
|
||||||
|
vi.mocked(existsSync).mockReturnValue(false)
|
||||||
|
|
||||||
|
const result = findExistingEnvFile("/test/dir")
|
||||||
|
expect(result).toBeNull()
|
||||||
|
expect(existsSync).toHaveBeenCalledTimes(4)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should check all variants in correct order", () => {
|
||||||
|
vi.mocked(existsSync).mockReturnValue(false)
|
||||||
|
|
||||||
|
findExistingEnvFile("/test/dir")
|
||||||
|
|
||||||
|
expect(existsSync).toHaveBeenCalledWith("/test/dir/.env.local")
|
||||||
|
expect(existsSync).toHaveBeenCalledWith("/test/dir/.env")
|
||||||
|
expect(existsSync).toHaveBeenCalledWith("/test/dir/.env.development.local")
|
||||||
|
expect(existsSync).toHaveBeenCalledWith("/test/dir/.env.development")
|
||||||
|
})
|
||||||
|
})
|
||||||
113
packages/shadcn/src/utils/env-helpers.ts
Normal file
113
packages/shadcn/src/utils/env-helpers.ts
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
import { existsSync } from "fs"
|
||||||
|
import path from "path"
|
||||||
|
|
||||||
|
export function isEnvFile(filePath: string) {
|
||||||
|
const fileName = path.basename(filePath)
|
||||||
|
return /^\.env(\.|$)/.test(fileName)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds a file variant in the project.
|
||||||
|
* TODO: abstract this to a more generic function.
|
||||||
|
*/
|
||||||
|
export function findExistingEnvFile(targetDir: string) {
|
||||||
|
const variants = [
|
||||||
|
".env.local",
|
||||||
|
".env",
|
||||||
|
".env.development.local",
|
||||||
|
".env.development",
|
||||||
|
]
|
||||||
|
|
||||||
|
for (const variant of variants) {
|
||||||
|
const filePath = path.join(targetDir, variant)
|
||||||
|
if (existsSync(filePath)) {
|
||||||
|
return filePath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse .env content into key-value pairs.
|
||||||
|
*/
|
||||||
|
export function parseEnvContent(content: string) {
|
||||||
|
const lines = content.split("\n")
|
||||||
|
const env: Record<string, string> = {}
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const trimmed = line.trim()
|
||||||
|
|
||||||
|
if (!trimmed || trimmed.startsWith("#")) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the first = and split there
|
||||||
|
const equalIndex = trimmed.indexOf("=")
|
||||||
|
if (equalIndex === -1) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = trimmed.substring(0, equalIndex).trim()
|
||||||
|
const value = trimmed.substring(equalIndex + 1).trim()
|
||||||
|
|
||||||
|
if (key) {
|
||||||
|
env[key] = value.replace(/^["']|["']$/g, "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return env
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the list of new keys that would be added when merging env content.
|
||||||
|
*/
|
||||||
|
export function getNewEnvKeys(existingContent: string, newContent: string) {
|
||||||
|
const existingEnv = parseEnvContent(existingContent)
|
||||||
|
const newEnv = parseEnvContent(newContent)
|
||||||
|
|
||||||
|
const newKeys = []
|
||||||
|
for (const key of Object.keys(newEnv)) {
|
||||||
|
if (!(key in existingEnv)) {
|
||||||
|
newKeys.push(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return newKeys
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merge env content by appending ONLY new keys that don't exist in the existing content.
|
||||||
|
* Existing keys are preserved with their original values.
|
||||||
|
*/
|
||||||
|
export function mergeEnvContent(existingContent: string, newContent: string) {
|
||||||
|
const existingEnv = parseEnvContent(existingContent)
|
||||||
|
const newEnv = parseEnvContent(newContent)
|
||||||
|
|
||||||
|
let result = existingContent.trimEnd()
|
||||||
|
if (result && !result.endsWith("\n")) {
|
||||||
|
result += "\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
const newKeys: string[] = []
|
||||||
|
for (const [key, value] of Object.entries(newEnv)) {
|
||||||
|
if (!(key in existingEnv)) {
|
||||||
|
newKeys.push(`${key}=${value}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newKeys.length > 0) {
|
||||||
|
if (result) {
|
||||||
|
result += "\n"
|
||||||
|
}
|
||||||
|
result += newKeys.join("\n")
|
||||||
|
return result + "\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure existing content ends with newline.
|
||||||
|
if (result && !result.endsWith("\n")) {
|
||||||
|
return result + "\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
@@ -72,6 +72,14 @@ export const FRAMEWORKS = {
|
|||||||
tailwind: "https://tailwindcss.com/docs/guides/gatsby",
|
tailwind: "https://tailwindcss.com/docs/guides/gatsby",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
expo: {
|
||||||
|
name: "expo",
|
||||||
|
label: "Expo",
|
||||||
|
links: {
|
||||||
|
installation: "https://ui.shadcn.com/docs/installation/expo",
|
||||||
|
tailwind: "https://www.nativewind.dev/docs/getting-started/installation",
|
||||||
|
},
|
||||||
|
},
|
||||||
manual: {
|
manual: {
|
||||||
name: "manual",
|
name: "manual",
|
||||||
label: "Manual",
|
label: "Manual",
|
||||||
|
|||||||
@@ -225,3 +225,64 @@ export async function getTargetStyleFromConfig(cwd: string, fallback: string) {
|
|||||||
const projectInfo = await getProjectInfo(cwd)
|
const projectInfo = await getProjectInfo(cwd)
|
||||||
return projectInfo?.tailwindVersion === "v4" ? "new-york-v4" : fallback
|
return projectInfo?.tailwindVersion === "v4" ? "new-york-v4" : fallback
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DeepPartial<T> = {
|
||||||
|
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a config object with sensible defaults.
|
||||||
|
* Useful for universal registry items that bypass framework detection.
|
||||||
|
*
|
||||||
|
* @param partial - Partial config values to override defaults
|
||||||
|
* @returns A complete Config object
|
||||||
|
*/
|
||||||
|
export function createConfig(partial?: DeepPartial<Config>): Config {
|
||||||
|
const defaultConfig: Config = {
|
||||||
|
resolvedPaths: {
|
||||||
|
cwd: process.cwd(),
|
||||||
|
tailwindConfig: "",
|
||||||
|
tailwindCss: "",
|
||||||
|
utils: "",
|
||||||
|
components: "",
|
||||||
|
ui: "",
|
||||||
|
lib: "",
|
||||||
|
hooks: "",
|
||||||
|
},
|
||||||
|
style: "",
|
||||||
|
tailwind: {
|
||||||
|
config: "",
|
||||||
|
css: "",
|
||||||
|
baseColor: "",
|
||||||
|
cssVariables: false,
|
||||||
|
},
|
||||||
|
rsc: false,
|
||||||
|
tsx: true,
|
||||||
|
aliases: {
|
||||||
|
components: "",
|
||||||
|
utils: "",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deep merge the partial config with defaults
|
||||||
|
if (partial) {
|
||||||
|
return {
|
||||||
|
...defaultConfig,
|
||||||
|
...partial,
|
||||||
|
resolvedPaths: {
|
||||||
|
...defaultConfig.resolvedPaths,
|
||||||
|
...(partial.resolvedPaths || {}),
|
||||||
|
},
|
||||||
|
tailwind: {
|
||||||
|
...defaultConfig.tailwind,
|
||||||
|
...(partial.tailwind || {}),
|
||||||
|
},
|
||||||
|
aliases: {
|
||||||
|
...defaultConfig.aliases,
|
||||||
|
...(partial.aliases || {}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultConfig
|
||||||
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user