feat: refactor registry (#8598)

* feat: refactor registry

* fix: remove components

* refactor: getActiveStyle

* fix: prettier in build-registry

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>

* fix

* Update apps/v4/scripts/build-registry.mts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* fix

* Update apps/v4/scripts/build-registry.mts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update apps/v4/components/block-viewer.tsx

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
shadcn
2025-10-29 15:07:56 +04:00
committed by GitHub
parent 39fdf94550
commit 84bd724d97
39 changed files with 7688 additions and 11116 deletions

View File

@@ -1,6 +1,7 @@
import { getAllBlockIds } from "@/lib/blocks"
import { registryCategories } from "@/lib/categories"
import { BlockDisplay } from "@/components/block-display"
import { registryCategories } from "@/registry/registry-categories"
import { getActiveStyle } from "@/registry/styles"
export const revalidate = false
export const dynamic = "force-static"
@@ -17,13 +18,16 @@ export default async function BlocksPage({
}: {
params: Promise<{ categories?: string[] }>
}) {
const { categories = [] } = await params
const [{ categories = [] }, activeStyle] = await Promise.all([
params,
getActiveStyle(),
])
const blocks = await getAllBlockIds(["registry:block"], categories)
return (
<div className="flex flex-col gap-12 md:gap-24">
{blocks.map((name) => (
<BlockDisplay name={name} key={name} />
<BlockDisplay name={name} key={name} styleName={activeStyle.name} />
))}
</div>
)

View File

@@ -2,6 +2,7 @@ import Link from "next/link"
import { BlockDisplay } from "@/components/block-display"
import { Button } from "@/registry/new-york-v4/ui/button"
import { getActiveStyle } from "@/registry/styles"
export const dynamic = "force-static"
export const revalidate = false
@@ -15,10 +16,12 @@ const FEATURED_BLOCKS = [
]
export default async function BlocksPage() {
const activeStyle = await getActiveStyle()
return (
<div className="flex flex-col gap-12 md:gap-24">
{FEATURED_BLOCKS.map((name) => (
<BlockDisplay name={name} key={name} />
<BlockDisplay name={name} key={name} styleName={activeStyle.name} />
))}
<div className="container-wrapper">
<div className="container flex justify-center py-6">

View File

@@ -3,6 +3,7 @@ import { notFound } from "next/navigation"
import { cn } from "@/lib/utils"
import { ChartDisplay } from "@/components/chart-display"
import { getActiveStyle } from "@/registry/styles"
import { charts } from "@/app/(app)/charts/charts"
export const revalidate = false
@@ -41,6 +42,7 @@ export default async function ChartPage({ params }: ChartPageProps) {
const chartType = type as ChartType
const chartList = charts[chartType]
const activeStyle = await getActiveStyle()
return (
<div className="grid flex-1 gap-12 lg:gap-24">
@@ -54,6 +56,7 @@ export default async function ChartPage({ params }: ChartPageProps) {
<ChartDisplay
key={chart.id}
name={chart.id}
styleName={activeStyle.name}
className={cn(chart.fullWidth && "md:col-span-2 lg:col-span-3")}
>
<chart.component />

View File

@@ -3,6 +3,7 @@ import { NextResponse, type NextRequest } from "next/server"
import { processMdxForLLMs } from "@/lib/llm"
import { source } from "@/lib/source"
import { getActiveStyle } from "@/registry/styles"
export const revalidate = false
@@ -10,14 +11,18 @@ export async function GET(
_req: NextRequest,
{ params }: { params: Promise<{ slug?: string[] }> }
) {
const slug = (await params).slug
const [{ slug }, activeStyle] = await Promise.all([params, getActiveStyle()])
const page = source.getPage(slug)
if (!page) {
notFound()
}
const processedContent = processMdxForLLMs(await page.data.getText("raw"))
const processedContent = processMdxForLLMs(
await page.data.getText("raw"),
activeStyle.name
)
return new NextResponse(processedContent, {
headers: {

View File

@@ -0,0 +1,105 @@
import { Metadata } from "next"
import { notFound } from "next/navigation"
import { siteConfig } from "@/lib/config"
import { getRegistryComponent, getRegistryItems } from "@/lib/registry"
import { absoluteUrl, cn } from "@/lib/utils"
import { getStyle, STYLES } from "@/registry/styles"
export const revalidate = false
export const dynamic = "force-static"
export const dynamicParams = false
const allowedTypes = ["registry:example"]
export async function generateMetadata({
params,
}: {
params: Promise<{
style: string
}>
}): Promise<Metadata> {
const { style: styleName } = await params
const style = getStyle(styleName)
if (!style) {
return {}
}
const title = style.title
return {
title,
openGraph: {
title,
type: "article",
url: absoluteUrl(`/sandbox/${style.name}`),
images: [
{
url: siteConfig.ogImage,
width: 1200,
height: 630,
alt: siteConfig.name,
},
],
},
twitter: {
card: "summary_large_image",
title,
images: [siteConfig.ogImage],
creator: "@shadcn",
},
}
}
export async function generateStaticParams() {
return STYLES.map((style) => ({
style: style.name,
}))
}
export default async function BlockPage({
params,
}: {
params: Promise<{
style: string
}>
}) {
const { style: styleName } = await params
const style = getStyle(styleName)
if (!style) {
return notFound()
}
const items = await getRegistryItems(style.name, (item) =>
allowedTypes.includes(item.type)
)
if (items.length === 0) {
return notFound()
}
return (
<>
<div className={cn("grid gap-6")}>
{items
.filter((item) => item !== null)
.map((item) => {
const Component = getRegistryComponent(item.name, style.name)
if (!Component) {
return null
}
return (
<div
key={item.name}
className={cn("bg-background", item.meta?.container)}
>
<Component />
</div>
)
})}
</div>
</>
)
}

View File

@@ -2,30 +2,38 @@
import * as React from "react"
import { Metadata } from "next"
import { notFound } from "next/navigation"
import { registryItemSchema } from "shadcn/schema"
import { z } from "zod"
import { siteConfig } from "@/lib/config"
import { getRegistryComponent, getRegistryItem } from "@/lib/registry"
import { absoluteUrl, cn } from "@/lib/utils"
import { getStyle, STYLES, type Style } from "@/registry/styles"
export const revalidate = false
export const dynamic = "force-static"
export const dynamicParams = false
const getCachedRegistryItem = React.cache(async (name: string) => {
return await getRegistryItem(name)
})
const getCachedRegistryItem = React.cache(
async (name: string, styleName: Style["name"]) => {
return await getRegistryItem(name, styleName)
}
)
export async function generateMetadata({
params,
}: {
params: Promise<{
style: string
name: string
}>
}): Promise<Metadata> {
const { name } = await params
const item = await getCachedRegistryItem(name)
const { style: styleName, name } = await params
const style = getStyle(styleName)
if (!style) {
return {}
}
const item = await getCachedRegistryItem(name, style.name)
if (!item) {
return {}
@@ -35,13 +43,13 @@ export async function generateMetadata({
const description = item.description
return {
title: item.description,
title: item.name,
description,
openGraph: {
title,
description,
type: "article",
url: absoluteUrl(`/view/${item.name}`),
url: absoluteUrl(`/view/${style.name}/${item.name}`),
images: [
{
url: siteConfig.ogImage,
@@ -63,32 +71,52 @@ export async function generateMetadata({
export async function generateStaticParams() {
const { Index } = await import("@/registry/__index__")
const index = z.record(registryItemSchema).parse(Index)
const params: Array<{ style: string; name: string }> = []
return Object.values(index)
.filter((block) =>
[
"registry:block",
"registry:component",
"registry:example",
"registry:internal",
].includes(block.type)
)
.map((block) => ({
name: block.name,
}))
for (const style of STYLES) {
if (!Index[style.name]) {
continue
}
const styleIndex = Index[style.name]
for (const itemName in styleIndex) {
const item = styleIndex[itemName]
if (
[
"registry:block",
"registry:component",
"registry:example",
"registry:internal",
].includes(item.type)
) {
params.push({
style: style.name,
name: item.name,
})
}
}
}
return params
}
export default async function BlockPage({
params,
}: {
params: Promise<{
style: string
name: string
}>
}) {
const { name } = await params
const item = await getCachedRegistryItem(name)
const Component = getRegistryComponent(name)
const { style: styleName, name } = await params
const style = getStyle(styleName)
if (!style) {
return notFound()
}
const item = await getCachedRegistryItem(name, style.name)
const Component = getRegistryComponent(name, style.name)
if (!item || !Component) {
return notFound()

View File

@@ -10,9 +10,16 @@ import {
import { cn } from "@/lib/utils"
import { BlockViewer } from "@/components/block-viewer"
import { ComponentPreview } from "@/components/component-preview"
import { type Style } from "@/registry/styles"
export async function BlockDisplay({ name }: { name: string }) {
const item = await getCachedRegistryItem(name)
export async function BlockDisplay({
name,
styleName,
}: {
name: string
styleName: Style["name"]
}) {
const item = await getCachedRegistryItem(name, styleName)
if (!item?.files) {
return null
@@ -24,9 +31,15 @@ export async function BlockDisplay({ name }: { name: string }) {
])
return (
<BlockViewer item={item} tree={tree} highlightedFiles={highlightedFiles}>
<BlockViewer
item={item}
tree={tree}
highlightedFiles={highlightedFiles}
styleName={styleName}
>
<ComponentPreview
name={item.name}
styleName={styleName}
hideCode
className={cn(
"my-0 **:[.preview]:h-auto **:[.preview]:p-4 **:[.preview>.p-6]:p-0",
@@ -37,9 +50,11 @@ export async function BlockDisplay({ name }: { name: string }) {
)
}
const getCachedRegistryItem = React.cache(async (name: string) => {
return await getRegistryItem(name)
})
const getCachedRegistryItem = React.cache(
async (name: string, styleName: Style["name"]) => {
return await getRegistryItem(name, styleName)
}
)
const getCachedFileTree = React.cache(
async (files: Array<{ path: string; target?: string }>) => {

View File

@@ -54,6 +54,7 @@ import {
ToggleGroup,
ToggleGroupItem,
} from "@/registry/new-york-v4/ui/toggle-group"
import { type Style } from "@/registry/styles"
type BlockViewerContext = {
item: z.infer<typeof registryItemSchema>
@@ -128,7 +129,15 @@ function BlockViewerProvider({
)
}
function BlockViewerToolbar() {
type BlockViewerProps = Pick<
BlockViewerContext,
"item" | "tree" | "highlightedFiles"
> & {
children: React.ReactNode
styleName: Style["name"]
}
function BlockViewerToolbar({ styleName }: { styleName: Style["name"] }) {
const { setView, view, item, resizablePanelRef, setIframeKey } =
useBlockViewer()
const { copyToClipboard, isCopied } = useCopyToClipboard()
@@ -181,7 +190,7 @@ function BlockViewerToolbar() {
asChild
title="Open in New Tab"
>
<Link href={`/view/${item.name}`} target="_blank">
<Link href={`/view/${styleName}/${item.name}`} target="_blank">
<span className="sr-only">Open in New Tab</span>
<Fullscreen />
</Link>
@@ -222,13 +231,19 @@ function BlockViewerToolbar() {
)
}
function BlockViewerIframe({ className }: { className?: string }) {
function BlockViewerIframe({
className,
styleName,
}: {
className?: string
styleName: Style["name"]
}) {
const { item, iframeKey } = useBlockViewer()
return (
<iframe
key={iframeKey}
src={`/view/${item.name}`}
src={`/view/${styleName}/${item.name}`}
height={item.meta?.iframeHeight ?? 930}
loading="lazy"
className={cn(
@@ -239,7 +254,7 @@ function BlockViewerIframe({ className }: { className?: string }) {
)
}
function BlockViewerView() {
function BlockViewerView({ styleName }: { styleName: Style["name"] }) {
const { resizablePanelRef } = useBlockViewer()
return (
@@ -256,7 +271,7 @@ function BlockViewerView() {
defaultSize={100}
minSize={30}
>
<BlockViewerIframe />
<BlockViewerIframe styleName={styleName} />
</ResizablePanel>
<ResizableHandle className="after:bg-border relative hidden w-3 bg-transparent p-0 after:absolute after:top-1/2 after:right-0 after:h-8 after:w-[6px] after:translate-x-[-1px] after:-translate-y-1/2 after:rounded-full after:transition-all after:hover:h-10 md:block" />
<ResizablePanel defaultSize={0} minSize={0} />
@@ -471,10 +486,9 @@ function BlockViewer({
tree,
highlightedFiles,
children,
styleName,
...props
}: Pick<BlockViewerContext, "item" | "tree" | "highlightedFiles"> & {
children: React.ReactNode
}) {
}: BlockViewerProps) {
return (
<BlockViewerProvider
item={item}
@@ -482,8 +496,8 @@ function BlockViewer({
highlightedFiles={highlightedFiles}
{...props}
>
<BlockViewerToolbar />
<BlockViewerView />
<BlockViewerToolbar styleName={styleName} />
<BlockViewerView styleName={styleName} />
<BlockViewerCode />
<BlockViewerMobile>{children}</BlockViewerMobile>
</BlockViewerProvider>

View File

@@ -3,8 +3,8 @@
import Link from "next/link"
import { usePathname } from "next/navigation"
import { registryCategories } from "@/lib/categories"
import { ScrollArea, ScrollBar } from "@/registry/new-york-v4/ui/scroll-area"
import { registryCategories } from "@/registry/registry-categories"
export function BlocksNav() {
const pathname = usePathname()

View File

@@ -6,6 +6,7 @@ import { highlightCode } from "@/lib/highlight-code"
import { getRegistryItem } from "@/lib/registry"
import { cn } from "@/lib/utils"
import { ChartToolbar } from "@/components/chart-toolbar"
import { type Style } from "@/registry/styles"
export type Chart = z.infer<typeof registryItemSchema> & {
highlightedCode: string
@@ -13,10 +14,14 @@ export type Chart = z.infer<typeof registryItemSchema> & {
export async function ChartDisplay({
name,
styleName,
children,
className,
}: { name: string } & React.ComponentProps<"div">) {
const chart = await getCachedRegistryItem(name)
}: {
name: string
styleName: Style["name"]
} & React.ComponentProps<"div">) {
const chart = await getCachedRegistryItem(name, styleName)
const highlightedCode = await getChartHighlightedCode(
chart?.files?.[0]?.content ?? ""
)
@@ -45,9 +50,11 @@ export async function ChartDisplay({
)
}
const getCachedRegistryItem = React.cache(async (name: string) => {
return await getRegistryItem(name)
})
const getCachedRegistryItem = React.cache(
async (name: string, styleName: Style["name"]) => {
return await getRegistryItem(name, styleName)
}
)
const getChartHighlightedCode = React.cache(async (content: string) => {
return await highlightCode(content)

View File

@@ -3,9 +3,11 @@ import Image from "next/image"
import { ComponentPreviewTabs } from "@/components/component-preview-tabs"
import { ComponentSource } from "@/components/component-source"
import { Index } from "@/registry/__index__"
import { type Style } from "@/registry/styles"
export function ComponentPreview({
name,
styleName = "new-york-v4",
type,
className,
align = "center",
@@ -14,13 +16,14 @@ export function ComponentPreview({
...props
}: React.ComponentProps<"div"> & {
name: string
styleName?: Style["name"]
align?: "center" | "start" | "end"
description?: string
hideCode?: boolean
type?: "block" | "component" | "example"
chromeLessOnMobile?: boolean
}) {
const Component = Index[name]?.component
const Component = Index[styleName]?.[name]?.component
if (!Component) {
return (
@@ -52,7 +55,7 @@ export function ComponentPreview({
className="bg-background absolute top-0 left-0 z-20 hidden w-[970px] max-w-none sm:w-[1280px] md:hidden dark:block md:dark:hidden"
/>
<div className="bg-background absolute inset-0 hidden w-[1600px] md:block">
<iframe src={`/view/${name}`} className="size-full" />
<iframe src={`/view/${styleName}/${name}`} className="size-full" />
</div>
</div>
)
@@ -64,7 +67,13 @@ export function ComponentPreview({
align={align}
hideCode={hideCode}
component={<Component />}
source={<ComponentSource name={name} collapsible={false} />}
source={
<ComponentSource
name={name}
collapsible={false}
styleName={styleName}
/>
}
chromeLessOnMobile={chromeLessOnMobile}
{...props}
/>

View File

@@ -8,6 +8,7 @@ import { cn } from "@/lib/utils"
import { CodeCollapsibleWrapper } from "@/components/code-collapsible-wrapper"
import { CopyButton } from "@/components/copy-button"
import { getIconForLanguageExtension } from "@/components/icons"
import { type Style } from "@/registry/styles"
export async function ComponentSource({
name,
@@ -16,12 +17,14 @@ export async function ComponentSource({
language,
collapsible = true,
className,
styleName = "new-york-v4",
}: React.ComponentProps<"div"> & {
name?: string
src?: string
title?: string
language?: string
collapsible?: boolean
styleName?: Style["name"]
}) {
if (!name && !src) {
return null
@@ -30,7 +33,7 @@ export async function ComponentSource({
let code: string | undefined
if (name) {
const item = await getRegistryItem(name)
const item = await getRegistryItem(name, styleName)
code = item?.files?.[0]?.content
}
@@ -44,8 +47,8 @@ export async function ComponentSource({
}
// Fix imports.
// Replace @/registry/new-york-v4/ with @/components/.
code = code.replaceAll("@/registry/new-york-v4/", "@/components/")
// Replace @/registry/${style}/ with @/components/.
code = code.replaceAll(`@/registry/${styleName}/`, "@/components/")
// Replace export default with export.
code = code.replaceAll("export default", "export")

View File

@@ -9,6 +9,7 @@ import { cn } from "@/lib/utils"
import { useThemeConfig } from "@/components/active-theme"
import { copyToClipboardWithMeta } from "@/components/copy-button"
import { Icons } from "@/components/icons"
import { BaseColor, baseColors, baseColorsOKLCH } from "@/registry/base-colors"
import { Button } from "@/registry/new-york-v4/ui/button"
import {
Dialog,
@@ -42,11 +43,6 @@ import {
TabsList,
TabsTrigger,
} from "@/registry/new-york-v4/ui/tabs"
import {
BaseColor,
baseColors,
baseColorsOKLCH,
} from "@/registry/registry-base-colors"
interface BaseColorOKLCH {
light: Record<string, string>

View File

@@ -23,9 +23,31 @@ export async function getAllBlocks(
categories: string[] = []
) {
const { Index } = await import("@/registry/__index__")
const index = z.record(registryItemSchema).parse(Index)
return Object.values(index).filter(
// Collect all blocks from all styles.
const allBlocks: z.infer<typeof registryItemSchema>[] = []
for (const style in Index) {
const styleIndex = Index[style]
if (typeof styleIndex === "object" && styleIndex !== null) {
for (const itemName in styleIndex) {
const item = styleIndex[itemName]
allBlocks.push(item)
}
}
}
// Validate each block.
const validatedBlocks = allBlocks
.map((block) => {
const result = registryItemSchema.safeParse(block)
return result.success ? result.data : null
})
.filter(
(block): block is z.infer<typeof registryItemSchema> => block !== null
)
return validatedBlocks.filter(
(block) =>
types.includes(block.type) &&
(categories.length === 0 ||

View File

@@ -1,6 +1,6 @@
import { z } from "zod"
import { colors } from "@/registry/registry-colors"
import { colors } from "@/registry/colors"
const colorSchema = z.object({
name: z.string(),

View File

@@ -1,14 +1,15 @@
import fs from "fs"
import { Index } from "@/registry/__index__"
import { type Style } from "@/registry/styles"
export function processMdxForLLMs(content: string) {
export function processMdxForLLMs(content: string, style: Style["name"]) {
const componentPreviewRegex =
/<ComponentPreview[\s\S]*?name="([^"]+)"[\s\S]*?\/>/g
return content.replace(componentPreviewRegex, (match, name) => {
try {
const component = Index[name]
const component = Index[style]?.[name]
if (!component?.files) {
return match
}

View File

@@ -6,13 +6,36 @@ import { Project, ScriptKind } from "ts-morph"
import { z } from "zod"
import { Index } from "@/registry/__index__"
import { type Style } from "@/registry/styles"
export function getRegistryComponent(name: string) {
return Index[name]?.component
export function getRegistryComponent(name: string, styleName: Style["name"]) {
return Index[styleName]?.[name]?.component
}
export async function getRegistryItem(name: string) {
const item = Index[name]
export async function getRegistryItems(
styleName: Style["name"],
filter?: (item: z.infer<typeof registryItemSchema>) => boolean
) {
const styleIndex = Index[styleName]
if (!styleIndex) {
return []
}
const entries = Object.values(styleIndex)
const filteredEntries = filter ? entries.filter(filter) : entries
return await Promise.all(
filteredEntries.map(async (entry) => {
const item = await getRegistryItem(entry.name, styleName)
return item
})
).then((results) => results.filter(Boolean))
}
export async function getRegistryItem(name: string, styleName: Style["name"]) {
const item = Index[styleName]?.[name]
if (!item) {
return null

View File

@@ -4,6 +4,7 @@ import { u } from "unist-builder"
import { visit } from "unist-util-visit"
import { Index } from "@/registry/__index__"
import { getActiveStyle } from "@/registry/styles"
interface UnistNode {
type: string
@@ -26,6 +27,8 @@ export interface UnistTree {
export function rehypeComponent() {
return async (tree: UnistTree) => {
const activeStyle = await getActiveStyle()
visit(tree, (node: UnistNode) => {
// src prop overrides both name and fileName.
const { value: srcPath } =
@@ -111,7 +114,7 @@ export function rehypeComponent() {
}
try {
const component = Index[name]
const component = Index[activeStyle.name]?.[name]
const src = component.files[0]?.path
// Read the source file.

View File

@@ -1,4 +1,4 @@
import { baseColors } from "@/registry/registry-base-colors"
import { baseColors } from "@/registry/base-colors"
export const THEMES = baseColors.filter(
(theme) => !["slate", "stone", "gray", "zinc"].includes(theme.name)

File diff suppressed because one or more lines are too long

View File

@@ -8,7 +8,7 @@
"files": [
{
"path": "registry/new-york-v4/examples/sheet-side.tsx",
"content": "\"use client\"\n\nimport { Button } from \"@/registry/new-york-v4/ui/button\"\nimport { Input } from \"@/registry/new-york-v4/ui/input\"\nimport { Label } from \"@/registry/new-york-v4/ui/label\"\nimport {\n Sheet,\n SheetClose,\n SheetContent,\n SheetDescription,\n SheetFooter,\n SheetHeader,\n SheetTitle,\n SheetTrigger,\n} from \"@/registry/new-york-v4/ui/sheet\"\n\nconst SHEET_SIDES = [\"top\", \"right\", \"bottom\", \"left\"] as const\n\ntype SheetSide = (typeof SHEET_SIDES)[number]\n\nexport default function SheetSide() {\n return (\n <div className=\"grid grid-cols-2 gap-2\">\n {SHEET_SIDES.map((side) => (\n <Sheet key={side}>\n <SheetTrigger asChild>\n <Button variant=\"outline\">{side}</Button>\n </SheetTrigger>\n <SheetContent side={side}>\n <SheetHeader>\n <SheetTitle>Edit profile</SheetTitle>\n <SheetDescription>\n Make changes to your profile here. Click save when you're done.\n </SheetDescription>\n </SheetHeader>\n <div className=\"grid gap-4 py-4\">\n <div className=\"grid grid-cols-4 items-center gap-4\">\n <Label htmlFor=\"name\" className=\"text-right\">\n Name\n </Label>\n <Input id=\"name\" value=\"Pedro Duarte\" className=\"col-span-3\" />\n </div>\n <div className=\"grid grid-cols-4 items-center gap-4\">\n <Label htmlFor=\"username\" className=\"text-right\">\n Username\n </Label>\n <Input id=\"username\" value=\"@peduarte\" className=\"col-span-3\" />\n </div>\n </div>\n <SheetFooter>\n <SheetClose asChild>\n <Button type=\"submit\">Save changes</Button>\n </SheetClose>\n </SheetFooter>\n </SheetContent>\n </Sheet>\n ))}\n </div>\n )\n}\n",
"content": "\"use client\"\n\nimport { Button } from \"@/registry/new-york-v4/ui/button\"\nimport { Input } from \"@/registry/new-york-v4/ui/input\"\nimport { Label } from \"@/registry/new-york-v4/ui/label\"\nimport {\n Sheet,\n SheetClose,\n SheetContent,\n SheetDescription,\n SheetFooter,\n SheetHeader,\n SheetTitle,\n SheetTrigger,\n} from \"@/registry/new-york-v4/ui/sheet\"\n\nconst SHEET_SIDES = [\"top\", \"right\", \"bottom\", \"left\"] as const\n\ntype SheetSide = (typeof SHEET_SIDES)[number]\n\nexport default function SheetSide() {\n return (\n <div className=\"grid grid-cols-2 gap-2\">\n {SHEET_SIDES.map((side) => (\n <Sheet key={side}>\n <SheetTrigger asChild>\n <Button variant=\"outline\">{side}</Button>\n </SheetTrigger>\n <SheetContent side={side}>\n <SheetHeader>\n <SheetTitle>Edit profile</SheetTitle>\n <SheetDescription>\n Make changes to your profile here. Click save when you&apos;re\n done.\n </SheetDescription>\n </SheetHeader>\n <div className=\"grid gap-4 py-4\">\n <div className=\"grid grid-cols-4 items-center gap-4\">\n <Label htmlFor=\"name\" className=\"text-right\">\n Name\n </Label>\n <Input id=\"name\" value=\"Pedro Duarte\" className=\"col-span-3\" />\n </div>\n <div className=\"grid grid-cols-4 items-center gap-4\">\n <Label htmlFor=\"username\" className=\"text-right\">\n Username\n </Label>\n <Input id=\"username\" value=\"@peduarte\" className=\"col-span-3\" />\n </div>\n </div>\n <SheetFooter>\n <SheetClose asChild>\n <Button type=\"submit\">Save changes</Button>\n </SheetClose>\n </SheetFooter>\n </SheetContent>\n </Sheet>\n ))}\n </div>\n )\n}\n",
"type": "registry:example"
}
]

View File

@@ -18,7 +18,7 @@
},
{
"path": "registry/new-york-v4/blocks/sidebar-11/components/app-sidebar.tsx",
"content": "import * as React from \"react\"\nimport { ChevronRight, File, Folder } from \"lucide-react\"\n\nimport {\n Collapsible,\n CollapsibleContent,\n CollapsibleTrigger,\n} from \"@/registry/new-york-v4/ui/collapsible\"\nimport {\n Sidebar,\n SidebarContent,\n SidebarGroup,\n SidebarGroupContent,\n SidebarGroupLabel,\n SidebarMenu,\n SidebarMenuBadge,\n SidebarMenuButton,\n SidebarMenuItem,\n SidebarMenuSub,\n SidebarRail,\n} from \"@/registry/new-york-v4/ui/sidebar\"\n\n// This is sample data.\nconst data = {\n changes: [\n {\n file: \"README.md\",\n state: \"M\",\n },\n {\n file: \"api/hello/route.ts\",\n state: \"U\",\n },\n {\n file: \"app/layout.tsx\",\n state: \"M\",\n },\n ],\n tree: [\n [\n \"app\",\n [\n \"api\",\n [\"hello\", [\"route.ts\"]],\n \"page.tsx\",\n \"layout.tsx\",\n [\"blog\", [\"page.tsx\"]],\n ],\n ],\n [\n \"components\",\n [\"ui\", \"button.tsx\", \"card.tsx\"],\n \"header.tsx\",\n \"footer.tsx\",\n ],\n [\"lib\", [\"util.ts\"]],\n [\"public\", \"favicon.ico\", \"vercel.svg\"],\n \".eslintrc.json\",\n \".gitignore\",\n \"next.config.js\",\n \"tailwind.config.js\",\n \"package.json\",\n \"README.md\",\n ],\n}\n\nexport function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {\n return (\n <Sidebar {...props}>\n <SidebarContent>\n <SidebarGroup>\n <SidebarGroupLabel>Changes</SidebarGroupLabel>\n <SidebarGroupContent>\n <SidebarMenu>\n {data.changes.map((item, index) => (\n <SidebarMenuItem key={index}>\n <SidebarMenuButton>\n <File />\n {item.file}\n </SidebarMenuButton>\n <SidebarMenuBadge>{item.state}</SidebarMenuBadge>\n </SidebarMenuItem>\n ))}\n </SidebarMenu>\n </SidebarGroupContent>\n </SidebarGroup>\n <SidebarGroup>\n <SidebarGroupLabel>Files</SidebarGroupLabel>\n <SidebarGroupContent>\n <SidebarMenu>\n {data.tree.map((item, index) => (\n <Tree key={index} item={item} />\n ))}\n </SidebarMenu>\n </SidebarGroupContent>\n </SidebarGroup>\n </SidebarContent>\n <SidebarRail />\n </Sidebar>\n )\n}\n\nfunction Tree({ item }: { item: string | any[] }) {\n const [name, ...items] = Array.isArray(item) ? item : [item]\n\n if (!items.length) {\n return (\n <SidebarMenuButton\n isActive={name === \"button.tsx\"}\n className=\"data-[active=true]:bg-transparent\"\n >\n <File />\n {name}\n </SidebarMenuButton>\n )\n }\n\n return (\n <SidebarMenuItem>\n <Collapsible\n className=\"group/collapsible [&[data-state=open]>button>svg:first-child]:rotate-90\"\n defaultOpen={name === \"components\" || name === \"ui\"}\n >\n <CollapsibleTrigger asChild>\n <SidebarMenuButton>\n <ChevronRight className=\"transition-transform\" />\n <Folder />\n {name}\n </SidebarMenuButton>\n </CollapsibleTrigger>\n <CollapsibleContent>\n <SidebarMenuSub>\n {items.map((subItem, index) => (\n <Tree key={index} item={subItem} />\n ))}\n </SidebarMenuSub>\n </CollapsibleContent>\n </Collapsible>\n </SidebarMenuItem>\n )\n}\n",
"content": "import * as React from \"react\"\nimport { ChevronRight, File, Folder } from \"lucide-react\"\n\nimport {\n Collapsible,\n CollapsibleContent,\n CollapsibleTrigger,\n} from \"@/registry/new-york-v4/ui/collapsible\"\nimport {\n Sidebar,\n SidebarContent,\n SidebarGroup,\n SidebarGroupContent,\n SidebarGroupLabel,\n SidebarMenu,\n SidebarMenuBadge,\n SidebarMenuButton,\n SidebarMenuItem,\n SidebarMenuSub,\n SidebarRail,\n} from \"@/registry/new-york-v4/ui/sidebar\"\n\n// This is sample data.\nconst data = {\n changes: [\n {\n file: \"README.md\",\n state: \"M\",\n },\n {\n file: \"api/hello/route.ts\",\n state: \"U\",\n },\n {\n file: \"app/layout.tsx\",\n state: \"M\",\n },\n ],\n tree: [\n [\n \"app\",\n [\n \"api\",\n [\"hello\", [\"route.ts\"]],\n \"page.tsx\",\n \"layout.tsx\",\n [\"blog\", [\"page.tsx\"]],\n ],\n ],\n [\n \"components\",\n [\"ui\", \"button.tsx\", \"card.tsx\"],\n \"header.tsx\",\n \"footer.tsx\",\n ],\n [\"lib\", [\"util.ts\"]],\n [\"public\", \"favicon.ico\", \"vercel.svg\"],\n \".eslintrc.json\",\n \".gitignore\",\n \"next.config.js\",\n \"tailwind.config.js\",\n \"package.json\",\n \"README.md\",\n ],\n}\n\nexport function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {\n return (\n <Sidebar {...props}>\n <SidebarContent>\n <SidebarGroup>\n <SidebarGroupLabel>Changes</SidebarGroupLabel>\n <SidebarGroupContent>\n <SidebarMenu>\n {data.changes.map((item, index) => (\n <SidebarMenuItem key={index}>\n <SidebarMenuButton>\n <File />\n {item.file}\n </SidebarMenuButton>\n <SidebarMenuBadge>{item.state}</SidebarMenuBadge>\n </SidebarMenuItem>\n ))}\n </SidebarMenu>\n </SidebarGroupContent>\n </SidebarGroup>\n <SidebarGroup>\n <SidebarGroupLabel>Files</SidebarGroupLabel>\n <SidebarGroupContent>\n <SidebarMenu>\n {data.tree.map((item, index) => (\n <Tree key={index} item={item} />\n ))}\n </SidebarMenu>\n </SidebarGroupContent>\n </SidebarGroup>\n </SidebarContent>\n <SidebarRail />\n </Sidebar>\n )\n}\n\ntype TreeItem = string | TreeItem[]\n\nfunction Tree({ item }: { item: TreeItem }) {\n const [name, ...items] = Array.isArray(item) ? item : [item]\n\n if (!items.length) {\n return (\n <SidebarMenuButton\n isActive={name === \"button.tsx\"}\n className=\"data-[active=true]:bg-transparent\"\n >\n <File />\n {name}\n </SidebarMenuButton>\n )\n }\n\n return (\n <SidebarMenuItem>\n <Collapsible\n className=\"group/collapsible [&[data-state=open]>button>svg:first-child]:rotate-90\"\n defaultOpen={name === \"components\" || name === \"ui\"}\n >\n <CollapsibleTrigger asChild>\n <SidebarMenuButton>\n <ChevronRight className=\"transition-transform\" />\n <Folder />\n {name}\n </SidebarMenuButton>\n </CollapsibleTrigger>\n <CollapsibleContent>\n <SidebarMenuSub>\n {items.map((subItem, index) => (\n <Tree key={index} item={subItem} />\n ))}\n </SidebarMenuSub>\n </CollapsibleContent>\n </Collapsible>\n </SidebarMenuItem>\n )\n}\n",
"type": "registry:component"
}
],

View File

@@ -5,7 +5,7 @@
"files": [
{
"path": "registry/new-york-v4/examples/typography-table.tsx",
"content": "export default function TypographyTable() {\n return (\n <div className=\"my-6 w-full overflow-y-auto\">\n <table className=\"w-full\">\n <thead>\n <tr className=\"even:bg-muted m-0 border-t p-0\">\n <th className=\"border px-4 py-2 text-left font-bold [&[align=center]]:text-center [&[align=right]]:text-right\">\n King's Treasury\n </th>\n <th className=\"border px-4 py-2 text-left font-bold [&[align=center]]:text-center [&[align=right]]:text-right\">\n People's happiness\n </th>\n </tr>\n </thead>\n <tbody>\n <tr className=\"even:bg-muted m-0 border-t p-0\">\n <td className=\"border px-4 py-2 text-left [&[align=center]]:text-center [&[align=right]]:text-right\">\n Empty\n </td>\n <td className=\"border px-4 py-2 text-left [&[align=center]]:text-center [&[align=right]]:text-right\">\n Overflowing\n </td>\n </tr>\n <tr className=\"even:bg-muted m-0 border-t p-0\">\n <td className=\"border px-4 py-2 text-left [&[align=center]]:text-center [&[align=right]]:text-right\">\n Modest\n </td>\n <td className=\"border px-4 py-2 text-left [&[align=center]]:text-center [&[align=right]]:text-right\">\n Satisfied\n </td>\n </tr>\n <tr className=\"even:bg-muted m-0 border-t p-0\">\n <td className=\"border px-4 py-2 text-left [&[align=center]]:text-center [&[align=right]]:text-right\">\n Full\n </td>\n <td className=\"border px-4 py-2 text-left [&[align=center]]:text-center [&[align=right]]:text-right\">\n Ecstatic\n </td>\n </tr>\n </tbody>\n </table>\n </div>\n )\n}\n",
"content": "export default function TypographyTable() {\n return (\n <div className=\"my-6 w-full overflow-y-auto\">\n <table className=\"w-full\">\n <thead>\n <tr className=\"even:bg-muted m-0 border-t p-0\">\n <th className=\"border px-4 py-2 text-left font-bold [&[align=center]]:text-center [&[align=right]]:text-right\">\n King&apos;s Treasury\n </th>\n <th className=\"border px-4 py-2 text-left font-bold [&[align=center]]:text-center [&[align=right]]:text-right\">\n People&apos;s happiness\n </th>\n </tr>\n </thead>\n <tbody>\n <tr className=\"even:bg-muted m-0 border-t p-0\">\n <td className=\"border px-4 py-2 text-left [&[align=center]]:text-center [&[align=right]]:text-right\">\n Empty\n </td>\n <td className=\"border px-4 py-2 text-left [&[align=center]]:text-center [&[align=right]]:text-right\">\n Overflowing\n </td>\n </tr>\n <tr className=\"even:bg-muted m-0 border-t p-0\">\n <td className=\"border px-4 py-2 text-left [&[align=center]]:text-center [&[align=right]]:text-right\">\n Modest\n </td>\n <td className=\"border px-4 py-2 text-left [&[align=center]]:text-center [&[align=right]]:text-right\">\n Satisfied\n </td>\n </tr>\n <tr className=\"even:bg-muted m-0 border-t p-0\">\n <td className=\"border px-4 py-2 text-left [&[align=center]]:text-center [&[align=right]]:text-right\">\n Full\n </td>\n <td className=\"border px-4 py-2 text-left [&[align=center]]:text-center [&[align=right]]:text-right\">\n Ecstatic\n </td>\n </tr>\n </tbody>\n </table>\n </div>\n )\n}\n",
"type": "registry:example"
}
]

File diff suppressed because it is too large Load Diff

View File

@@ -1,14 +1,14 @@
import { registryItemSchema, type Registry } from "shadcn/schema"
import { z } from "zod"
import { blocks } from "@/registry/registry-blocks"
import { charts } from "@/registry/registry-charts"
import { examples } from "@/registry/registry-examples"
import { hooks } from "@/registry/registry-hooks"
import { internal } from "@/registry/registry-internal"
import { lib } from "@/registry/registry-lib"
import { themes } from "@/registry/registry-themes"
import { ui } from "@/registry/registry-ui"
import { themes } from "../themes"
import { blocks } from "./blocks/_registry"
import { charts } from "./charts/_registry"
import { examples } from "./examples/_registry"
import { hooks } from "./hooks/_registry"
import { internal } from "./internal/_registry"
import { lib } from "./lib/_registry"
import { ui } from "./ui/_registry"
const DEPRECATED_ITEMS = [
"toast",

View File

@@ -0,0 +1,14 @@
export const STYLES = [
{ name: "new-york-v4" as const, title: "New York" },
] as const
export type Style = (typeof STYLES)[number]
export async function getActiveStyle() {
// In the future, this can read from cookies, session, etc.
return STYLES[0]
}
export function getStyle(name: string) {
return STYLES.find((style) => style.name === name)
}

View File

@@ -1,6 +1,6 @@
import { type Registry } from "shadcn/schema"
import { baseColorsV4 } from "@/registry/registry-base-colors"
import { baseColorsV4 } from "@/registry/base-colors"
// Create a theme for each color in the base colors.
export const themes: Registry["items"] = Object.keys(baseColorsV4).map(

View File

@@ -1,12 +1,13 @@
import { exec } from "child_process"
import { exec, execFile } from "child_process"
import { existsSync, promises as fs } from "fs"
import path from "path"
import { rimraf } from "rimraf"
import { registrySchema } from "shadcn/schema"
import { getAllBlocks } from "@/lib/blocks"
import { registry } from "@/registry/index"
import { STYLES, type Style } from "@/registry/styles"
async function buildRegistryIndex() {
async function buildRegistryIndex(styles: Style[]) {
let index = `/* eslint-disable @typescript-eslint/ban-ts-comment */
/* eslint-disable @typescript-eslint/no-explicit-any */
// @ts-nocheck
@@ -14,69 +15,111 @@ async function buildRegistryIndex() {
// Do not edit this file directly.
import * as React from "react"
export const Index: Record<string, any> = {`
for (const item of registry.items) {
const resolveFiles = item.files?.map(
(file) => `registry/new-york-v4/${file.path}`
export const Index: Record<string, Record<string, any>> = {`
for (const style of styles) {
// Dynamically import the registry for this style.
const { registry: importedRegistry } = await import(
`../registry/${style.name}/registry.ts`
)
if (!resolveFiles) {
continue
// Validate the registry schema.
const parseResult = registrySchema.safeParse(importedRegistry)
if (!parseResult.success) {
console.error(`❌ Registry validation failed for ${style.name}:`)
console.error(parseResult.error.format())
throw new Error(`Invalid registry schema for ${style.name}`)
}
const componentPath = item.files?.[0]?.path
? `@/registry/new-york-v4/${item.files[0].path}`
: ""
const registry = parseResult.data
index += `
"${style.name}": {`
for (const item of registry.items) {
const files =
item.files?.map((file) => ({
path: typeof file === "string" ? file : file.path,
type: typeof file === "string" ? item.type : file.type,
target: typeof file === "string" ? undefined : file.target,
})) ?? []
if (files.length === 0) {
continue
}
const componentPath = item.files?.[0]?.path
? `@/registry/${style.name}/${item.files[0].path}`
: ""
index += `
"${item.name}": {
name: "${item.name}",
description: "${item.description ?? ""}",
type: "${item.type}",
registryDependencies: ${JSON.stringify(item.registryDependencies)},
files: [${files.map((file) => {
const filePath = `registry/${style.name}/${file.path}`
return `{
path: "${filePath}",
type: "${file.type}",
target: "${file.target ?? ""}"
}`
})}],
component: ${
componentPath
? `React.lazy(async () => {
const mod = await import("${componentPath}")
const exportName = Object.keys(mod).find(key => typeof mod[key] === 'function' || typeof mod[key] === 'object') || item.name
return { default: mod.default || mod[exportName] }
})`
: "null"
},
categories: ${JSON.stringify(item.categories)},
meta: ${JSON.stringify(item.meta)},
},`
}
index += `
"${item.name}": {
name: "${item.name}",
description: "${item.description ?? ""}",
type: "${item.type}",
registryDependencies: ${JSON.stringify(item.registryDependencies)},
files: [${item.files?.map((file) => {
const filePath = `registry/new-york-v4/${typeof file === "string" ? file : file.path}`
const resolvedFilePath = path.resolve(filePath)
return typeof file === "string"
? `"${resolvedFilePath}"`
: `{
path: "${filePath}",
type: "${file.type}",
target: "${file.target ?? ""}"
}`
})}],
component: ${
componentPath
? `React.lazy(async () => {
const mod = await import("${componentPath}")
const exportName = Object.keys(mod).find(key => typeof mod[key] === 'function' || typeof mod[key] === 'object') || item.name
return { default: mod.default || mod[exportName] }
})`
: "null"
},
categories: ${JSON.stringify(item.categories)},
meta: ${JSON.stringify(item.meta)},
},`
}
index += `
}`
}`
console.log(`#️⃣ ${Object.keys(registry.items).length} items found`)
console.log(
`#️⃣ Built multi-style index with ${styles.length} styles: ${styles.map((s) => s.name).join(", ")}`
)
// Write style index.
// Write unified index.
rimraf.sync(path.join(process.cwd(), "registry/__index__.tsx"))
await fs.writeFile(path.join(process.cwd(), "registry/__index__.tsx"), index)
}
async function buildRegistryJsonFile() {
// 1. Fix the path for registry items.
async function buildRegistryJsonFile(styleName: string) {
// 1. Import the registry for this style.
const { registry: importedRegistry } = await import(
`../registry/${styleName}/registry.ts`
)
// 2. Validate the registry schema.
const parseResult = registrySchema.safeParse(importedRegistry)
if (!parseResult.success) {
console.error(`❌ Registry validation failed for ${styleName}:`)
console.error(parseResult.error.format())
throw new Error(`Invalid registry schema for ${styleName}`)
}
const registry = parseResult.data
// 3. Fix the path for registry items.
const fixedRegistry = {
...registry,
items: registry.items.map((item) => {
const files = item.files?.map((file) => {
return {
...file,
path: `registry/new-york-v4/${file.path}`,
path: `registry/${styleName}/${file.path}`,
}
})
@@ -87,36 +130,42 @@ async function buildRegistryJsonFile() {
}),
}
// 2. Write the content of the registry to `registry.json`
rimraf.sync(path.join(process.cwd(), `registry.json`))
await fs.writeFile(
path.join(process.cwd(), `registry.json`),
JSON.stringify(fixedRegistry, null, 2)
// 3. Create the output directory and write registry.json.
const outputDir = path.join(
process.cwd(),
styleName === "new-york-v4" ? `public/r/styles/${styleName}` : `public/r/${styleName}`
)
await fs.mkdir(outputDir, { recursive: true })
// 3. Format the registry.json file.
await exec(`prettier --write registry.json`)
// 4. Write registry.json to output directory and format it.
const registryJsonPath = path.join(outputDir, "registry.json")
await fs.writeFile(registryJsonPath, JSON.stringify(fixedRegistry, null, 2))
await new Promise<void>((resolve, reject) => {
execFile('prettier', ['--write', registryJsonPath], (error) => {
if (error) {
reject(error);
} else {
resolve();
}
});
})
// 3. Copy the registry.json to the www/public/r/styles/new-york-v4 directory.
await fs.cp(
path.join(process.cwd(), "registry.json"),
path.join(
process.cwd(),
"../www/public/r/styles/new-york-v4/registry.json"
),
{ recursive: true }
)
// 5. Write temporary registry file needed by shadcn build.
const tempRegistryPath = path.join(process.cwd(), `registry-${styleName}.json`)
await fs.writeFile(tempRegistryPath, JSON.stringify(fixedRegistry, null, 2))
}
async function buildRegistry() {
async function buildRegistry(styleName: string) {
return new Promise((resolve, reject) => {
// Use local shadcn copy.
const outputPath =
styleName === "new-york-v4" ? `public/r/styles/${styleName}` : `public/r/${styleName}`
const process = exec(
`node ../../packages/shadcn/dist/index.js build registry.json --output ../www/public/r/styles/new-york-v4`
`node ../../packages/shadcn/dist/index.js build registry-${styleName}.json --output ${outputPath}`
)
// exec(
// `pnpm dlx shadcn build registry.json --output ../www/public/r/styles/new-york-v4`
// `pnpm dlx shadcn build registry-${styleName}.json --output public/r/styles/${styleName}`
// )
process.on("exit", (code) => {
@@ -129,49 +178,6 @@ async function buildRegistry() {
})
}
async function syncRegistry() {
// Store the current registry content
const registryDir = path.join(process.cwd(), "registry")
const registryIndexPath = path.join(registryDir, "__index__.tsx")
let registryContent = null
try {
registryContent = await fs.readFile(registryIndexPath, "utf8")
} catch {
// File might not exist yet, that's ok
}
// 0. Copy registries.json from v4 to www before building www registry.
const v4RegistriesPath = path.join(process.cwd(), "public/r/registries.json")
const wwwRegistriesPath = path.resolve(
process.cwd(),
"../www/public/r/registries.json"
)
if (existsSync(v4RegistriesPath)) {
// Ensure the www/public/r directory exists.
await fs.mkdir(path.dirname(wwwRegistriesPath), { recursive: true })
// Copy registries.json to www.
await fs.cp(v4RegistriesPath, wwwRegistriesPath)
}
// 1. Call pnpm registry:build for www.
await exec("pnpm --filter=www registry:build")
// 2. Copy the www/public/r directory to v4/public/r.
rimraf.sync(path.join(process.cwd(), "public/r"))
await fs.cp(
path.resolve(process.cwd(), "../www/public/r"),
path.resolve(process.cwd(), "public/r"),
{ recursive: true }
)
// 3. Restore the registry content if we had it
if (registryContent) {
await fs.writeFile(registryIndexPath, registryContent, "utf8")
}
}
async function buildBlocksIndex() {
const blocks = await getAllBlocks(["registry:block"])
@@ -191,20 +197,35 @@ async function buildBlocksIndex() {
}
try {
console.log("🗂️ Building registry/__index__.tsx...")
await buildRegistryIndex()
const styles = Array.from(STYLES)
console.log(`🎨 Found ${styles.length} styles: ${styles.map((s) => s.name).join(", ")}`)
console.log("🗂️ Building registry/__blocks__.json...")
// Build unified multi-style index.
console.log("\n🗂 Building unified multi-style registry/__index__.tsx...")
await buildRegistryIndex(styles)
for (const style of styles) {
console.log(`\n📦 Processing style: ${style.name}`)
console.log(`💅 Building registry-${style.name}.json...`)
await buildRegistryJsonFile(style.name)
console.log(`🏗️ Building registry for ${style.name}...`)
await buildRegistry(style.name)
}
console.log("\n🗂 Building registry/__blocks__.json...")
await buildBlocksIndex()
console.log("💅 Building registry.json...")
await buildRegistryJsonFile()
// Clean up intermediate files.
console.log("\n🧹 Cleaning up intermediate files...")
for (const style of styles) {
if (existsSync(path.join(process.cwd(), `registry-${style.name}.json`))) {
await fs.unlink(path.join(process.cwd(), `registry-${style.name}.json`))
}
}
console.log("🏗️ Building registry...")
await buildRegistry()
console.log("🔄 Syncing registry...")
await syncRegistry()
console.log("\n✅ Build complete!")
} catch (error) {
console.error(error)
process.exit(1)