This commit is contained in:
熊潇 2025-05-06 02:13:23 +08:00
commit 22baba9e7b
52 changed files with 13258 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
node_modules

14
package.json Normal file
View File

@ -0,0 +1,14 @@
{
"name": "test-shadcn",
"version": "0.0.1",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "abearxiong <xiongxiao@xiongxiao.me> (https://www.xiongxiao.me)",
"license": "MIT",
"packageManager": "pnpm@10.6.2",
"type": "module"
}

4511
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

6
pnpm-workspace.yaml Normal file
View File

@ -0,0 +1,6 @@
packages:
- 'packages/*'
- 'apps/*'
- 'registry*/*'
- 'vite-react-template'
- 'vite-*'

41
registry-template/.gitignore vendored Normal file
View File

@ -0,0 +1,41 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

21
registry-template/LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 shadcn
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,23 @@
# registry-template
You can use the `shadcn` CLI to run your own component registry. Running your own
component registry allows you to distribute your custom components, hooks, pages, and
other files to any React project.
> [!IMPORTANT]
> This template uses Tailwind v3. For Tailwind v4, see [registry-template](https://github.com/shadcn-ui/registry-template-v4).
## Getting Started
This is a template for creating a custom registry using Next.js.
- The template uses a `registry.json` file to define components and their files.
- The `shadcn build` command is used to build the registry.
- The registry items are served as static files under `public/r/[name].json`.
- The template also includes a route handler for serving registry items.
- Every registry item are compatible with the `shadcn` CLI.
- We have also added v0 integration using the `Open in v0` api.
## Documentation
Visit the [shadcn documentation](https://ui.shadcn.com/docs/registry) to view the full documentation.

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@ -0,0 +1,136 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 240 10% 3.9%;
--card: 0 0% 100%;
--card-foreground: 240 10% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 240 10% 3.9%;
--primary: 240 5.9% 10%;
--primary-foreground: 0 0% 98%;
--secondary: 240 4.8% 95.9%;
--secondary-foreground: 240 5.9% 10%;
--muted: 240 4.8% 95.9%;
--muted-foreground: 240 3.8% 46.1%;
--accent: 240 4.8% 95.9%;
--accent-foreground: 240 5.9% 10%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border: 240 5.9% 90%;
--input: 240 5.9% 90%;
--ring: 240 10% 3.9%;
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
--radius: 0.5rem;
--sidebar-background: 0 0% 98%;
--sidebar-foreground: 240 5.3% 26.1%;
--sidebar-primary: 240 5.9% 10%;
--sidebar-primary-foreground: 0 0% 98%;
--sidebar-accent: 240 4.8% 95.9%;
--sidebar-accent-foreground: 240 5.9% 10%;
--sidebar-border: 220 13% 91%;
--sidebar-ring: 217.2 91.2% 59.8%;
}
.dark {
--background: 240 10% 3.9%;
--foreground: 0 0% 98%;
--card: 240 10% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 240 10% 3.9%;
--popover-foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 240 5.9% 10%;
--secondary: 240 3.7% 15.9%;
--secondary-foreground: 0 0% 98%;
--muted: 240 3.7% 15.9%;
--muted-foreground: 240 5% 64.9%;
--accent: 240 3.7% 15.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 240 3.7% 15.9%;
--input: 240 3.7% 15.9%;
--ring: 240 4.9% 83.9%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
--sidebar-background: 240 5.9% 10%;
--sidebar-foreground: 240 4.8% 95.9%;
--sidebar-primary: 224.3 76.3% 48%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 240 3.7% 15.9%;
--sidebar-accent-foreground: 240 4.8% 95.9%;
--sidebar-border: 240 3.7% 15.9%;
--sidebar-ring: 217.2 91.2% 59.8%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}
[data-rehype-pretty-code-fragment] {
@apply relative text-white;
}
[data-rehype-pretty-code-fragment] code {
@apply grid min-w-full break-words rounded-none border-0 bg-transparent p-0;
counter-reset: line;
box-decoration-break: clone;
}
[data-rehype-pretty-code-fragment] .line {
@apply px-4 min-h-[1rem] py-0.5 w-full inline-block;
}
[data-rehype-pretty-code-fragment] [data-line-numbers] .line {
@apply px-2;
}
[data-rehype-pretty-code-fragment] [data-line-numbers] > .line::before {
@apply text-zinc-50/40 text-xs;
counter-increment: line;
content: counter(line);
display: inline-block;
width: 1.8rem;
margin-right: 1.4rem;
text-align: right;
}
[data-rehype-pretty-code-fragment] .line--highlighted {
@apply bg-zinc-700/50;
}
[data-rehype-pretty-code-fragment] .line-highlighted span {
@apply relative;
}
[data-rehype-pretty-code-fragment] .word--highlighted {
@apply rounded-md bg-zinc-700/50 border-zinc-700/70 p-1;
}
.dark [data-rehype-pretty-code-fragment] .word--highlighted {
@apply bg-zinc-900;
}
[data-rehype-pretty-code-title] {
@apply mt-2 pt-6 px-4 text-sm font-medium text-foreground;
}
[data-rehype-pretty-code-title] + pre {
@apply mt-2;
}

View File

@ -0,0 +1,34 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
{children}
</body>
</html>
);
}

View File

@ -0,0 +1,58 @@
import * as React from "react"
import { OpenInV0Button } from "@/components/open-in-v0-button"
import { HelloWorld } from "@/registry/new-york/hello-world/hello-world"
import { ExampleForm } from "@/registry/new-york/example-form/example-form"
import PokemonPage from "@/registry/new-york/complex-component/page"
// This page displays items from the custom registry.
// You are free to implement this with your own design as needed.
export default function Home() {
return (
<div className="max-w-3xl mx-auto flex flex-col min-h-svh px-4 py-8 gap-8">
<header className="flex flex-col gap-1">
<h1 className="text-3xl font-bold tracking-tight">Custom Registry</h1>
<p className="text-muted-foreground">
A custom registry for distributing code using shadcn.
</p>
</header>
<main className="flex flex-col flex-1 gap-8">
<div className="flex flex-col gap-4 border rounded-lg p-4 min-h-[450px] relative">
<div className="flex items-center justify-between">
<h2 className="text-sm text-muted-foreground sm:pl-3">
A simple hello world component
</h2>
<OpenInV0Button name="hello-world" className="w-fit" />
</div>
<div className="flex items-center justify-center min-h-[400px] relative">
<HelloWorld />
</div>
</div>
<div className="flex flex-col gap-4 border rounded-lg p-4 min-h-[450px] relative">
<div className="flex items-center justify-between">
<h2 className="text-sm text-muted-foreground sm:pl-3">
A contact form with Zod validation.
</h2>
<OpenInV0Button name="example-form" className="w-fit" />
</div>
<div className="flex items-center justify-center min-h-[500px] relative">
<ExampleForm />
</div>
</div>
<div className="flex flex-col gap-4 border rounded-lg p-4 min-h-[450px] relative">
<div className="flex items-center justify-between">
<h2 className="text-sm text-muted-foreground sm:pl-3">
A complex component showing hooks, libs and components.
</h2>
<OpenInV0Button name="complex-component" className="w-fit" />
</div>
<div className="flex items-center justify-center min-h-[400px] relative">
<PokemonPage />
</div>
</div>
</main>
</div>
)
}

View File

@ -0,0 +1,64 @@
import { NextResponse } from "next/server"
import path from "path"
import { promises as fs } from "fs"
import { registryItemSchema } from "shadcn/registry"
// Use the registry.json file to generate static paths.
export const generateStaticParams = async () => {
const registryData = await import("@/registry.json");
const registry = registryData.default;
return registry.items.map((item) => ({
name: item.name,
}));
};
// This route shows an example for serving a component using a route handler.
export async function GET(
request: Request,
{ params }: { params: Promise<{ name: string }> }
) {
try {
const { name } = await params
// Cache the registry import
const registryData = await import("@/registry.json")
const registry = registryData.default
// Find the component from the registry.
const component = registry.items.find((c) => c.name === name)
// If the component is not found, return a 404 error.
if (!component) {
return NextResponse.json(
{ error: "Component not found" },
{ status: 404 }
)
}
// Validate before file operations.
const registryItem = registryItemSchema.parse(component)
// If the component has no files, return a 400 error.
if (!registryItem.files?.length) {
return NextResponse.json(
{ error: "Component has no files" },
{ status: 400 }
)
}
// Read all files in parallel.
const filesWithContent = await Promise.all(
registryItem.files.map(async (file) => {
const filePath = path.join(process.cwd(), file.path)
const content = await fs.readFile(filePath, "utf8")
return { ...file, content }
})
)
// Return the component with the files.
return NextResponse.json({ ...registryItem, files: filesWithContent })
} catch (error) {
console.error("Error processing component request:", error)
return NextResponse.json({ error: "Something went wrong" }, { status: 500 })
}
}

View File

@ -0,0 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.ts",
"css": "app/globals.css",
"baseColor": "zinc",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}

View File

@ -0,0 +1,29 @@
"use client"
import * as React from "react"
import { useTheme } from "next-themes"
import { Moon, Sun } from "lucide-react"
import { Button } from "@/components/ui/button"
export function ModeToggle() {
const { setTheme, resolvedTheme } = useTheme()
const [, startTransition] = React.useTransition()
return (
<Button
className="h-7 w-7"
onClick={() => {
startTransition(() => {
setTheme(resolvedTheme === "dark" ? "light" : "dark")
})
}}
size="icon"
variant="ghost"
>
<Moon className="dark:hidden" />
<Sun className="hidden dark:block" />
<span className="sr-only">Toggle theme</span>
</Button>
)
}

View File

@ -0,0 +1,41 @@
import { Button } from "@/components/ui/button"
import { cn } from "@/lib/utils"
export function OpenInV0Button({
name,
className,
}: { name: string } & React.ComponentProps<typeof Button>) {
return (
<Button
aria-label="Open in v0"
className={cn(
"h-7 gap-1 rounded-lg shadow-none bg-black px-3 text-xs text-white hover:bg-black hover:text-white dark:bg-white dark:text-black",
className
)}
asChild
>
<a
href={`https://v0.dev/chat/api/open?url=${process.env.NEXT_PUBLIC_BASE_URL}/r/${name}.json`}
target="_blank"
rel="noreferrer"
>
Open in{" "}
<svg
viewBox="0 0 40 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="h-5 w-5 text-current"
>
<path
d="M23.3919 0H32.9188C36.7819 0 39.9136 3.13165 39.9136 6.99475V16.0805H36.0006V6.99475C36.0006 6.90167 35.9969 6.80925 35.9898 6.71766L26.4628 16.079C26.4949 16.08 26.5272 16.0805 26.5595 16.0805H36.0006V19.7762H26.5595C22.6964 19.7762 19.4788 16.6139 19.4788 12.7508V3.68923H23.3919V12.7508C23.3919 12.9253 23.4054 13.0977 23.4316 13.2668L33.1682 3.6995C33.0861 3.6927 33.003 3.68923 32.9188 3.68923H23.3919V0Z"
fill="currentColor"
></path>
<path
d="M13.7688 19.0956L0 3.68759H5.53933L13.6231 12.7337V3.68759H17.7535V17.5746C17.7535 19.6705 15.1654 20.6584 13.7688 19.0956Z"
fill="currentColor"
></path>
</svg>
</a>
</Button>
)
}

View File

@ -0,0 +1,16 @@
"use client"
import { ThemeProvider } from "next-themes"
export function Providers({ children }: { children: React.ReactNode }) {
return (
<ThemeProvider
attribute="class"
defaultTheme="system"
disableTransitionOnChange
enableSystem
>
{children}
</ThemeProvider>
)
}

View File

@ -0,0 +1,18 @@
export function TailwindIndicator() {
if (
process.env.NODE_ENV === "production" ||
process.env.HIDE_TAILWIND_INDICATOR === "1"
)
return null
return (
<div className="fixed bottom-1 left-1 z-50 flex h-6 w-6 items-center justify-center rounded-full bg-zinc-800 p-3 font-mono text-xs text-white">
<div className="block sm:hidden">xs</div>
<div className="hidden sm:block md:hidden">sm</div>
<div className="hidden md:block lg:hidden">md</div>
<div className="hidden lg:block xl:hidden">lg</div>
<div className="hidden xl:block 2xl:hidden">xl</div>
<div className="hidden 2xl:block">2xl</div>
</div>
)
}

View File

@ -0,0 +1,57 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
outline:
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8",
icon: "h-9 w-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }

View File

@ -0,0 +1,76 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-xl border bg-card text-card-foreground shadow",
className
)}
{...props}
/>
))
Card.displayName = "Card"
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
))
CardHeader.displayName = "CardHeader"
const CardTitle = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("font-semibold leading-none tracking-tight", className)}
{...props}
/>
))
CardTitle.displayName = "CardTitle"
const CardDescription = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
CardDescription.displayName = "CardDescription"
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
))
CardContent.displayName = "CardContent"
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
))
CardFooter.displayName = "CardFooter"
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }

View File

@ -0,0 +1,22 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = "Input"
export { Input }

View File

@ -0,0 +1,26 @@
"use client"
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
)
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root
ref={ref}
className={cn(labelVariants(), className)}
{...props}
/>
))
Label.displayName = LabelPrimitive.Root.displayName
export { Label }

View File

@ -0,0 +1,22 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Textarea = React.forwardRef<
HTMLTextAreaElement,
React.ComponentProps<"textarea">
>(({ className, ...props }, ref) => {
return (
<textarea
className={cn(
"flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
ref={ref}
{...props}
/>
)
})
Textarea.displayName = "Textarea"
export { Textarea }

View File

@ -0,0 +1,16 @@
import { dirname } from "path";
import { fileURLToPath } from "url";
import { FlatCompat } from "@eslint/eslintrc";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
});
const eslintConfig = [
...compat.extends("next/core-web-vitals", "next/typescript"),
];
export default eslintConfig;

View File

@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

View File

@ -0,0 +1,10 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
outputFileTracingIncludes: {
registry: ["./registry/**/*"],
},
/* config options here */
};
export default nextConfig;

View File

@ -0,0 +1,39 @@
{
"name": "registry-template",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev --turbopack",
"build": "next build",
"start": "next start",
"lint": "next lint",
"registry:build": "shadcn build"
},
"dependencies": {
"@radix-ui/react-label": "^2.1.1",
"@radix-ui/react-slot": "^1.1.1",
"@tanstack/react-query": "^5.64.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.471.1",
"next": "15.1.4",
"next-themes": "^0.4.4",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"shadcn": "2.4.0-canary.12",
"tailwind-merge": "^2.6.0",
"tailwindcss-animate": "^1.0.7",
"zod": "^3.24.1"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "15.1.4",
"postcss": "^8",
"tailwindcss": "^3.4.1",
"typescript": "^5"
}
}

5029
registry-template/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,8 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
tailwindcss: {},
},
};
export default config;

View File

@ -0,0 +1 @@
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 391 B

View File

@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,38 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "complex-component",
"type": "registry:component",
"title": "Complex Component",
"description": "A complex component showing hooks, libs and components.",
"registryDependencies": [
"card"
],
"files": [
{
"path": "registry/new-york/complex-component/page.tsx",
"content": "import { cache } from \"react\"\nimport { PokemonCard } from \"@/registry/new-york/complex-component/components/pokemon-card\"\nimport { getPokemonList } from \"@/registry/new-york/complex-component/lib/pokemon\"\n\nconst getCachedPokemonList = cache(getPokemonList)\n\nexport default async function Page() {\n const pokemons = await getCachedPokemonList({ limit: 12 })\n\n if (!pokemons) {\n return null\n }\n\n return (\n <div className=\"mx-auto w-full max-w-2xl px-4\">\n <div className=\"grid grid-cols-2 gap-4 py-10 sm:grid-cols-3 md:grid-cols-4\">\n {pokemons.results.map((p) => (\n <PokemonCard key={p.name} name={p.name} />\n ))}\n </div>\n </div>\n )\n}\n",
"type": "registry:page",
"target": "app/pokemon/page.tsx"
},
{
"path": "registry/new-york/complex-component/components/pokemon-card.tsx",
"content": "import { cache } from \"react\"\nimport { getPokemon } from \"@/registry/new-york/complex-component/lib/pokemon\"\nimport { Card, CardContent } from \"@/components/ui/card\"\nimport { PokemonImage } from \"@/registry/new-york/complex-component/components/pokemon-image\"\n\nconst cachedGetPokemon = cache(getPokemon)\n\nexport async function PokemonCard({ name }: { name: string }) {\n const pokemon = await cachedGetPokemon(name)\n\n if (!pokemon) {\n return null\n }\n\n return (\n <Card>\n <CardContent className=\"flex flex-col items-center p-2\">\n <div>\n <PokemonImage name={pokemon.name} number={pokemon.id} />\n </div>\n <div className=\"text-center font-medium\">{pokemon.name}</div>\n </CardContent>\n </Card>\n )\n}\n",
"type": "registry:component"
},
{
"path": "registry/new-york/complex-component/components/pokemon-image.tsx",
"content": "\"use client\"\n\n/* eslint-disable @next/next/no-img-element */\nimport { usePokemonImage } from \"@/registry/new-york/complex-component/hooks/use-pokemon\"\n\nexport function PokemonImage({\n name,\n number,\n}: {\n name: string\n number: number\n}) {\n const imageUrl = usePokemonImage(number)\n\n if (!imageUrl) {\n return null\n }\n\n return <img src={imageUrl} alt={name} />\n}\n",
"type": "registry:component"
},
{
"path": "registry/new-york/complex-component/lib/pokemon.ts",
"content": "import { z } from \"zod\"\n\nexport async function getPokemonList({ limit = 10 }: { limit?: number }) {\n try {\n const response = await fetch(\n `https://pokeapi.co/api/v2/pokemon?limit=${limit}`\n )\n return z\n .object({\n results: z.array(z.object({ name: z.string() })),\n })\n .parse(await response.json())\n } catch (error) {\n console.error(error)\n return null\n }\n}\n\nexport async function getPokemon(name: string) {\n try {\n const response = await fetch(`https://pokeapi.co/api/v2/pokemon/${name}`)\n\n if (!response.ok) {\n throw new Error(\"Failed to fetch pokemon\")\n }\n\n return z\n .object({\n name: z.string(),\n id: z.number(),\n sprites: z.object({\n front_default: z.string(),\n }),\n stats: z.array(\n z.object({\n base_stat: z.number(),\n stat: z.object({\n name: z.string(),\n }),\n })\n ),\n })\n .parse(await response.json())\n } catch (error) {\n console.error(error)\n return null\n }\n}\n",
"type": "registry:lib"
},
{
"path": "registry/new-york/complex-component/hooks/use-pokemon.ts",
"content": "\"use client\"\n\n// Totally unnecessary hook, but it's a good example of how to use a hook in a custom registry.\n\nexport function usePokemonImage(number: number) {\n return `https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/${number}.png`\n}\n",
"type": "registry:hook"
}
]
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,17 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "hello-world",
"type": "registry:component",
"title": "Hello World",
"description": "A simple hello world component",
"registryDependencies": [
"button"
],
"files": [
{
"path": "registry/new-york/hello-world/hello-world.tsx",
"content": "export function HelloWorld() {\n return <h1 className=\"text-2xl font-bold\">Hello World</h1>\n}\n",
"type": "registry:component"
}
]
}

View File

@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 128 B

View File

@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>

After

Width:  |  Height:  |  Size: 385 B

View File

@ -0,0 +1,64 @@
{
"$schema": "https://ui.shadcn.com/schema/registry.json",
"name": "acme",
"homepage": "https://acme.com",
"items": [
{
"name": "hello-world",
"type": "registry:component",
"title": "Hello World",
"description": "A simple hello world component",
"registryDependencies": ["button"],
"files": [
{
"path": "registry/new-york/hello-world/hello-world.tsx",
"type": "registry:component"
}
]
},
{
"name": "example-form",
"type": "registry:component",
"title": "Example Form",
"description": "A contact form with Zod validation.",
"dependencies": ["zod"],
"registryDependencies": ["button", "input", "label", "textarea", "card"],
"files": [
{
"path": "registry/new-york/example-form/example-form.tsx",
"type": "registry:component"
}
]
},
{
"name": "complex-component",
"type": "registry:component",
"title": "Complex Component",
"description": "A complex component showing hooks, libs and components.",
"registryDependencies": ["card"],
"files": [
{
"path": "registry/new-york/complex-component/page.tsx",
"type": "registry:page",
"target": "app/pokemon/page.tsx"
},
{
"path": "registry/new-york/complex-component/components/pokemon-card.tsx",
"type": "registry:component"
},
{
"path": "registry/new-york/complex-component/components/pokemon-image.tsx",
"type": "registry:component"
},
{
"path": "registry/new-york/complex-component/lib/pokemon.ts",
"type": "registry:lib"
},
{
"path": "registry/new-york/complex-component/hooks/use-pokemon.ts",
"type": "registry:hook"
}
]
}
]
}

View File

@ -0,0 +1,5 @@
import "@/app/globals.css"
export default function Layout({ children }: { children: React.ReactNode }) {
return <>{children}</>
}

View File

@ -0,0 +1,25 @@
import { cache } from "react"
import { getPokemon } from "@/registry/new-york/complex-component/lib/pokemon"
import { Card, CardContent } from "@/components/ui/card"
import { PokemonImage } from "@/registry/new-york/complex-component/components/pokemon-image"
const cachedGetPokemon = cache(getPokemon)
export async function PokemonCard({ name }: { name: string }) {
const pokemon = await cachedGetPokemon(name)
if (!pokemon) {
return null
}
return (
<Card>
<CardContent className="flex flex-col items-center p-2">
<div>
<PokemonImage name={pokemon.name} number={pokemon.id} />
</div>
<div className="text-center font-medium">{pokemon.name}</div>
</CardContent>
</Card>
)
}

View File

@ -0,0 +1,20 @@
"use client"
/* eslint-disable @next/next/no-img-element */
import { usePokemonImage } from "@/registry/new-york/complex-component/hooks/use-pokemon"
export function PokemonImage({
name,
number,
}: {
name: string
number: number
}) {
const imageUrl = usePokemonImage(number)
if (!imageUrl) {
return null
}
return <img src={imageUrl} alt={name} />
}

View File

@ -0,0 +1,7 @@
"use client"
// Totally unnecessary hook, but it's a good example of how to use a hook in a custom registry.
export function usePokemonImage(number: number) {
return `https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/${number}.png`
}

View File

@ -0,0 +1,48 @@
import { z } from "zod"
export async function getPokemonList({ limit = 10 }: { limit?: number }) {
try {
const response = await fetch(
`https://pokeapi.co/api/v2/pokemon?limit=${limit}`
)
return z
.object({
results: z.array(z.object({ name: z.string() })),
})
.parse(await response.json())
} catch (error) {
console.error(error)
return null
}
}
export async function getPokemon(name: string) {
try {
const response = await fetch(`https://pokeapi.co/api/v2/pokemon/${name}`)
if (!response.ok) {
throw new Error("Failed to fetch pokemon")
}
return z
.object({
name: z.string(),
id: z.number(),
sprites: z.object({
front_default: z.string(),
}),
stats: z.array(
z.object({
base_stat: z.number(),
stat: z.object({
name: z.string(),
}),
})
),
})
.parse(await response.json())
} catch (error) {
console.error(error)
return null
}
}

View File

@ -0,0 +1,23 @@
import { cache } from "react"
import { PokemonCard } from "@/registry/new-york/complex-component/components/pokemon-card"
import { getPokemonList } from "@/registry/new-york/complex-component/lib/pokemon"
const getCachedPokemonList = cache(getPokemonList)
export default async function Page() {
const pokemons = await getCachedPokemonList({ limit: 12 })
if (!pokemons) {
return null
}
return (
<div className="mx-auto w-full max-w-2xl px-4">
<div className="grid grid-cols-2 gap-4 py-10 sm:grid-cols-3 md:grid-cols-4">
{pokemons.results.map((p) => (
<PokemonCard key={p.name} name={p.name} />
))}
</div>
</div>
)
}

View File

@ -0,0 +1,164 @@
"use client"
import * as React from "react"
import {
Card,
CardTitle,
CardHeader,
CardDescription,
CardContent,
CardFooter,
} from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Button } from "@/components/ui/button"
import { Textarea } from "@/components/ui/textarea"
import { z } from "zod"
const exampleFormSchema = z.object({
name: z.string().min(1),
email: z.string().email(),
message: z.string().min(1),
})
export function ExampleForm() {
const [pending, setPending] = React.useState(false)
const [state, setState] = React.useState({
defaultValues: {
name: "",
email: "",
message: "",
},
success: false,
errors: {
name: "",
email: "",
message: "",
},
})
const handleSubmit = React.useCallback(
(e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
setPending(true)
const formData = new FormData(e.target as HTMLFormElement)
const data = Object.fromEntries(formData.entries())
const result = exampleFormSchema.safeParse(data)
if (!result.success) {
setState({
...state,
errors: Object.fromEntries(
Object.entries(result.error.flatten().fieldErrors).map(
([key, value]) => [key, value?.[0] ?? ""]
)
) as Record<keyof typeof state.errors, string>,
})
setPending(false)
return
}
setPending(false)
},
[state]
)
return (
<Card className="w-full max-w-sm">
<CardHeader>
<CardTitle>How can we help?</CardTitle>
<CardDescription>
Need help with your project? We&apos;re here to assist you.
</CardDescription>
</CardHeader>
<form onSubmit={handleSubmit}>
<CardContent className="flex flex-col gap-6">
<div
className="group/field grid gap-2"
data-invalid={!!state.errors?.name}
>
<Label
htmlFor="name"
className="group-data-[invalid=true]/field:text-destructive"
>
Name <span aria-hidden="true">*</span>
</Label>
<Input
id="name"
name="name"
placeholder="Lee Robinson"
className="group-data-[invalid=true]/field:border-destructive focus-visible:group-data-[invalid=true]/field:ring-destructive"
disabled={pending}
aria-invalid={!!state.errors?.name}
aria-errormessage="error-name"
defaultValue={state.defaultValues.name}
/>
{state.errors?.name && (
<p id="error-name" className="text-destructive text-sm">
{state.errors.name}
</p>
)}
</div>
<div
className="group/field grid gap-2"
data-invalid={!!state.errors?.email}
>
<Label
htmlFor="email"
className="group-data-[invalid=true]/field:text-destructive"
>
Email <span aria-hidden="true">*</span>
</Label>
<Input
id="email"
name="email"
placeholder="leerob@acme.com"
className="group-data-[invalid=true]/field:border-destructive focus-visible:group-data-[invalid=true]/field:ring-destructive"
disabled={pending}
aria-invalid={!!state.errors?.email}
aria-errormessage="error-email"
defaultValue={state.defaultValues.email}
/>
{state.errors?.email && (
<p id="error-email" className="text-destructive text-sm">
{state.errors.email}
</p>
)}
</div>
<div
className="group/field grid gap-2"
data-invalid={!!state.errors?.message}
>
<Label
htmlFor="message"
className="group-data-[invalid=true]/field:text-destructive"
>
Message <span aria-hidden="true">*</span>
</Label>
<Textarea
id="message"
name="message"
placeholder="Type your message here..."
className="group-data-[invalid=true]/field:border-destructive focus-visible:group-data-[invalid=true]/field:ring-destructive"
disabled={pending}
aria-invalid={!!state.errors?.message}
aria-errormessage="error-message"
defaultValue={state.defaultValues.message}
/>
{state.errors?.message && (
<p id="error-message" className="text-destructive text-sm">
{state.errors.message}
</p>
)}
</div>
</CardContent>
<CardFooter>
<Button type="submit" size="sm" disabled={pending}>
{pending ? "Sending..." : "Send Message"}
</Button>
</CardFooter>
</form>
</Card>
)
}

View File

@ -0,0 +1,3 @@
export function HelloWorld() {
return <h1 className="text-2xl font-bold">Hello World</h1>
}

View File

@ -0,0 +1,64 @@
import type { Config } from "tailwindcss"
import tailwindAnimate from "tailwindcss-animate"
export default {
darkMode: ["class"],
content: [
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
"./components/**/*.{js,ts,jsx,tsx,mdx}",
"./app/**/*.{js,ts,jsx,tsx,mdx}",
"./registry/**/*.{js,ts,jsx,tsx,mdx}",
],
theme: {
extend: {
colors: {
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
chart: {
"1": "hsl(var(--chart-1))",
"2": "hsl(var(--chart-2))",
"3": "hsl(var(--chart-3))",
"4": "hsl(var(--chart-4))",
"5": "hsl(var(--chart-5))",
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
},
},
plugins: [tailwindAnimate],
} satisfies Config

View File

@ -0,0 +1,27 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}

View File

View File

@ -0,0 +1,20 @@
{
"name": "registry",
"version": "0.0.1",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"init": "shadcn init",
"registry:build": "shadcn registry:build"
},
"keywords": [],
"author": "abearxiong <xiongxiao@xiongxiao.me> (https://www.xiongxiao.me)",
"license": "MIT",
"packageManager": "pnpm@10.6.2",
"type": "module",
"dependencies": {
"shadcn": "2.6.0-canary.2"
}
}

2335
registry-test/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,19 @@
{
"$schema": "https://ui.shadcn.com/schema/registry.json",
"name": "xiongxiao",
"homepage": "https://xiongxiao.me",
"items": [
{
"name": "hello-world",
"type": "registry:block",
"title": "Hello World",
"description": "A simple hello world component.",
"files": [
{
"path": "registry/test/hello-world/hello-world.tsx",
"type": "registry:component"
}
]
}
]
}

View File

@ -0,0 +1,5 @@
import { Button } from "@/components/ui/button"
export function HelloWorld() {
return <Button>Hello World</Button>
}