mirror of
https://github.com/shadcn-ui/ui.git
synced 2026-06-22 20:25:44 +00:00
Compare commits
12 Commits
shadcn@2.9
...
shadcn@2.9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e04567b07 | ||
|
|
6f63b04d28 | ||
|
|
e38228b574 | ||
|
|
8807103586 | ||
|
|
3424ab709e | ||
|
|
4a86a55cac | ||
|
|
2926574d0e | ||
|
|
20e913d8e1 | ||
|
|
3433aaffaa | ||
|
|
d9cdc3f7ae | ||
|
|
e75e7b3866 | ||
|
|
ed5237c231 |
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": []
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
|||||||
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()
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
Open in v0
|
||||||
|
</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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -70,6 +70,14 @@ const nextConfig = {
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
rewrites() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
source: "/docs/:path*.mdx",
|
||||||
|
destination: "/llm/:path*",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const withMDX = createMDX({})
|
const withMDX = createMDX({})
|
||||||
|
|||||||
@@ -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.9.0",
|
"shadcn": "2.9.3",
|
||||||
"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",
|
||||||
|
|||||||
@@ -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.9.0",
|
"shadcn": "2.9.3",
|
||||||
"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",
|
||||||
|
|||||||
@@ -1,5 +1,23 @@
|
|||||||
# @shadcn/ui
|
# @shadcn/ui
|
||||||
|
|
||||||
|
## 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
|
## 2.9.0
|
||||||
|
|
||||||
### Minor Changes
|
### Minor Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "shadcn",
|
"name": "shadcn",
|
||||||
"version": "2.9.0",
|
"version": "2.9.3",
|
||||||
"description": "Add components to your apps.",
|
"description": "Add components to your apps.",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public"
|
"access": "public"
|
||||||
|
|||||||
@@ -137,18 +137,32 @@ describe("isLocalFile", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe("isUniversalRegistryItem", () => {
|
describe("isUniversalRegistryItem", () => {
|
||||||
it("should return true when all files have targets", () => {
|
it("should return true when all files have targets with registry:file type", () => {
|
||||||
const registryItem = {
|
const registryItem = {
|
||||||
files: [
|
files: [
|
||||||
{
|
{
|
||||||
path: "file1.ts",
|
path: "file1.ts",
|
||||||
target: "src/file1.ts",
|
target: "src/file1.ts",
|
||||||
type: "registry:lib" as const,
|
type: "registry:file" as const,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "file2.ts",
|
path: "file2.ts",
|
||||||
target: "src/utils/file2.ts",
|
target: "src/utils/file2.ts",
|
||||||
type: "registry:lib" as const,
|
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,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
@@ -161,9 +175,27 @@ describe("isUniversalRegistryItem", () => {
|
|||||||
{
|
{
|
||||||
path: "file1.ts",
|
path: "file1.ts",
|
||||||
target: "src/file1.ts",
|
target: "src/file1.ts",
|
||||||
type: "registry:lib" as const,
|
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
|
||||||
},
|
},
|
||||||
{ path: "file2.ts", target: "", type: "registry:lib" as const },
|
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
expect(isUniversalRegistryItem(registryItem)).toBe(false)
|
expect(isUniversalRegistryItem(registryItem)).toBe(false)
|
||||||
@@ -172,8 +204,8 @@ describe("isUniversalRegistryItem", () => {
|
|||||||
it("should return false when no files have targets", () => {
|
it("should return false when no files have targets", () => {
|
||||||
const registryItem = {
|
const registryItem = {
|
||||||
files: [
|
files: [
|
||||||
{ path: "file1.ts", target: "", type: "registry:lib" as const },
|
{ path: "file1.ts", target: "", type: "registry:file" as const },
|
||||||
{ path: "file2.ts", target: "", type: "registry:lib" as const },
|
{ path: "file2.ts", target: "", type: "registry:file" as const },
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
expect(isUniversalRegistryItem(registryItem)).toBe(false)
|
expect(isUniversalRegistryItem(registryItem)).toBe(false)
|
||||||
@@ -205,7 +237,7 @@ describe("isUniversalRegistryItem", () => {
|
|||||||
{
|
{
|
||||||
path: "file1.ts",
|
path: "file1.ts",
|
||||||
target: null as any,
|
target: null as any,
|
||||||
type: "registry:lib" as const,
|
type: "registry:file" as const,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
@@ -214,60 +246,96 @@ describe("isUniversalRegistryItem", () => {
|
|||||||
|
|
||||||
it("should return false when target is undefined", () => {
|
it("should return false when target is undefined", () => {
|
||||||
const registryItem = {
|
const registryItem = {
|
||||||
files: [{ path: "file1.ts", type: "registry:lib" as const }],
|
files: [
|
||||||
|
{
|
||||||
|
path: "file1.ts",
|
||||||
|
type: "registry:file" as const,
|
||||||
|
target: undefined as any,
|
||||||
|
},
|
||||||
|
],
|
||||||
}
|
}
|
||||||
expect(isUniversalRegistryItem(registryItem)).toBe(false)
|
expect(isUniversalRegistryItem(registryItem)).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should handle mixed file types correctly", () => {
|
it("should return false when files have registry:component type even with targets", () => {
|
||||||
const registryItem = {
|
const registryItem = {
|
||||||
files: [
|
files: [
|
||||||
{
|
{
|
||||||
path: "component.tsx",
|
path: "component.tsx",
|
||||||
target: "components/ui/component.tsx",
|
target: "components/ui/component.tsx",
|
||||||
type: "registry:ui" as const,
|
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",
|
path: "utils.ts",
|
||||||
target: "lib/utils.ts",
|
target: "lib/utils.ts",
|
||||||
type: "registry:lib" as const,
|
type: "registry:lib" as const,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: "hook.ts",
|
|
||||||
target: "hooks/use-something.ts",
|
|
||||||
type: "registry:hook" as const,
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
expect(isUniversalRegistryItem(registryItem)).toBe(true)
|
expect(isUniversalRegistryItem(registryItem)).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should return true when all targets are non-empty strings", () => {
|
it("should return true when all targets are non-empty strings for registry:file", () => {
|
||||||
const registryItem = {
|
const registryItem = {
|
||||||
files: [
|
files: [
|
||||||
{ path: "file1.ts", target: " ", type: "registry:lib" as const }, // whitespace is truthy
|
{ path: "file1.ts", target: " ", type: "registry:file" as const }, // whitespace is truthy
|
||||||
{ path: "file2.ts", target: "0", type: "registry:lib" as const }, // "0" is truthy
|
{ path: "file2.ts", target: "0", type: "registry:file" as const }, // "0" is truthy
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
expect(isUniversalRegistryItem(registryItem)).toBe(true)
|
expect(isUniversalRegistryItem(registryItem)).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should handle real-world example with path traversal attempts", () => {
|
it("should handle real-world example with path traversal attempts for registry:file", () => {
|
||||||
const registryItem = {
|
const registryItem = {
|
||||||
files: [
|
files: [
|
||||||
{
|
{
|
||||||
path: "malicious.ts",
|
path: "malicious.ts",
|
||||||
target: "../../../etc/passwd",
|
target: "../../../etc/passwd",
|
||||||
type: "registry:lib" as const,
|
type: "registry:file" as const,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "normal.ts",
|
path: "normal.ts",
|
||||||
target: "src/normal.ts",
|
target: "src/normal.ts",
|
||||||
type: "registry:lib" as const,
|
type: "registry:file" as const,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
// The function should still return true - path validation is handled elsewhere
|
// The function should still return true - path validation is handled elsewhere
|
||||||
expect(isUniversalRegistryItem(registryItem)).toBe(true)
|
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)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -259,7 +259,9 @@ export function isLocalFile(path: string) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a registry item is universal (framework-agnostic).
|
* Check if a registry item is universal (framework-agnostic).
|
||||||
* A universal registry item has all files with explicit targets.
|
* 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.
|
* It can be installed without framework detection or components.json.
|
||||||
*/
|
*/
|
||||||
export function isUniversalRegistryItem(
|
export function isUniversalRegistryItem(
|
||||||
@@ -270,6 +272,8 @@ export function isUniversalRegistryItem(
|
|||||||
): boolean {
|
): boolean {
|
||||||
return (
|
return (
|
||||||
!!registryItem?.files?.length &&
|
!!registryItem?.files?.length &&
|
||||||
registryItem.files.every((file) => !!file.target)
|
registryItem.files.every(
|
||||||
|
(file) => !!file.target && file.type === "registry:file"
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import path from "path"
|
import path from "path"
|
||||||
import {
|
import {
|
||||||
fetchRegistry,
|
fetchRegistry,
|
||||||
|
getRegistryItem,
|
||||||
getRegistryParentMap,
|
getRegistryParentMap,
|
||||||
getRegistryTypeAliasMap,
|
getRegistryTypeAliasMap,
|
||||||
registryResolveItemsTree,
|
registryResolveItemsTree,
|
||||||
@@ -327,8 +328,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(
|
||||||
|
|||||||
4
pnpm-lock.yaml
generated
4
pnpm-lock.yaml
generated
@@ -322,7 +322,7 @@ importers:
|
|||||||
specifier: ^6.0.1
|
specifier: ^6.0.1
|
||||||
version: 6.0.1
|
version: 6.0.1
|
||||||
shadcn:
|
shadcn:
|
||||||
specifier: 2.9.0
|
specifier: 2.9.3
|
||||||
version: link:../../packages/shadcn
|
version: link:../../packages/shadcn
|
||||||
shiki:
|
shiki:
|
||||||
specifier: ^1.10.1
|
specifier: ^1.10.1
|
||||||
@@ -602,7 +602,7 @@ importers:
|
|||||||
specifier: 2.12.7
|
specifier: 2.12.7
|
||||||
version: 2.12.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
version: 2.12.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
shadcn:
|
shadcn:
|
||||||
specifier: 2.9.0
|
specifier: 2.9.3
|
||||||
version: link:../../packages/shadcn
|
version: link:../../packages/shadcn
|
||||||
sharp:
|
sharp:
|
||||||
specifier: ^0.32.6
|
specifier: ^0.32.6
|
||||||
|
|||||||
Reference in New Issue
Block a user