mirror of
https://github.com/shadcn-ui/ui.git
synced 2026-06-11 09:51:40 +00:00
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:
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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: {
|
||||
|
||||
105
apps/v4/app/(sandbox)/sandbox/[style]/page.tsx
Normal file
105
apps/v4/app/(sandbox)/sandbox/[style]/page.tsx
Normal 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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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()
|
||||
@@ -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 }>) => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 ||
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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'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"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
],
|
||||
|
||||
@@ -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'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",
|
||||
"type": "registry:example"
|
||||
}
|
||||
]
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||
14
apps/v4/registry/styles.ts
Normal file
14
apps/v4/registry/styles.ts
Normal 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)
|
||||
}
|
||||
@@ -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(
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user