This commit is contained in:
shadcn
2026-01-08 22:25:29 +04:00
parent cbe672151a
commit dced7f6045
12 changed files with 2843 additions and 2866 deletions

View File

@@ -4,7 +4,7 @@ import * as React from "react"
import { highlightCode } from "@/lib/highlight-code"
import { getDemoItem, getRegistryItem } from "@/lib/registry"
import { transformForDisplay } from "@/lib/rehype"
import { formatCode } from "@/lib/rehype"
import { cn } from "@/lib/utils"
import { CodeCollapsibleWrapper } from "@/components/code-collapsible-wrapper"
import { CopyButton } from "@/components/copy-button"
@@ -33,7 +33,6 @@ export async function ComponentSource({
let code: string | undefined
if (name) {
// Try demo item first, then fall back to registry item.
const item =
(await getDemoItem(name, styleName)) ??
(await getRegistryItem(name, styleName))
@@ -49,17 +48,9 @@ export async function ComponentSource({
return null
}
// Fix imports.
// Replace @/registry/${style}/ with @/components/.
code = code.replaceAll(`@/registry/${styleName}/`, "@/components/")
// Replace export default with export.
code = code.replaceAll("export default", "export")
code = await formatCode(code, styleName)
code = code.replaceAll("/* eslint-disable react/no-children-prop */\n", "")
// Apply transforms (cn-* → Tailwind, IconPlaceholder → icons, etc.).
code = await transformForDisplay(code, styleName)
const lang = language ?? title?.split(".").pop() ?? "tsx"
const highlightedCode = await highlightCode(code, lang)

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@ import {
export default function AccordionBorders() {
return (
<Accordion className="w-full">
<Accordion className="w-full max-w-sm">
<AccordionItem
value="billing"
className="border px-4 first:rounded-t-lg last:rounded-b-lg"

View File

@@ -14,7 +14,7 @@ import {
export default function AccordionCard() {
return (
<Card className="w-full">
<Card className="w-full max-w-sm">
<CardHeader>
<CardTitle>Subscription & Billing</CardTitle>
<CardDescription>

View File

@@ -7,7 +7,7 @@ import {
export default function AccordionMultiple() {
return (
<Accordion multiple className="w-full">
<Accordion multiple className="w-full max-w-md">
<AccordionItem value="item-1">
<AccordionTrigger>
What are the key considerations when implementing a comprehensive

View File

@@ -7,7 +7,7 @@ import {
export default function AccordionBorders() {
return (
<Accordion type="single" collapsible className="w-full">
<Accordion type="single" collapsible className="w-full max-w-sm">
<AccordionItem
value="billing"
className="border px-4 first:rounded-t-lg last:rounded-b-lg"

View File

@@ -14,7 +14,7 @@ import {
export default function AccordionCard() {
return (
<Card className="w-full">
<Card className="w-full max-w-sm">
<CardHeader>
<CardTitle>Subscription & Billing</CardTitle>
<CardDescription>

View File

@@ -7,7 +7,7 @@ import {
export default function AccordionMultiple() {
return (
<Accordion type="multiple" className="w-full">
<Accordion type="multiple" className="w-full max-w-md">
<AccordionItem value="item-1">
<AccordionTrigger>
What are the key considerations when implementing a comprehensive

View File

@@ -7,32 +7,26 @@ import { Project, ScriptKind } from "ts-morph"
import { type z } from "zod"
import { Index as StylesIndex } from "@/registry/__index__"
import { BASES } from "@/registry/bases"
import { Index as BasesIndex } from "@/registry/bases/__index__"
// Styles that have their own index in StylesIndex (built with style transforms).
const INDEXED_STYLES = ["new-york-v4"]
// Get the base name from a style name (e.g., "base-nova" -> "base").
// Returns null for legacy styles that don't use the examples system.
function getBaseForStyle(styleName: string) {
if (styleName.startsWith("radix-")) {
return "radix"
for (const base of BASES) {
if (styleName.startsWith(`${base.name}-`)) {
return base.name
}
}
if (styleName.startsWith("base-")) {
return "base"
}
// Legacy styles (e.g., "new-york-v4") don't use the examples system.
return null
}
// Get a demo component from the examples index.
export function getDemoComponent(name: string, styleName: string) {
const base = getBaseForStyle(styleName)
if (!base) return undefined
return ExamplesIndex[base]?.[name]?.component
}
// Get a demo item with file content from the examples index.
export async function getDemoItem(name: string, styleName: string) {
const base = getBaseForStyle(styleName)
if (!base) return null
@@ -58,30 +52,25 @@ export async function getDemoItem(name: string, styleName: string) {
}
}
// Map style names to their corresponding index and key.
function getIndexForStyle(styleName: string) {
// Use StylesIndex for styles that are built with transforms.
if (INDEXED_STYLES.includes(styleName)) {
return { index: StylesIndex, key: styleName }
}
// Fall back to BasesIndex for other base-style combinations.
if (styleName.startsWith("radix-")) {
return { index: BasesIndex, key: "radix" }
}
if (styleName.startsWith("base-")) {
return { index: BasesIndex, key: "base" }
const base = getBaseForStyle(styleName)
if (base) {
return { index: BasesIndex, key: base }
}
return { index: StylesIndex, key: styleName }
}
export function getRegistryComponent(name: string, styleName: string) {
// Check demo index first.
const demoComponent = getDemoComponent(name, styleName)
if (demoComponent) {
return demoComponent
}
// Fall back to registry.
const { index, key } = getIndexForStyle(styleName)
return index[key]?.[name]?.component
}

View File

@@ -1,6 +1,6 @@
import fs, { promises as fsPromises } from "fs"
import path from "path"
import type { configSchema } from "shadcn/schema"
import { ExamplesIndex } from "@/examples/__index__"
import {
createStyleMap,
transformIcons,
@@ -11,31 +11,45 @@ import {
import { Project, ScriptKind } from "ts-morph"
import { u } from "unist-builder"
import { visit } from "unist-util-visit"
import { type z } from "zod"
import { Index as StylesIndex } from "@/registry/__index__"
import { getActiveStyle } from "@/registry/_legacy-styles"
import { BASES } from "@/registry/bases"
import { Index as BasesIndex } from "@/registry/bases/__index__"
// Map style names to their corresponding index and key.
function getIndexForStyle(styleName: string) {
if (styleName.startsWith("radix-")) {
return { index: BasesIndex, key: "radix" }
function getBaseForStyle(styleName: string) {
for (const base of BASES) {
if (styleName.startsWith(`${base.name}-`)) {
return base.name
}
}
if (styleName.startsWith("base-")) {
return { index: BasesIndex, key: "base" }
return null
}
function getDemoFilePath(name: string, styleName: string) {
const base = getBaseForStyle(styleName)
if (!base) return null
const demo = ExamplesIndex[base]?.[name]
if (!demo) return null
return path.join(process.cwd(), demo.filePath)
}
function getIndexForStyle(styleName: string) {
const base = getBaseForStyle(styleName)
if (base) {
return { index: BasesIndex, key: base }
}
return { index: StylesIndex, key: styleName }
}
// Extract style name from compound style (e.g., "radix-nova" → "nova").
function getStyleFromStyleName(styleName: string) {
const parts = styleName.split("-")
return parts.length > 1 ? parts.slice(1).join("-") : styleName
}
// Build minimal config for transforms.
function buildDisplayConfig(styleName: string): z.infer<typeof configSchema> {
function buildDisplayConfig(styleName: string) {
return {
$schema: "https://ui.shadcn.com/schema.json",
style: styleName,
@@ -69,7 +83,6 @@ function buildDisplayConfig(styleName: string): z.infer<typeof configSchema> {
}
}
// Cache for style maps to avoid repeated file reads.
const styleMapCache = new Map<string, Record<string, string>>()
async function getStyleMap(styleName: string) {
@@ -89,18 +102,25 @@ async function getStyleMap(styleName: string) {
styleMapCache.set(style, styleMap)
return styleMap
} catch {
// Return empty style map if file not found.
return {}
}
}
export async function transformForDisplay(content: string, styleName: string) {
try {
// 1. Apply style transformation (cn-* → Tailwind classes).
const styleMap = await getStyleMap(styleName)
const transformed = await transformStyle(content, { styleMap })
export async function formatCode(code: string, styleName: string) {
code = code.replaceAll(`@/registry/${styleName}/`, "@/components/")
// 2. Apply icon/menu/render transforms.
for (const base of BASES) {
code = code.replaceAll(`@/registry/bases/${base.name}/`, "@/components/")
code = code.replaceAll(`@/examples/${base.name}/ui/`, "@/components/ui/")
code = code.replaceAll(`@/examples/${base.name}/lib/`, "@/lib/")
code = code.replaceAll(`@/examples/${base.name}/hooks/`, "@/hooks/")
}
code = code.replaceAll("export default", "export")
try {
const styleMap = await getStyleMap(styleName)
const transformed = await transformStyle(code, { styleMap })
const config = buildDisplayConfig(styleName)
const project = new Project({ compilerOptions: {} })
const sourceFile = project.createSourceFile("component.tsx", transformed, {
@@ -119,9 +139,8 @@ export async function transformForDisplay(content: string, styleName: string) {
return sourceFile.getText()
} catch (error) {
// Return original content if transform fails.
console.error("Transform failed:", error)
return content
return code
}
}
@@ -144,7 +163,6 @@ export interface UnistTree {
children: UnistNode[]
}
// Collected node info for async processing.
interface NodeToProcess {
node: UnistNode
type: "ComponentSource" | "ComponentPreview"
@@ -159,7 +177,6 @@ export function rehypeComponent() {
const activeStyle = await getActiveStyle()
const nodesToProcess: NodeToProcess[] = []
// First pass: collect all nodes that need processing.
visit(tree, (node: UnistNode) => {
const { value: srcPath } =
(getNodeAttributeByName(node, "src") as {
@@ -206,50 +223,44 @@ export function rehypeComponent() {
}
})
// Second pass: process all collected nodes asynchronously.
await Promise.all(
nodesToProcess.map(async (item) => {
try {
let src: string
let src: string | null = null
if (item.srcPath) {
src = path.join(process.cwd(), item.srcPath)
} else {
const { index, key } = getIndexForStyle(item.styleName)
const component = index[key]?.[item.name]
src = getDemoFilePath(item.name, item.styleName)
if (item.type === "ComponentSource" && item.fileName) {
src =
component.files.find((file: unknown) => {
if (typeof file === "string") {
return (
file.endsWith(`${item.fileName}.tsx`) ||
file.endsWith(`${item.fileName}.ts`)
)
}
return false
}) || component.files[0]?.path
} else {
src = component.files[0]?.path
if (!src) {
const { index, key } = getIndexForStyle(item.styleName)
const component = index[key]?.[item.name]
if (item.type === "ComponentSource" && item.fileName) {
src =
component.files.find((file: unknown) => {
if (typeof file === "string") {
return (
file.endsWith(`${item.fileName}.tsx`) ||
file.endsWith(`${item.fileName}.ts`)
)
}
return false
}) || component.files[0]?.path
} else {
src = component.files[0]?.path
}
}
}
// Read the source file.
let source = fs.readFileSync(src, "utf8")
if (!src) {
return
}
// Replace imports.
source = source.replaceAll(
`@/registry/${item.styleName}/`,
"@/components/"
)
source = source.replaceAll(`@/registry/bases/radix/`, "@/components/")
source = source.replaceAll(`@/registry/bases/base/`, "@/components/")
source = source.replaceAll("export default", "export")
const raw = fs.readFileSync(src, "utf8")
const source = await formatCode(raw, item.styleName)
// Apply transforms (cn-* → Tailwind, IconPlaceholder → icons, etc.).
source = await transformForDisplay(source, item.styleName)
// Add code as children so that rehype can take over at build time.
item.node.children?.push(
u("element", {
tagName: "pre",

View File

@@ -2,7 +2,7 @@ import { promises as fs } from "fs"
import path from "path"
import { rimraf } from "rimraf"
const BASES = ["base", "radix"] as const
import { BASES } from "@/registry/bases"
async function buildExamplesIndex() {
const cwd = process.cwd()
@@ -18,38 +18,36 @@ import * as React from "react"
export const ExamplesIndex: Record<string, Record<string, any>> = {`
for (const base of BASES) {
const baseDir = path.join(examplesDir, base)
const baseDir = path.join(examplesDir, base.name)
// Check if base directory exists.
try {
await fs.access(baseDir)
} catch {
console.log(` Skipping ${base} - directory does not exist`)
console.log(` Skipping ${base.name} - directory does not exist`)
continue
}
// Find all demo files (excluding subdirectories like ui/).
const allEntries = await fs.readdir(baseDir, { withFileTypes: true })
const files = allEntries
.filter((entry) => entry.isFile() && entry.name.endsWith(".tsx"))
.map((entry) => entry.name)
.sort()
console.log(` Found ${files.length} demos for ${base}`)
console.log(` Found ${files.length} demos for ${base.name}`)
index += `
"${base}": {`
"${base.name}": {`
for (const file of files) {
const name = file.replace(/\.tsx$/, "")
const filePath = `examples/${base}/${file}`
const filePath = `examples/${base.name}/${file}`
index += `
"${name}": {
name: "${name}",
filePath: "${filePath}",
component: React.lazy(async () => {
const mod = await import("./${base}/${name}")
const mod = await import("./${base.name}/${name}")
const exportName = Object.keys(mod).find(key => typeof mod[key] === 'function' || typeof mod[key] === 'object') || "${name}"
return { default: mod.default || mod[exportName] }
}),
@@ -64,7 +62,6 @@ export const ExamplesIndex: Record<string, Record<string, any>> = {`
}
`
// Write the index file.
const indexPath = path.join(examplesDir, "__index__.tsx")
rimraf.sync(indexPath)
await fs.writeFile(indexPath, index)

View File

@@ -488,51 +488,40 @@ async function buildConfig() {
}
async function copyUIToExamples() {
const bases = [
{ name: "base", sourceStyle: "base-nova" },
{ name: "radix", sourceStyle: "radix-nova" },
]
const defaultStyle = "nova"
const directories = ["ui", "lib", "hooks"]
for (const base of bases) {
for (const base of BASES) {
const sourceStyle = `${base.name}-${defaultStyle}`
for (const dir of directories) {
const fromDir = path.join(
process.cwd(),
`registry/${base.sourceStyle}/${dir}`
)
const fromDir = path.join(process.cwd(), `registry/${sourceStyle}/${dir}`)
const toDir = path.join(process.cwd(), `examples/${base.name}/${dir}`)
// Check if source directory exists.
try {
await fs.access(fromDir)
} catch {
console.log(
` ⚠️ registry/${base.sourceStyle}/${dir} not found, skipping`
)
console.log(` ⚠️ registry/${sourceStyle}/${dir} not found, skipping`)
continue
}
// Clean and create target directory.
rimraf.sync(toDir)
await fs.mkdir(toDir, { recursive: true })
// Copy all files and transform imports.
const files = await fs.readdir(fromDir)
for (const file of files) {
const sourcePath = path.join(fromDir, file)
const targetPath = path.join(toDir, file)
// Read, transform imports, and write.
let content = await fs.readFile(sourcePath, "utf-8")
content = content.replace(
new RegExp(`@/registry/${base.sourceStyle}/`, "g"),
new RegExp(`@/registry/${sourceStyle}/`, "g"),
`@/examples/${base.name}/`
)
await fs.writeFile(targetPath, content)
}
console.log(
` ✅ registry/${base.sourceStyle}/${dir} → examples/${base.name}/${dir}`
` ✅ registry/${sourceStyle}/${dir} → examples/${base.name}/${dir}`
)
}
}